From 6d4258e2ade74b099279ef0e7293ac365e11a631 Mon Sep 17 00:00:00 2001 From: Ryn Oliphant Date: Thu, 9 Oct 2025 15:43:08 -0400 Subject: [PATCH 1/8] adding distance code --- coxeter/shapes/_distance2d.py | 516 +++++++++++++++++++ coxeter/shapes/_distance3d.py | 580 ++++++++++++++++++++++ coxeter/shapes/convex_spheropolygon.py | 110 ++++ coxeter/shapes/convex_spheropolyhedron.py | 158 ++++++ coxeter/shapes/polygon.py | 118 +++++ coxeter/shapes/polyhedron.py | 162 ++++++ 6 files changed, 1644 insertions(+) create mode 100644 coxeter/shapes/_distance2d.py create mode 100644 coxeter/shapes/_distance3d.py diff --git a/coxeter/shapes/_distance2d.py b/coxeter/shapes/_distance2d.py new file mode 100644 index 00000000..26fc61b7 --- /dev/null +++ b/coxeter/shapes/_distance2d.py @@ -0,0 +1,516 @@ +from .polygon import Polygon +from .convex_spheropolygon import ConvexSpheropolygon +import numpy as np +import numpy.linalg as LA + +# --- "Hidden" Functions --- +#good? +def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: + ''' + Calculates the distances between several points and several varying lines. + + n is the total number of distance calculations that are being made. For example, let's say + we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the distances between: + - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W + n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] + + Args: + point (np.ndarray): the positions of the points [shape = (n, 3)] + vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] + edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] + + Returns: + np.ndarray: distances [shape = (n,)] + ''' + edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges + + dist = LA.norm(((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)), axis=1) #distances + return dist + +#good? +def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: + ''' + Calculates the displacements between several points and several varying lines. + + n is the total number of displacement calculations that are being made. For example, let's say + we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the displacements between: + - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W + n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] + + Args: + point (np.ndarray): the positions of the points [shape = (n, 3)] + vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] + edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] + + Returns: + np.ndarray: displacements [shape = (n, 3)] + ''' + edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges + + disp = ((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)) #displacements + return disp + +#good? +def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: + ''' + Calculates the distances between a single point and the plane of the polygon. + + Args: + point (np.ndarray): the positions of the points [shape=(n_points, 3)] + vert (np.ndarray): a point that lies on the plane of the polygon [shape=(3,)] + face_normal (np.ndarray): the normal that describes the plane of the polygon [shape=(3,)] + + Returns: + np.ndarray: distances [shape = (n_points,)] + ''' + + vert_point_vect = -1*vert + point + face_unit = face_normal / LA.norm(face_normal) #unit vector of the normal of the polygon + dist = abs(vert_point_vect@np.transpose(face_unit)) + + return dist + +#good? +def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: + ''' + Calculates the displacements between a single point and the plane of the polygon. + + Args: + point (np.ndarray): the positions of the points (shape=(n_points, 3)) + vert (np.ndarray): a point that lies on the plane of the polygon (shape=(3,)) + face_normal (np.ndarray): the normal that describes the plane of the polygon (shape=(3,)) + + Returns: + np.ndarray: displacements (n_points, 3) + ''' + vert_point_vect = -1*vert + point + face_unit = face_normal / LA.norm(face_normal) #unit vector of the normal of the polygon + disp = np.expand_dims(np.sum(vert_point_vect*face_unit, axis=1), axis=1) * face_unit *(-1) + + return disp + +#good input +def get_vert_zones (shape: Polygon): + ''' + Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones + where the shortest distance from any point that is within a vertex zone is the distance between the + point and the corresponding vertex. + + Args: + #will be just `self` once added into coxeter + + Returns: + dict: "constraint": np.ndarray [shape = (n_verts, 2, 3)], "bounds": np.ndarray [shape = (n_verts, 2)] + ''' + vert_constraint = np.append( + np.expand_dims(shape.edge_vectors,axis=1), + -1*np.expand_dims(np.append(np.expand_dims(shape.edge_vectors[-1],axis=0), shape.edge_vectors[:-1], axis=0), axis=1), + axis=1) + + vert_bounds = np.sum(vert_constraint * np.expand_dims(shape.vertices, axis=1), axis=2) + + return {"constraint": vert_constraint, "bounds":vert_bounds} + +#good input +def get_edge_zones (shape: Polygon): + ''' + Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones + where the shortest distance from any point that is within an edge zone is the distance between the + point and the corresponding edge. + + Args: + #will be just `self` once added into coxeter + + Returns: + dict: "constraint": np.ndarray [shape = (n_edges, 3, 3)], "bounds": np.ndarray [shape = (n_edges, 3)] + ''' + #vectors that are 90 degrees from the edges and point inwards + edges_90 = -1*np.expand_dims(np.cross(shape.edge_vectors, shape.normal), axis=1) + + #Calculating the constraint [shape = (n_edges, 3, 3)] + edge_constraint = np.append( -1*np.expand_dims(shape.edge_vectors, axis=1) , np.expand_dims(shape.edge_vectors, axis=1), axis=1 ) + edge_constraint = np.append( edge_constraint, edges_90 , axis=1) + + #Bounds [shape = (n_edges, 3)] + edge_bounds = np.zeros((shape.num_vertices, 3)) + edge_bounds[:,0] = np.sum(edge_constraint[:,0] *(shape.vertices), axis=1) + edge_bounds[:,1] = np.sum(edge_constraint[:,1] *(np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0],axis=0), axis=0)), axis=1) + edge_bounds[:,2] = np.sum(edge_constraint[:,2] *(np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0],axis=0), axis=0)), axis=1) + + return {"constraint":edge_constraint, "bounds":edge_bounds} + +#good input +def get_face_zones (shape: Polygon): + ''' + Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones + where the shortest distance from any point that is within a triangulated face zone is the distance between the + point and the corresponding triangulated face. + + Args: + #will be just `self` once added into coxeter + + Returns: + dict: "constraint": np.ndarray , "bounds": np.ndarray + ''' + face_constraint = np.cross(shape.edge_vectors, shape.normal) #only one face zone for a polygon | shape = (n_edges, 3) + face_bounds = np.sum(face_constraint * shape.vertices, axis=1) #shape = (n_edges,) + + #Checking to see if all the vertices are in the face zone. If not, the polygon is nonconvex. + if np.all(face_constraint @ np.transpose(shape.vertices) <= np.expand_dims(face_bounds, axis=1)+5e-6) == False: + #--Polygon is nonconvex and needs to be triangulated-- + triangle_verts =[] + + for tri in shape._triangulation(): + triangle_verts.append(list(tri)) + + triangle_verts = np.asarray(triangle_verts) + tri_edges = np.append(triangle_verts[:,1:], np.expand_dims(triangle_verts[:,0], axis=1), axis=1) - triangle_verts #edges point counterclockwise + + face_constraint = np.cross(tri_edges, shape.normal) #shape = (n_triangles, 3, 3) + face_bounds = np.sum(face_constraint*triangle_verts, axis=2) #shape = (n_triangles, 3) + + else: + #--Polygon is convex-- + face_constraint = np.expand_dims(face_constraint, axis=0) #shape = (1, n_edges, 3) + face_bounds = np.expand_dims(face_bounds, axis=0) #shape = (1, n_edges) + + return {"constraint":face_constraint, "bounds":face_bounds} + + +# --- User Available Functions --- +#good input +def shortest_distance_to_surface ( + shape: Polygon, + points: np.ndarray, + translation_vector: np.ndarray, + +) -> np.ndarray: + ''' + Solves for the shortest distance between points and the surface of a polygon. + If the point lies inside the polyhedron, the distance is negative. + + This function calculates the shortest distance by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + point lies in, determines the distance calculation(s) done. For a vertex zone, + the distance is calculated between a point and the vertex. For an edge zone, the + distance is calculated between a point and the edge. For a face zone, the distance + is calculated between a point and the face. Zones are allowed to overlap, and points + can be in more than one zone. By taking the minimum of all the calculated distances, + the shortest distances are found. + + Args: + points (list or np.ndarray): positions of the points + translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,) or (2,)] + + Returns: + np.ndarray: shortest distances [shape = (n_points,)] + ''' + + points = np.asarray(points) + translation_vector = np.asarray(translation_vector) + + if len(points.shape) == 1: + points = np.expand_dims(points, axis=0) + + n_points = len(points) #number of inputted points + + if points.shape[1] == 2: + points = np.append(points, np.zeros((n_points,1)), axis=1) + + if translation_vector.shape[0]>3 or len(translation_vector.shape)>1 or translation_vector.shape[0]<2: + raise ValueError(f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}") + + if translation_vector.shape[0] == 2: + translation_vector = np.append(translation_vector, [0]) + + #Updating bounds with the position of the polyhedron + vert_bounds = shape._vertex_zones["bounds"] + (shape._vertex_zones["constraint"] @ translation_vector) + edge_bounds = shape._edge_zones["bounds"] + (shape._edge_zones["constraint"] @ translation_vector) + face_bounds = shape._face_zones["bounds"] + (shape._face_zones["constraint"] @ translation_vector) + + + points_trans = np.transpose(points) + + max_value = 3*np.max(LA.norm(points - (translation_vector+shape.vertices[0]), axis=1)) + + min_dist_arr = np.ones((len(points),1))*max_value + + #Solving for the distances between the points and any relevant vertices + vert_bool = np.all((shape._vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + if np.any(vert_bool): + + #v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0,shape.num_vertices,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0,n_points,1), (shape.num_vertices,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + + vert_dist = np.ones((shape.num_vertices,n_points))*max_value + vert_dist[vert_bool]=LA.norm(points[v_points_used] - (shape.vertices[vert_used] + translation_vector), axis=1) #Distances between two points + vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) + + vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) + vert_dist = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) + + min_dist_arr = np.concatenate((min_dist_arr, vert_dist), axis=1) + + +# Solving for the distances between the points and any relevant edges + edge_bool = np.all((shape._edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + if np.any(edge_bool): + + #v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0,shape.num_vertices,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0,n_points,1), (shape.num_vertices,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool + + vert_on_edge = shape.vertices[shape.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges + edge_vectors = np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0) - shape.vertices + + edge_dist = np.ones((shape.num_vertices,n_points))*max_value + edge_dist[edge_bool]=point_to_edge_distance(points[e_points_used], vert_on_edge, edge_vectors[edge_used]) #Distances between a point and a line + edge_dist = np.transpose(edge_dist) #<--- shape = (n_points, n_edges) + + edge_dist_arg = np.expand_dims(np.argmin(abs(edge_dist), axis=1), axis=1) + edge_dist = np.take_along_axis(edge_dist, edge_dist_arg, axis=1) + + min_dist_arr = np.concatenate((min_dist_arr, edge_dist), axis=1) + + + face_bool = np.all((shape._face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + if np.any(face_bool): + + vert_on_face = shape.vertices[0] + translation_vector + face_dist = point_to_face_distance(points, vert_on_face, shape.normal) + face_dist = face_dist + max_value*(np.any(face_bool,axis=0) == False).astype(int) + face_dist = np.expand_dims(face_dist, axis=1) + + min_dist_arr = np.concatenate((min_dist_arr, face_dist), axis=1) + + true_min_dist = np.min(min_dist_arr, axis=1) + + return true_min_dist + +#good input +def shortest_displacement_to_surface ( + shape: Polygon, + points: np.ndarray, + translation_vector: np.ndarray, +) -> np.ndarray: + ''' + Solves for the shortest displacement between points and the surface of a polyhedron. + + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + point lies in, determines the displacement calculation(s) done. For a vertex zone, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, + and points can be in more than one zone. By taking the minimum of all the distances of + the calculated displacements, the shortest displacements are found. + + Args: + points (list or np.ndarray): positions of the points + translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,) or (2,)] + + Returns: + np.ndarray: shortest displacements [shape = (n_points, 3)] + ''' + points = np.asarray(points) + translation_vector = np.asarray(translation_vector) + + if points.shape == (3,) or points.shape == (2,): + points = np.expand_dims(points, axis=0) + + n_points = len(points) #number of inputted points + n_verts = shape.num_vertices #number of vertices = number of vertex zones + n_edges = n_verts #number of edges = number of edge zones + + if points.shape[1] == 2: + points = np.append(points, np.zeros((n_points,1)), axis=1) + + if translation_vector.shape[0]>3 or len(translation_vector.shape)>1 or translation_vector.shape[0]<2: + raise ValueError(f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}") + + if translation_vector.shape[0] == 2: + translation_vector = np.append(translation_vector, [0]) + + #Updating bounds with the position of the polyhedron + vert_bounds = shape._vertex_zones["bounds"] + (shape._vertex_zones["constraint"] @ translation_vector) + edge_bounds = shape._edge_zones["bounds"] + (shape._edge_zones["constraint"] @ translation_vector) + face_bounds = shape._face_zones["bounds"] + (shape._face_zones["constraint"] @ translation_vector) + + + points_trans = np.transpose(points) + + max_value = 3*np.max(LA.norm(points - (translation_vector+shape.vertices[0]), axis=1)) + + min_disp_arr = np.ones((n_points,1, 3))*max_value + + #Solving for the distances between the points and any relevant vertices + vert_bool = np.all((shape._vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + if np.any(vert_bool): + + #v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + + vert_disp = np.ones((n_verts,n_points,3))*max_value + vert_disp[vert_bool]=(shape.vertices[vert_used] + translation_vector) - points[v_points_used] #Displacements between two points + vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + + vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) + + +# Solving for the distances between the points and any relevant edges + edge_bool = np.all((shape._edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + if np.any(edge_bool): + + #v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool + + vert_on_edge = shape.vertices[shape.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges + edge_vectors = np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0) - shape.vertices + + edge_disp = np.ones((n_edges,n_points,3))*max_value + edge_disp[edge_bool]=point_to_edge_displacement(points[e_points_used], vert_on_edge, edge_vectors[edge_used]) #Displacements between a point and a line + edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) + + edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) + + + face_bool = np.all((shape._face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + if np.any(face_bool): + + face_disp = point_to_face_displacement(points, shape.vertices[0]+translation_vector, shape.normal) + np.repeat(np.expand_dims((max_value*(np.any(face_bool,axis=0) == False).astype(int)), axis=1), 3, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, np.expand_dims(face_disp, axis=1)), axis=1) + + disp_list_bool = np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1).reshape(n_points, 1, 1) + true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_list_bool, axis=1), axis=1) + + return true_min_disp + + +#think it is right/will work correctly? +def spheropolygon_shortest_displacement_to_surface ( + shape: ConvexSpheropolygon, + points: np.ndarray, + translation_vector: np.ndarray, +) -> np.ndarray: + ''' + Solves for the shortest displacement between points and the surface of a polyhedron. + + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + point lies in, determines the displacement calculation(s) done. For a vertex zone, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, + and points can be in more than one zone. By taking the minimum of all the distances of + the calculated displacements, the shortest displacements are found. + + Args: + points (list or np.ndarray): positions of the points + translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,) or (2,)] + + Returns: + np.ndarray: shortest displacements [shape = (n_points, 3)] + ''' + points = np.asarray(points) + translation_vector = np.asarray(translation_vector) + + if points.shape == (3,) or points.shape == (2,): + points = np.expand_dims(points, axis=0) + + n_points = len(points) #number of inputted points + n_verts = shape.num_vertices #number of vertices = number of vertex zones + n_edges = n_verts #number of edges = number of edge zones + + if points.shape[1] == 2: + points = np.append(points, np.zeros((n_points,1)), axis=1) + + if translation_vector.shape[0]>3 or len(translation_vector.shape)>1 or translation_vector.shape[0]<2: + raise ValueError(f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}") + + if translation_vector.shape[0] == 2: + translation_vector = np.append(translation_vector, [0]) + + #Updating bounds with the position of the polyhedron + vert_bounds = shape._vertex_zones["bounds"] + (shape._vertex_zones["constraint"] @ translation_vector) + edge_bounds = shape._edge_zones["bounds"] + (shape._edge_zones["constraint"] @ translation_vector) + face_bounds = shape._face_zones["bounds"] + (shape._face_zones["constraint"] @ translation_vector) + + + points_trans = np.transpose(points) + + max_value = 3*np.max(LA.norm(points - (translation_vector+shape.vertices[0]), axis=1)) + + min_disp_arr = np.ones((n_points,1, 3))*max_value + + #Solving for the distances between the points and any relevant vertices + vert_bool = np.all((shape._vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + if np.any(vert_bool): + + #v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + + vert_disp = np.ones((n_verts,n_points,3))*max_value + vert_disp[vert_bool]=(shape.vertices[vert_used] + translation_vector) - points[v_points_used] #Displacements between two points + vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + + vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + + #for spheropolygon + vert_projection = vert_disp - np.expand_dims(vert_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)) + v_projection_bool = np.linalg.norm(vert_projection, axis=1) > shape.radius + vert_projection[v_projection_bool] = shape.radius * vert_projection[v_projection_bool]/np.linalg.norm(vert_projection[v_projection_bool], axis=1) + vert_disp = vert_disp - vert_projection + + min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) + + +# Solving for the distances between the points and any relevant edges + edge_bool = np.all((shape._edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + if np.any(edge_bool): + + #v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool + + vert_on_edge = shape.vertices[shape.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges + edge_vectors = np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0) - shape.vertices + + edge_disp = np.ones((n_edges,n_points,3))*max_value + edge_disp[edge_bool]=point_to_edge_displacement(points[e_points_used], vert_on_edge, edge_vectors[edge_used]) #Displacements between a point and a line + edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) + + edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) + + #for spheropolygon + edge_projection = edge_disp - np.expand_dims(edge_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)) + e_projection_bool = np.linalg.norm(edge_projection, axis=1) > shape.radius + edge_projection[e_projection_bool] = shape.radius * edge_projection[e_projection_bool]/np.linalg.norm(edge_projection[e_projection_bool], axis=1) + edge_disp = edge_disp - edge_projection + + min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) + + + face_bool = np.all((shape._face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + if np.any(face_bool): + + face_disp = point_to_face_displacement(points, shape.vertices[0]+translation_vector, shape.normal) + np.repeat(np.expand_dims((max_value*(np.any(face_bool,axis=0) == False).astype(int)), axis=1), 3, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, np.expand_dims(face_disp, axis=1)), axis=1) + + disp_list_bool = np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1).reshape(n_points, 1, 1) + true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_list_bool, axis=1), axis=1) + + return true_min_disp diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py new file mode 100644 index 00000000..2127d180 --- /dev/null +++ b/coxeter/shapes/_distance3d.py @@ -0,0 +1,580 @@ +from .polyhedron import Polyhedron +import numpy as np +import numpy.linalg as LA + +#TODO: update docstrings + +#good input +def get_edge_face_neighbors (shape: Polyhedron) -> np.ndarray: + ''' + Gets the indices of the faces that are adjacent to each edge. + + Args: + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) + + Returns: + np.ndarray: the indices of the nearest faces for each edge [shape = (n_edges, 2)] + ''' + faces_len = shape.num_faces + num_edges = shape.num_edges + + #appending the vertex list of each face and a -1 to the end of each list of vertices, then flattening the awkward array (the -1 indicates a change in face) + faces_flat = [] + + for face in shape.faces: + face_extra = np.array([face[0], -1]) + + faces_flat = np.append(faces_flat, face) + faces_flat = np.append(faces_flat, face_extra) + + faces_flat = np.asarray(faces_flat) + + #creating a matrix where each row corresponds to an edge that contains the indices of its two corresponding vertices (a -1 index indicates a change in face) + list_len = len(faces_flat) + face_edge_mat = np.block([np.expand_dims(faces_flat[:-1],axis=1), np.expand_dims(faces_flat[1:],axis=1)]) + + #finding the number of edges associated with each face + fe_mat_inds = np.arange(0,list_len-1,1) + find_num_edges = fe_mat_inds[(fe_mat_inds==0) + (np.any(face_edge_mat==-1, axis=1))] + find_num_edges[:][0] = -1 + find_num_edges = find_num_edges.reshape(faces_len,2) + face_num_edges = find_num_edges[:,1] - find_num_edges[:,0] -1 + + #repeating each face index for the number of edges that are associated with it; length equals num_edges * 2 + face_correspond_inds = np.repeat(np.arange(0,faces_len,1), face_num_edges) + + #shape.edges lists the indices of the edge vertices lowest to highest. edges1_reshape lists the indices of the edge vertices highest to lowest + edges1_reshape = np.hstack((np.expand_dims(shape.edges[:,1], axis=1), np.expand_dims(shape.edges[:,0], axis=1))) + + #For the new_edge_ind_bool: rows correspond with the face_correspond_inds and columns correpond with the edge index; finding the neighboring faces for each edge + true_face_edge_mat = np.tile(np.expand_dims(face_edge_mat[np.all(face_edge_mat!=-1, axis=1)],axis=1), (1, num_edges,1)) + new_edge_ind_bool0 = np.all(true_face_edge_mat == np.expand_dims(shape.edges, axis=0), axis=2).astype(int) #faces to the LEFT of edges if edges are oriented pointing upwards + new_edge_ind_bool1 = np.all(true_face_edge_mat == np.expand_dims(edges1_reshape, axis=0), axis=2).astype(int) #faces to the RIGHT of edges if edges are oriented pointing upwards + + #tiling face_correspond_inds so it can be multiplied to the new_edge_ind_bool0 and new_edge_ind_bool1 + new_face_corr_inds = np.tile(np.expand_dims(face_correspond_inds, axis=1), (1,num_edges)) + + #getting the face indices and completing the edge-face neighbors + ef_neighbor0 = np.expand_dims(np.sum(new_face_corr_inds*new_edge_ind_bool0, axis=0), axis=1) #faces to the LEFT + ef_neighbor1 = np.expand_dims(np.sum(new_face_corr_inds*new_edge_ind_bool1, axis=0), axis=1) #faces to the RIGHT + ef_neighbor = np.hstack((ef_neighbor0, ef_neighbor1)) + + return ef_neighbor + +#good? +def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: + ''' + Calculates the distances between several points and several varying lines. + + n is the total number of distance calculations that are being made. For example, let's say + we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the distances between: + - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W + n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] + + Args: + point (np.ndarray): the positions of the points [shape = (n, 3)] + vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] + edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] + + Returns: + np.ndarray: distances [shape = (n,)] + ''' + edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges + + dist = LA.norm(((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)), axis=1) #distances + return dist + +#good? +def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: + ''' + Calculates the displacements between several points and several varying lines. + + n is the total number of displacement calculations that are being made. For example, let's say + we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the displacements between: + - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W + n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] + + Args: + point (np.ndarray): the positions of the points [shape = (n, 3)] + vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] + edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] + + Returns: + np.ndarray: displacements [shape = (n, 3)] + ''' + edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges + + disp = ((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)) #displacements + return disp + +#good? +def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: + ''' + Calculates the distances between several points and several varying planes. + + n is the total number of distance calculations that are being made. For example, let's say + we have points: A, B, C & D, and faces: P, Q & R, and we want to calculate the distances between: + - A & P, A & R, B & P, C & Q, C & R, D & P, D & Q, and D & R + n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [P,R,P,Q,R,P,Q.R] + + Args: + point (np.ndarray): the positions of the points [shape = (n, 3)] + vert (np.ndarray): points that lie on each corresponding plane [shape = (n, 3)] + face_normal (np.ndarray): the normals that describe each plane [shape = (n, 3)] + + Returns: + np.ndarray: distances [shape = (n,)] + ''' + vert_point_vect = point - vert #displacements between the points and relevent vertices + face_unit = face_normal / np.expand_dims(LA.norm(face_normal, axis=1), axis=1) #unit vectors of the normals of the faces + + dist = np.sum(vert_point_vect*face_unit, axis=1) #distances + + return dist + +#good? +def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: + ''' + Calculates the displacements between several points and several varying planes. + + n is the total number of displacement calculations that are being made. For example, let's say + we have points: A, B, C & D, and faces: P, Q & R, and we want to calculate the displacements between: + - A & P, A & R, B & P, C & Q, C & R, D & P, D & Q, and D & R + n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [P,R,P,Q,R,P,Q.R] + + Args: + point (np.ndarray): the positions of the points [shape = (n, 3)] + vert (np.ndarray): points that lie on each corresponding plane [shape = (n, 3)] + face_normal (np.ndarray): the normals that describe each plane [shape = (n, 3)] + + Returns: + np.ndarray: displacements [shape = (n, 3)] + ''' + vert_point_vect = point - vert #displacements between the points and relevent vertices + face_units = face_normal / np.expand_dims(LA.norm(face_normal, axis=1), axis=1) #unit vectors of the normals of the faces + + disp = np.expand_dims(np.sum(vert_point_vect*face_units, axis=1), axis=1) * face_units *(-1) #displacements + + return disp + +#good input +def get_vert_zones (shape: Polyhedron): + ''' + Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones + where the shortest distance from any point that is within a vertex zone is the distance between the + point and the corresponding vertex. + + Args: + vertices (np.ndarray): vertices of the shape + edges (np.ndarray): the vectors that correspond to each edge of the shape + ev_neighbors (np.ndarray): the indices of the vertices that correspond to each edge [shape = (n_edge, 2)] + + Returns: + dict: "constraint": np.ndarray [shape = (n_verts, n_edges, 3)], "bounds": np.ndarray [shape = (n_verts, n_edges)] + ''' + #For a generalized shape, we cannot assume that every vertex has the same number of edges connected to it + #(EX:vertices in a cube have 3 connected edges each, and for an icosahedron, vertices have 5 conncected edges). + #This would result in a ragged list for the constraint and bounds, which is not ideal. + + + + #v--- This `for`` loop is used to build and fill that ragged list with zeros, so that it makes an easy to work with array. ---v + for v_i in range(len(shape.vertices)): + pos_adj_edges = shape.edge_vectors[shape.edges[:,0] == v_i] #edges that point away from v_i + neg_adj_edges = (-1)*shape.edge_vectors[shape.edges[:,1] == v_i] #edges that point towards v_i, so have to multiply by -1 + adjacent_edges = np.append(pos_adj_edges, neg_adj_edges, axis=0) + + if v_i == 0: #initial vertex, start of building the constraint + vert_constraint = np.asarray([adjacent_edges]) + + else: + difference = len(adjacent_edges) - vert_constraint.shape[1] + #^---difference between the # of edges v_i has and the max # of edges from a previous vertex + + if difference < 0: #adjacent_edges needs to be filled with zeros to match the length of axis=1 for vert_constraint + adjacent_edges = np.append(adjacent_edges, np.zeros((abs(difference), 3)), axis=0) + + if difference > 0: #vert_constraint needs to be filled with zeros to match the # of edges for v_i + vert_constraint = np.append(vert_constraint, np.zeros((len(vert_constraint), difference, 3)), axis=1) + + vert_constraint = np.append(vert_constraint, np.asarray([adjacent_edges]), axis=0) + + vert_bounds = np.sum(vert_constraint * np.expand_dims(shape.vertices, axis=1), axis=2) + + return {"constraint":vert_constraint, "bounds":vert_bounds} + +#good input +def get_edge_zones (shape: Polyhedron,): + ''' + Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones + where the shortest distance from any point that is within an edge zone is the distance between the + point and the corresponding edge. + + Args: + shp_verts (np.ndarray): vertices of the shape + shp_edges (np.ndarray): the vectors that correspond to each edge of the shape + shp_faces (np.ndarray): the normals that correspond to each face of the shape + shp_edge_vert (np.ndarray): the indices of the vertices that correspond to each edge [shape = (n_edge, 2)] + shp_edge_face (np.ndarray): the indices of the faces that correspond to each edge [shape = (n_edge, 2)] + n_edges (int): the number of total edges for this shape + + Returns: + dict: "constraint": np.ndarray [shape = (n_edges, 4, 3)], "bounds": np.ndarray [shape = (n_edges, 4)] + ''' + #Set up + edge_constraint = np.zeros((shape.num_edges, 4, 3)) + edge_bounds = np.zeros((shape.num_edges, 4)) + + #Calculating the normals of the plane boundaries + edge_constraint[:,0] = shape.edge_vectors + edge_constraint[:,1] = -1*shape.edge_vectors + edge_constraint[:,2] = np.cross(shape.edge_vectors, shape.normals[shape.edge_face_neighbors[:,1]]) + edge_constraint[:,3] = -1*np.cross(shape.edge_vectors, shape.normals[shape.edge_face_neighbors[:,0]]) + #Constraint shape = (n_edges, 4, 3) + + #Bounds [shape = (n_edges, 4)] + edge_verts = np.zeros((shape.num_edges, 2, 3)) + edge_verts[:,0] = shape.vertices[shape.edges[:,0]] + edge_verts[:,1] = shape.vertices[shape.edges[:,1]] + + edge_bounds[:,0] = np.sum(edge_constraint[:,0] *(edge_verts[:,1]), axis=1) + edge_bounds[:,1] = np.sum(edge_constraint[:,1] *(edge_verts[:,0]), axis=1) + edge_bounds[:,2] = np.sum(edge_constraint[:,2] *(edge_verts[:,0]), axis=1) + edge_bounds[:,3] = np.sum(edge_constraint[:,3] *(edge_verts[:,0]), axis=1) + + return {"constraint":edge_constraint, "bounds":edge_bounds} + +#good input +def get_face_zones (shape: Polyhedron): + ''' + Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones + where the shortest distance from any point that is within a triangulated face zone is the distance between the + point and the corresponding triangulated face. + + Args: + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) + + Returns: + dict: "constraint": np.ndarray [shape = (n_tri_faces, 3, 3)], "bounds": np.ndarray [shape = (n_tri_faces, 3)], + "face_points": np.ndarray [shape= (n_tri_faces, 3)], "normals": np.ndarray [shape=(n_tri_faces, 3)] + ''' + #----- Triangulating the surface of the shape ----- + try: + #checking to see if faces are already triangulated + something = np.asarray(shape.faces).reshape(shape.num_faces,3) + + except: + #triangulating faces + triangle_verts = [] + for triangle in shape._surface_triangulation(): + triangle_verts.append(list(triangle)) + + triangle_verts = np.asarray(triangle_verts) #vertices of the triangulated faces + tri_edges = np.append(triangle_verts[:,1:], triangle_verts[:,0].reshape(len(triangle_verts),1,3), axis=1) - triangle_verts #edges point counterclockwise + tri_face_normals = np.cross(tri_edges[:,0], tri_edges[:,1]) #normals of the triangulated faces + + else: + triangle_verts = shape.vertices[shape.faces] #vertices of the triangulated faces + tri_edges = np.append(triangle_verts[:,1:], triangle_verts[:,0].reshape(len(triangle_verts),1,3), axis=1) - triangle_verts #edges point counterclockwise + tri_face_normals = shape.normals #normals of the triangulated faces + + + face_constraint = np.cross(tri_edges, np.expand_dims(tri_face_normals, axis=1)) #shape = (n_tri_faces, 3, 3) + face_bounds = np.sum(face_constraint*triangle_verts, axis=2) #shape = (n_tri_faces, 3) + + face_one_vertex = triangle_verts[:,0] #a point (vertex) that lies on each of the planes of the triangulated faces + + return {"constraint":face_constraint, "bounds":face_bounds, "face_points":face_one_vertex, "normals": tri_face_normals} + +#good input +def get_edge_normals(shape: Polyhedron) -> np.ndarray: + ''' + Gets the analogous normals of the edges of the polyhedron. The normals point outwards from the polyhedron + and are used to determine whether an edge zone is outside or inside the polyhedron. + + Args: + ef_neighbors (np.ndarray): the indices of the faces that correspond to each edge [shape = (n_edge, 2)] + face_normals (np.ndarray): the normals of the faces of the polyhedron [shape = (n_faces, 3)] + + Returns: + np.ndarray: analogous edge normals [shape = (n_edges, 3)] + ''' + face_unit = shape.normals / np.expand_dims(LA.norm(shape.normals, axis=1), axis=1) #unit vectors of the face normals + face_unit1 = face_unit[shape.edge_face_neighbors[:,0]] + face_unit2 = face_unit[shape.edge_face_neighbors[:,1]] + + edge_normals = face_unit1 + face_unit2 #sum of the adjacent face normals for each edge + + #returning the unit vectors of the edge normals + return edge_normals / np.expand_dims(LA.norm(edge_normals, axis=1), axis=1) + +#good input +def get_vert_normals(shape: Polyhedron) -> np.ndarray: + ''' + Gets the analogous normals of the vertices of the polyhedron. The normals point outwards from the polyhedron + and are used to determine whether a vertex zone is outside or inside the polyhedron. + + Args: + ev_neighbors (np.ndarray): the indices of the vertices that correspond to each edge [shape = (n_edge, 2)] + edge_normals (np.ndarray): the analogous normals of the edges of the polyhedron [shape = (n_edges, 3)] + + Returns: + np.ndarray: analogous vertex normals [shape = (n_verts, 3)] + ''' + n_edges = len(shape.edge_normals) + n_verts = np.max(shape.edges) +1 + + #Tiling for set up + nverts_edge_vert0 = np.tile(shape.edges[:,0], (n_verts, 1)) + nverts_edge_vert1 = np.tile(shape.edges[:,1], (n_verts, 1)) + vert_inds = np.arange(0, n_verts, 1).reshape((n_verts, 1)) + nverts_tile_edges = np.tile(shape.edge_normals, (n_verts, 1)).reshape((n_verts, n_edges, 3)) + + #Creating the bools needed to get the edges that correspond to each vertex + evbool0 = (np.expand_dims(nverts_edge_vert0 == vert_inds, axis=2)).astype(int) + evbool1 = (np.expand_dims(nverts_edge_vert1 == vert_inds, axis=2)).astype(int) + + #Applying the bools to find the corresponding edges + vert_edges = nverts_tile_edges * evbool0 + nverts_tile_edges * evbool1 + + vert_normals = np.sum(vert_edges, axis=1) #sum of the adjacent edge normals for each vertex + + #returning the unit vectors of the vertex normals + return vert_normals / np.expand_dims(LA.norm(vert_normals, axis=1), axis=1) + + + +#good input +def shortest_distance_to_surface ( + shp: Polyhedron, + points: np.ndarray, + translation_vector: np.ndarray, +) -> np.ndarray: + ''' + Solves for the shortest distance between points and the surface of a polyhedron. + If the point lies inside the polyhedron, the distance is negative. + + This function calculates the shortest distance by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + point lies in, determines the distance calculation(s) done. For a vertex zone, + the distance is calculated between a point and the vertex. For an edge zone, the + distance is calculated between a point and the edge. For a face zone, the distance + is calculated between a point and the face. Zones are allowed to overlap, and points + can be in more than one zone. By taking the minimum of all the calculated distances, + the shortest distances are found. + + Args: + points (list or np.ndarray): positions of the points [shape = (n_points, 3)] + translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,)] + + Returns: + np.ndarray: shortest distances [shape = (n_points,)] + ''' + points = np.asarray(points) + translation_vector = np.asarray(translation_vector) + + if translation_vector.shape[0]!=3 or len(translation_vector.shape)>1: + raise ValueError(f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}") + + if points.shape == (3,): + points = points.reshape(1, 3) + + + n_points = len(points) #number of inputted points + n_verts = shp.num_vertices #number of vertices = number of vertex zones + n_edges = shp.num_edges #number of edges = number of edge zones + n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones + + #arrays consisting of 1 or -1, and used to determine if a point is inside the polyhedron + vert_inside_mult = np.diag(np.all((shp.vertex_zones["constraint"] @ np.transpose(shp.vertex_normals+shp.vertices)) <= np.expand_dims(shp.vertex_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 + edge_inside_mult = np.diag(np.all((shp.edge_zones["constraint"] @ np.transpose(shp.edge_normals+shp.vertices[shp.edges[:,0]])) <= np.expand_dims(shp.edge_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 + + + #Updating bounds with the position of the polyhedron + vert_bounds = shp.vertex_zones["bounds"] + (shp.vertex_zones["constraint"] @ translation_vector) + edge_bounds = shp.edge_zones["bounds"] + (shp.edge_zones["constraint"] @ translation_vector) + face_bounds = shp.face_zones["bounds"] + (shp.face_zones["constraint"] @ translation_vector) + + points_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ points_trans' returns the right shape and values + max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances + min_dist_arr = np.ones((len(points),1))*max_value #Initial min_dist_arr + + #Solving for the distances between the points and any relevant vertices + vert_bool = np.all((shp.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) + if np.any(vert_bool): + + #v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + + #Calculating the distances + vert_dist = np.ones((n_verts,n_points))*max_value + vert_dist[vert_bool]=LA.norm(points[v_points_used] - (shp.vertices[vert_used] + translation_vector), axis=1)*vert_inside_mult[vert_used] #Distances between two points + vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) + + #Taking the minimum of the distances for each point + vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) + vert_dist = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) + + min_dist_arr = np.concatenate((min_dist_arr, vert_dist), axis=1) + + #Solving for the distances between the points and any relevant edges + edge_bool = np.all((shp.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (n_edges, n_points) + if np.any(edge_bool): + + #v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool + + vert_on_edge = shp.vertices[shp.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges + + #Calculating the distances + edge_dist = np.ones((n_edges,n_points))*max_value + edge_dist[edge_bool]=point_to_edge_distance(points[e_points_used], vert_on_edge, shp.edge_vectors[edge_used])*edge_inside_mult[edge_used] #Distances between a point and a line + edge_dist = np.transpose(edge_dist) #<--- shape = (n_points, n_edges) + + #Taking the minimum of the distances for each point + edge_dist_arg = np.expand_dims(np.argmin(abs(edge_dist), axis=1), axis=1) + edge_dist = np.take_along_axis(edge_dist, edge_dist_arg, axis=1) + + min_dist_arr = np.concatenate((min_dist_arr, edge_dist), axis=1) + + #Solving for the distances between the points and any relevant faces + face_bool = np.all((shp.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (n_tri_faces, n_points) + if np.any(face_bool): + + #v--- shape = (number of True in face_bool,) ---v + face_used = np.transpose(np.tile(np.arange(0,n_tri_faces,1), (n_points,1)))[face_bool] #Contains the indices of the triangulated faces that hold True for face_bool + f_points_used = np.tile(np.arange(0,n_points,1), (n_tri_faces,1))[face_bool] #Contains the indices of the points that hold True for face_bool + + vert_on_face = (shp.face_zones["face_points"][face_used]) + translation_vector #Vertices that lie on the needed faces + + #Calculating the distances + face_dist = np.ones((n_tri_faces,n_points))*max_value + face_dist[face_bool]=point_to_face_distance(points[f_points_used], vert_on_face, shp.face_zones["normals"][face_used]) #Distances between a point and a plane + face_dist = np.transpose(face_dist) #<--- shape = (n_points, n_tri_faces) + + #Taking the minimum of the distances for each point + face_dist_arg = np.expand_dims(np.argmin(abs(face_dist), axis=1), axis=1) + face_dist = np.take_along_axis(face_dist, face_dist_arg, axis=1) + + min_dist_arr = np.concatenate((min_dist_arr, face_dist), axis=1) + + min_dist_arg = np.expand_dims(np.argmin(abs(min_dist_arr), axis=1), axis=1) #determining the distances that are the shortest + true_min_dist = np.take_along_axis(min_dist_arr, min_dist_arg, axis=1).flatten() + + return true_min_dist + +#good input +def shortest_displacement_to_surface ( + shp: Polyhedron, + points: np.ndarray, + translation_vector: np.ndarray +) -> np.ndarray: + ''' + Solves for the shortest displacement between points and the surface of a polyhedron. + + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + point lies in, determines the displacement calculation(s) done. For a vertex zone, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, + and points can be in more than one zone. By taking the minimum of all the distances of + the calculated displacements, the shortest displacements are found. + + Args: + points (list or np.ndarray): positions of the points [shape = (n_points, 3)] + translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,)] + + Returns: + np.ndarray: shortest displacements [shape = (n_points, 3)] + ''' + points = np.asarray(points) + translation_vector = np.asarray(translation_vector) + + if translation_vector.shape[0]!=3 or len(translation_vector.shape)>1: + raise ValueError(f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}") + + if points.shape == (3,): + points = points.reshape(1, 3) + + n_points = len(points) #number of inputted points + n_verts = shp.num_vertices #number of vertices = number of vertex zones + n_edges = shp.num_edges #number of edges = number of edge zones + n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones + + #Updating bounds with the position of the polyhedron + vert_bounds = shp.vertex_zones["bounds"] + (shp.vertex_zones["constraint"] @ translation_vector) + edge_bounds = shp.edge_zones["bounds"] + (shp.edge_zones["constraint"] @ translation_vector) + face_bounds = shp.face_zones["bounds"] + (shp.face_zones["constraint"] @ translation_vector) + + coord_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values + max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances + min_disp_arr = np.ones((n_points,1, 3))*max_value #Initial min_disp_arr + + #Solving for the displacements between the points and any relevant vertices + vert_bool = np.all((shp.vertex_zones["constraint"] @ coord_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) + if np.any(vert_bool): + + #v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + vcoords_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + + #Calculating the displacements + vert_disp = np.ones((n_verts,n_points,3))*max_value + vert_disp[vert_bool]=(shp.vertices[vert_used] + translation_vector) - points[vcoords_used] #Displacements between two points + vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + + #Taking the minimum of the displacements for each point + vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) + + #Solving for the displacements between the points and any relevant edges + edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (n_edges, n_points) + if np.any(edge_bool): + + #v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool + ecoords_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool + + vert_on_edge = shp.vertices[shp.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges + + #Calculating the displacements + edge_disp = np.ones((n_edges,n_points,3))*max_value + edge_disp[edge_bool]=point_to_edge_displacement(points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used]) #Displacements between a point and a line + edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) + + #Taking the minimum of the displacements for each point + edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) + + #Solving for the displacements between the points and any relevant faces + face_bool = np.all((shp.face_zones["constraint"] @ coord_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (n_tri_faces, n_points) + if np.any(face_bool): + + #v--- shape = (number of True in face_bool,) ---v + face_used = np.transpose(np.tile(np.arange(0,n_tri_faces,1), (n_points,1)))[face_bool] #Contains the indices of the triangulated faces that hold True for face_bool + fcoords_used = np.tile(np.arange(0,n_points,1), (n_tri_faces,1))[face_bool] #Contains the indices of the points that hold True for face_bool + + vert_on_face = (shp.face_zones["face_points"][face_used]) + translation_vector #Vertices that lie on the needed faces + + #Calculating the displacements + face_disp = np.ones((n_tri_faces,n_points,3))*max_value + face_disp[face_bool]=point_to_face_displacement(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) #Displacements between a point and a plane + face_disp = np.transpose(face_disp, (1, 0, 2)) #<--- shape = (n_points, n_tri_faces, 3) + + #Taking the minimum of the displacements for each point + face_disp_arg = np.expand_dims(np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1,2)) + face_disp = np.take_along_axis(face_disp, face_disp_arg, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, face_disp), axis=1) + + disp_arr_bool = np.expand_dims(np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1), axis=(1,2)) #determining the displacements that are shortest + true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_arr_bool, axis=1), axis=1) + + return true_min_disp diff --git a/coxeter/shapes/convex_spheropolygon.py b/coxeter/shapes/convex_spheropolygon.py index 098d8061..1e61d01a 100644 --- a/coxeter/shapes/convex_spheropolygon.py +++ b/coxeter/shapes/convex_spheropolygon.py @@ -14,6 +14,13 @@ from .polygon import _align_points_by_normal from .utils import _hoomd_dict_mapping, _map_dict_keys +from ._distance2d import ( + get_vert_zones, + get_edge_zones, + get_face_zones, + spheropolygon_shortest_displacement_to_surface +) + class ConvexSpheropolygon(Shape2D): """A convex spheropolygon. @@ -56,6 +63,10 @@ def __init__(self, vertices, radius, normal=None): self._polygon = ConvexPolygon(vertices, normal) if not _is_convex(self.vertices, self._polygon.normal): raise ValueError("The vertices do not define a convex polygon.") + + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None @property def polygon(self): @@ -107,6 +118,9 @@ def _rescale(self, scale): """ self.polygon._vertices *= scale self.radius *= scale + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None @property def signed_area(self): @@ -302,3 +316,99 @@ def to_hoomd(self): self._polygon.centroid = old_centroid return hoomd_dict + + + @property + def vertex_zones(self): + """dict: Get the constraints and bounds needed to partition the + volume surrounding a polygon into zones where the shortest + distance from any point that is within a vertex zone is the + distance between the point and the corresponding vertex. + """ + if self._vertex_zones is None: + self._vertex_zones = get_vert_zones(self) + return self._vertex_zones + + @property + def edge_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a polygon into zones where the + shortest distance from any point that is within an edge zone + is the distance between the point and the corresponding edge. + """ + if self._edge_zones is None: + self._edge_zones = get_edge_zones(self) + return self._edge_zones + + @property + def face_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a polygon into zones where the shortest + distance from any point that is within the face zone + is the distance between the point and the face of the polygon. + """ + if self._face_zones is None: + self._face_zones = get_face_zones(self) + return self._face_zones + + def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest distance (magnitude) between points and + the surface of a polygon. + + This function calculates the shortest distance by partitioning + the space around a polygon into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the distance + calculation(s) done. For a vertex zone,the distance is calculated + between a point and the vertex. For an edge zone, the distance is + calculated between a point and the edge. For a face zone, the + distance is calculated between a point and the face. Zones are + allowed to overlap, and points can be in more than one zone. By + taking the minimum of all the calculated distances, the shortest + distances are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points,3) or (n_points,2)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the polygon [shape = (3,) of (2,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest distance of each point to the surface + [shape = (n_points,)] + """ + return np.linalg.norm(spheropolygon_shortest_displacement_to_surface(self, points, translation_vector), axis=1) + + def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest displacement (vector) between points and + the surface of a polygon. + + This function calculates the shortest displacement by partitioning + the space around a polygon into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the displacement + calculation(s) done. For a vertex zone, the displacement is + calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face + zone, the displacement is calculated between a point and the face. + Zones are allowed to overlap, and points can be in more than one + zone. By taking the minimum of all the distances of the calculated + displacements, the shortest displacements are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points,3) or (n_points,2)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the polygon [shape = (3,) or (2,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest displacement of each point to the surface + [shape = (n_points, 3)] + """ + return spheropolygon_shortest_displacement_to_surface(self, points, translation_vector) \ No newline at end of file diff --git a/coxeter/shapes/convex_spheropolyhedron.py b/coxeter/shapes/convex_spheropolyhedron.py index 6a4f2808..19d41ece 100644 --- a/coxeter/shapes/convex_spheropolyhedron.py +++ b/coxeter/shapes/convex_spheropolyhedron.py @@ -13,6 +13,17 @@ from .convex_polyhedron import ConvexPolyhedron from .utils import _hoomd_dict_mapping, _map_dict_keys +from ._distance3d import ( + get_edge_face_neighbors, + get_vert_zones, + get_edge_zones, + get_face_zones, + get_vert_normals, + get_edge_normals, + shortest_displacement_to_surface, + shortest_distance_to_surface +) + class ConvexSpheropolyhedron(Shape3D): """A convex spheropolyhedron. @@ -68,6 +79,12 @@ class ConvexSpheropolyhedron(Shape3D): def __init__(self, vertices, radius): self._polyhedron = ConvexPolyhedron(vertices) self.radius = radius + self._edge_face_neighbors = None + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None + self._vertex_normals = None + self._edge_normals = None @property def gsd_shape_spec(self): @@ -97,6 +114,9 @@ def _rescale(self, scale): """ self.polyhedron._rescale(scale) self.radius *= scale + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None @property def volume(self): @@ -364,3 +384,141 @@ def to_hoomd(self): self._polyhedron.centroid = old_centroid return hoomd_dict + + + @property + def edge_face_neighbors(self): + """:class:`numpy.ndarray`: Get the indices of the faces that + are adjacent to each edge. + + For a given edge vector oriented pointing upwards and from + an outside perspective of the convex spheropolyhedron, the + index of the face to the left of the edge is given by the + first column, and the index of the face to the right of + the edge is given by the second column. + """ + if self._edge_face_neighbors is None: + self._edge_face_neighbors = get_edge_face_neighbors(self) + return self._edge_face_neighbors + + @property + def vertex_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a convex spheropolyhedron into zones + where the shortest distance from any point that is within a + vertex zone is the distance between the point and the + corresponding vertex. + """ + if self._vertex_zones is None: + self._vertex_zones = get_vert_zones(self) + return self._vertex_zones + + @property + def edge_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a convex spheropolyhedron into zones + where the shortest distance from any point that is within an + edge zone is the distance between the point and the + corresponding edge. + """ + if self._edge_zones is None: + self._edge_zones = get_edge_zones(self) + return self._edge_zones + + @property + def face_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a convex spheropolyhedron into zones + where the shortest distance from any point that is within a + triangulated face zone is the distance between the point + and the corresponding triangulated face. + """ + if self._face_zones is None: + self._face_zones = get_face_zones(self) + return self._face_zones + + @property + def vertex_normals(self): + """:class:`numpy.ndarray`: Get the unit vector normals of vertices + + The normals point outwards from the convex spheropolyhedron. + """ + if self._vertex_normals is None: + self._vertex_normals = get_vert_normals(self) + return self._vertex_normals + + @property + def edge_normals(self): + """:class:`numpy.ndarray`: Get the unit vector normals of edges + + The normals point outwards from the convex spheropolyhedron. + """ + if self._edge_normals is None: + self._edge_normals = get_edge_normals(self) + return self._edge_normals + + def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest distance (magnitude) between points and + the surface of a spheropolyhedron. If the point lies inside the + spheropolyhedron, the distance is negative. + + This function calculates the shortest distance by partitioning + the space around a spheropolyhedron into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the distance + calculation(s) done. For a vertex zone,the distance is calculated + between a point and the vertex. For an edge zone, the distance is + calculated between a point and the edge. For a face zone, the + distance is calculated between a point and the face. Zones are + allowed to overlap, and points can be in more than one zone. By + taking the minimum of all the calculated distances, the shortest + distances are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points, 3)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the spheropolyhedron [shape = (3,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest distance of each point to the surface + [shape = (n_points,)] + """ + return shortest_distance_to_surface(self, points, translation_vector) - self.radius + + def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest displacement (vector) between points and + the surface of a spheropolyhedron. + + This function calculates the shortest displacement by partitioning + the space around a spheropolyhedron into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the displacement + calculation(s) done. For a vertex zone, the displacement is + calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face + zone, the displacement is calculated between a point and the face. + Zones are allowed to overlap, and points can be in more than one + zone. By taking the minimum of all the distances of the calculated + displacements, the shortest displacements are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points, 3)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the spheropolyhedron [shape = (3,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest displacement of each point to the surface + [shape = (n_points, 3)] + """ + displacement = shortest_displacement_to_surface(self, points, translation_vector) + unit_displacement = displacement / np.linalg.norm(displacement, axis=1) + return displacement - self.radius*unit_displacement + diff --git a/coxeter/shapes/polygon.py b/coxeter/shapes/polygon.py index ea750091..6635698e 100644 --- a/coxeter/shapes/polygon.py +++ b/coxeter/shapes/polygon.py @@ -20,6 +20,14 @@ translate_inertia_tensor, ) +from ._distance2d import ( + get_vert_zones, + get_edge_zones, + get_face_zones, + shortest_displacement_to_surface, + shortest_distance_to_surface +) + try: import miniball @@ -132,6 +140,10 @@ class Polygon(Shape2D): def __init__(self, vertices, normal=None, planar_tolerance=1e-5, test_simple=True): vertices = np.array(vertices, dtype=np.float64) + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None + if len(vertices.shape) != 2 or vertices.shape[1] not in (2, 3): raise ValueError("Vertices must be specified as an Nx2 or Nx3 array.") if len(vertices) < 3: @@ -215,6 +227,9 @@ def _rescale(self, scale): Scale factor. """ self._vertices *= scale + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None @property def perimeter(self): @@ -418,6 +433,13 @@ def centroid(self): def centroid(self, value): self._vertices += np.asarray(value) - self.centroid + if self._vertex_zones is not None: + self._vertex_zones["bounds"] += self._vertex_zones["constraint"] @ (np.asarray(value) - self.centroid) + if self._edge_zones is not None: + self._edge_zones["bounds"] += self._edge_zones["constraint"] @ (np.asarray(value) - self.centroid) + if self._face_zones is not None: + self._face_zones["bounds"] += self._face_zones["constraint"] @ (np.asarray(value) - self.centroid) + @property def edges(self): """:class:`numpy.ndarray`: Get the polygon's edges. @@ -838,3 +860,99 @@ def to_hoomd(self): self.centroid = old_centroid return hoomd_dict + + + @property + def vertex_zones(self): + """dict: Get the constraints and bounds needed to partition the + volume surrounding a polygon into zones where the shortest + distance from any point that is within a vertex zone is the + distance between the point and the corresponding vertex. + """ + if self._vertex_zones is None: + self._vertex_zones = get_vert_zones(self) + return self._vertex_zones + + @property + def edge_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a polygon into zones where the + shortest distance from any point that is within an edge zone + is the distance between the point and the corresponding edge. + """ + if self._edge_zones is None: + self._edge_zones = get_edge_zones(self) + return self._edge_zones + + @property + def face_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a polygon into zones where the shortest + distance from any point that is within the face zone + is the distance between the point and the face of the polygon. + """ + if self._face_zones is None: + self._face_zones = get_face_zones(self) + return self._face_zones + + def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest distance (magnitude) between points and + the surface of a polygon. + + This function calculates the shortest distance by partitioning + the space around a polygon into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the distance + calculation(s) done. For a vertex zone,the distance is calculated + between a point and the vertex. For an edge zone, the distance is + calculated between a point and the edge. For a face zone, the + distance is calculated between a point and the face. Zones are + allowed to overlap, and points can be in more than one zone. By + taking the minimum of all the calculated distances, the shortest + distances are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points,3) or (n_points,2)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the polygon [shape = (3,) of (2,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest distance of each point to the surface + [shape = (n_points,)] + """ + return shortest_distance_to_surface(self, points, translation_vector) + + def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest displacement (vector) between points and + the surface of a polygon. + + This function calculates the shortest displacement by partitioning + the space around a polygon into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the displacement + calculation(s) done. For a vertex zone, the displacement is + calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face + zone, the displacement is calculated between a point and the face. + Zones are allowed to overlap, and points can be in more than one + zone. By taking the minimum of all the distances of the calculated + displacements, the shortest displacements are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points,3) or (n_points,2)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the polygon [shape = (3,) or (2,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest displacement of each point to the surface + [shape = (n_points, 3)] + """ + return shortest_displacement_to_surface(self, points, translation_vector) \ No newline at end of file diff --git a/coxeter/shapes/polyhedron.py b/coxeter/shapes/polyhedron.py index 73bc865c..be4864e8 100644 --- a/coxeter/shapes/polyhedron.py +++ b/coxeter/shapes/polyhedron.py @@ -24,6 +24,17 @@ translate_inertia_tensor, ) +from ._distance3d import ( + get_edge_face_neighbors, + get_vert_zones, + get_edge_zones, + get_face_zones, + get_vert_normals, + get_edge_normals, + shortest_displacement_to_surface, + shortest_distance_to_surface +) + try: import miniball @@ -138,6 +149,12 @@ def __init__(self, vertices, faces, faces_are_convex=None): self._faces_are_convex = faces_are_convex self._find_equations() self._find_neighbors() + self._edge_face_neighbors = None + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None + self._vertex_normals = None + self._edge_normals = None def _find_equations(self): """Find the plane equations of the polyhedron faces.""" @@ -204,6 +221,9 @@ def _rescale(self, scale): """ self._vertices *= scale self._equations[:, 3] *= scale + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None def merge_faces(self, atol=1e-8, rtol=1e-5): """Merge coplanar faces to a given tolerance. @@ -591,6 +611,13 @@ def centroid(self, value): self._vertices += np.asarray(value) - self.centroid self._find_equations() + if self._vertex_zones is not None: + self._vertex_zones["bounds"] += self._vertex_zones["constraint"] @ (np.asarray(value) - self.centroid) + if self._edge_zones is not None: + self._edge_zones["bounds"] += self._edge_zones["constraint"] @ (np.asarray(value) - self.centroid) + if self._face_zones is not None: + self._face_zones["bounds"] += self._face_zones["constraint"] @ (np.asarray(value) - self.centroid) + @property def bounding_sphere(self): """:class:`~.Sphere`: Get the polyhedron's bounding sphere.""" @@ -1057,3 +1084,138 @@ def save(self, filetype, filename): "filetype must be one of the following: OBJ, OFF, " "STL, PLY, VTK, X3D, HTML" ) + + + + @property + def edge_face_neighbors(self): + """:class:`numpy.ndarray`: Get the indices of the faces that + are adjacent to each edge. + + For a given edge vector oriented pointing upwards and from + an outside perspective of the polyhedron, the index of the + face to the left of the edge is given by the first column, + and the index of the face to the right of the edge is + given by the second column. + """ + if self._edge_face_neighbors is None: + self._edge_face_neighbors = get_edge_face_neighbors(self) + return self._edge_face_neighbors + + @property + def vertex_zones(self): + """dict: Get the constraints and bounds needed to partition the + volume surrounding a polyhedron into zones where the shortest + distance from any point that is within a vertex zone is the + distance between the point and the corresponding vertex. + """ + if self._vertex_zones is None: + self._vertex_zones = get_vert_zones(self) + return self._vertex_zones + + @property + def edge_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a polyhedron into zones where the + shortest distance from any point that is within an edge zone + is the distance between the point and the corresponding edge. + """ + if self._edge_zones is None: + self._edge_zones = get_edge_zones(self) + return self._edge_zones + + @property + def face_zones(self): + """dict: Get the constraints and bounds needed to partition + the volume surrounding a polyhedron into zones where the shortest + distance from any point that is within a triangulated face zone + is the distance between the point and the corresponding + triangulated face. + """ + if self._face_zones is None: + self._face_zones = get_face_zones(self) + return self._face_zones + + @property + def vertex_normals(self): + """:class:`numpy.ndarray`: Get the unit vector normals of vertices + + The normals point outwards from the polyhedron. + """ + if self._vertex_normals is None: + self._vertex_normals = get_vert_normals(self) + return self._vertex_normals + + @property + def edge_normals(self): + """:class:`numpy.ndarray`: Get the unit vector normals of edges + + The normals point outwards from the polyhedron. + """ + if self._edge_normals is None: + self._edge_normals = get_edge_normals(self) + return self._edge_normals + + def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest distance (magnitude) between points and + the surface of a polyhedron. If the point lies inside the + polyhedron, the distance is negative. + + This function calculates the shortest distance by partitioning + the space around a polyhedron into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the distance + calculation(s) done. For a vertex zone,the distance is calculated + between a point and the vertex. For an edge zone, the distance is + calculated between a point and the edge. For a face zone, the + distance is calculated between a point and the face. Zones are + allowed to overlap, and points can be in more than one zone. By + taking the minimum of all the calculated distances, the shortest + distances are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points, 3)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the polyhedron [shape = (3,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest distance of each point to the surface + [shape = (n_points,)] + """ + return shortest_distance_to_surface(self, points, translation_vector) + + def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + """ + Solves for the shortest displacement (vector) between points and + the surface of a polyhedron. + + This function calculates the shortest displacement by partitioning + the space around a polyhedron into zones: vertex, edge, and face. + Determining the zone(s) a point lies in, determines the displacement + calculation(s) done. For a vertex zone, the displacement is + calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face + zone, the displacement is calculated between a point and the face. + Zones are allowed to overlap, and points can be in more than one + zone. By taking the minimum of all the distances of the calculated + displacements, the shortest displacements are found. + + Args: + points (list or :class:`numpy.ndarray`): + positions of the points [shape = (n_points, 3)] + translation_vector (list or :class:`numpy.ndarray`): + translation vector of the polyhedron [shape = (3,)] + (Default value: [0,0,0]) + + Returns + ------- + :class:`numpy.ndarray`: + the shortest displacement of each point to the surface + [shape = (n_points, 3)] + """ + return shortest_displacement_to_surface(self, points, translation_vector) + From e8414c96adcd1fd4b43074415bea0ce8eb12b7e7 Mon Sep 17 00:00:00 2001 From: Ryn Oliphant Date: Thu, 16 Oct 2025 14:58:01 -0400 Subject: [PATCH 2/8] adding weight edge and vertex normals --- coxeter/shapes/_distance3d.py | 77 +++++++++++++++++------ coxeter/shapes/convex_spheropolyhedron.py | 18 ++++++ coxeter/shapes/polyhedron.py | 23 ++++++- 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py index 2127d180..eeceb17f 100644 --- a/coxeter/shapes/_distance3d.py +++ b/coxeter/shapes/_distance3d.py @@ -4,7 +4,7 @@ #TODO: update docstrings -#good input +#good? def get_edge_face_neighbors (shape: Polyhedron) -> np.ndarray: ''' Gets the indices of the faces that are adjacent to each edge. @@ -157,7 +157,7 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: return disp -#good input +#good? def get_vert_zones (shape: Polyhedron): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones @@ -165,9 +165,7 @@ def get_vert_zones (shape: Polyhedron): point and the corresponding vertex. Args: - vertices (np.ndarray): vertices of the shape - edges (np.ndarray): the vectors that correspond to each edge of the shape - ev_neighbors (np.ndarray): the indices of the vertices that correspond to each edge [shape = (n_edge, 2)] + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) Returns: dict: "constraint": np.ndarray [shape = (n_verts, n_edges, 3)], "bounds": np.ndarray [shape = (n_verts, n_edges)] @@ -203,7 +201,7 @@ def get_vert_zones (shape: Polyhedron): return {"constraint":vert_constraint, "bounds":vert_bounds} -#good input +#good? def get_edge_zones (shape: Polyhedron,): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones @@ -211,12 +209,7 @@ def get_edge_zones (shape: Polyhedron,): point and the corresponding edge. Args: - shp_verts (np.ndarray): vertices of the shape - shp_edges (np.ndarray): the vectors that correspond to each edge of the shape - shp_faces (np.ndarray): the normals that correspond to each face of the shape - shp_edge_vert (np.ndarray): the indices of the vertices that correspond to each edge [shape = (n_edge, 2)] - shp_edge_face (np.ndarray): the indices of the faces that correspond to each edge [shape = (n_edge, 2)] - n_edges (int): the number of total edges for this shape + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) Returns: dict: "constraint": np.ndarray [shape = (n_edges, 4, 3)], "bounds": np.ndarray [shape = (n_edges, 4)] @@ -244,7 +237,7 @@ def get_edge_zones (shape: Polyhedron,): return {"constraint":edge_constraint, "bounds":edge_bounds} -#good input +#good? def get_face_zones (shape: Polyhedron): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones @@ -286,15 +279,14 @@ def get_face_zones (shape: Polyhedron): return {"constraint":face_constraint, "bounds":face_bounds, "face_points":face_one_vertex, "normals": tri_face_normals} -#good input +#good? def get_edge_normals(shape: Polyhedron) -> np.ndarray: ''' Gets the analogous normals of the edges of the polyhedron. The normals point outwards from the polyhedron and are used to determine whether an edge zone is outside or inside the polyhedron. Args: - ef_neighbors (np.ndarray): the indices of the faces that correspond to each edge [shape = (n_edge, 2)] - face_normals (np.ndarray): the normals of the faces of the polyhedron [shape = (n_faces, 3)] + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) Returns: np.ndarray: analogous edge normals [shape = (n_edges, 3)] @@ -308,15 +300,14 @@ def get_edge_normals(shape: Polyhedron) -> np.ndarray: #returning the unit vectors of the edge normals return edge_normals / np.expand_dims(LA.norm(edge_normals, axis=1), axis=1) -#good input +#good? def get_vert_normals(shape: Polyhedron) -> np.ndarray: ''' Gets the analogous normals of the vertices of the polyhedron. The normals point outwards from the polyhedron and are used to determine whether a vertex zone is outside or inside the polyhedron. Args: - ev_neighbors (np.ndarray): the indices of the vertices that correspond to each edge [shape = (n_edge, 2)] - edge_normals (np.ndarray): the analogous normals of the edges of the polyhedron [shape = (n_edges, 3)] + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) Returns: np.ndarray: analogous vertex normals [shape = (n_verts, 3)] @@ -343,6 +334,54 @@ def get_vert_normals(shape: Polyhedron) -> np.ndarray: return vert_normals / np.expand_dims(LA.norm(vert_normals, axis=1), axis=1) +def get_weighted_edge_normals(shape: Polyhedron) -> np.ndarray: + ''' + Gets the weighted normals of the edges of the polyhedron. The normals point outwards from the polyhedron. + + Args: + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) + + Returns: + np.ndarray: analogous edge normals [shape = (n_edges, 3)] + ''' + face_1 = shape.normals[shape.edge_face_neighbors[:,0]] + face_2 = shape.normals[shape.edge_face_neighbors[:,1]] + + edge_normals = face_1 + face_2 #sum of the adjacent face normals for each edge + + return edge_normals + +def get_weighted_vert_normals(shape: Polyhedron) -> np.ndarray: + ''' + Gets the weighted normals of the vertices of the polyhedron. The normals point outwards from the polyhedron. + + Args: + shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) + + Returns: + np.ndarray: analogous vertex normals [shape = (n_verts, 3)] + ''' + n_edges = len(shape.edge_normals) + n_verts = np.max(shape.edges) +1 + + #Tiling for set up + nverts_edge_vert0 = np.tile(shape.edges[:,0], (n_verts, 1)) + nverts_edge_vert1 = np.tile(shape.edges[:,1], (n_verts, 1)) + vert_inds = np.arange(0, n_verts, 1).reshape((n_verts, 1)) + nverts_tile_edges = np.tile(shape.weighted_edge_normals, (n_verts, 1)).reshape((n_verts, n_edges, 3)) + + #Creating the bools needed to get the edges that correspond to each vertex + evbool0 = (np.expand_dims(nverts_edge_vert0 == vert_inds, axis=2)).astype(int) + evbool1 = (np.expand_dims(nverts_edge_vert1 == vert_inds, axis=2)).astype(int) + + #Applying the bools to find the corresponding edges + vert_edges = nverts_tile_edges * evbool0 + nverts_tile_edges * evbool1 + + vert_normals = np.sum(vert_edges, axis=1) #sum of the adjacent weighted edge normals for each vertex + + return vert_normals + + #good input def shortest_distance_to_surface ( diff --git a/coxeter/shapes/convex_spheropolyhedron.py b/coxeter/shapes/convex_spheropolyhedron.py index 19d41ece..5bd70912 100644 --- a/coxeter/shapes/convex_spheropolyhedron.py +++ b/coxeter/shapes/convex_spheropolyhedron.py @@ -20,6 +20,8 @@ get_face_zones, get_vert_normals, get_edge_normals, + get_weighted_vert_normals, + get_weighted_edge_normals, shortest_displacement_to_surface, shortest_distance_to_surface ) @@ -457,6 +459,22 @@ def edge_normals(self): self._edge_normals = get_edge_normals(self) return self._edge_normals + @property + def weighted_vertex_normals(self): + """:class:`numpy.ndarray`: Get the weighted normals of vertices + + The normals point outwards from the polyhedron. + """ + return get_weighted_vert_normals(self) + + @property + def weighted_edge_normals(self): + """:class:`numpy.ndarray`: Get the weighted normals of edges + + The normals point outwards from the polyhedron. + """ + return get_weighted_edge_normals(self) + def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): """ Solves for the shortest distance (magnitude) between points and diff --git a/coxeter/shapes/polyhedron.py b/coxeter/shapes/polyhedron.py index be4864e8..57a77e0d 100644 --- a/coxeter/shapes/polyhedron.py +++ b/coxeter/shapes/polyhedron.py @@ -31,6 +31,8 @@ get_face_zones, get_vert_normals, get_edge_normals, + get_weighted_edge_normals, + get_weighted_vert_normals, shortest_displacement_to_surface, shortest_distance_to_surface ) @@ -1138,7 +1140,7 @@ def face_zones(self): @property def vertex_normals(self): - """:class:`numpy.ndarray`: Get the unit vector normals of vertices + """:class:`numpy.ndarray`: Get the unit vector normals of vertices. The normals point outwards from the polyhedron. """ @@ -1148,7 +1150,7 @@ def vertex_normals(self): @property def edge_normals(self): - """:class:`numpy.ndarray`: Get the unit vector normals of edges + """:class:`numpy.ndarray`: Get the unit vector normals of edges. The normals point outwards from the polyhedron. """ @@ -1156,6 +1158,23 @@ def edge_normals(self): self._edge_normals = get_edge_normals(self) return self._edge_normals + @property + def weighted_vertex_normals(self): + """:class:`numpy.ndarray`: Get the weighted normals of vertices. + + The normals point outwards from the polyhedron. + """ + return get_weighted_vert_normals(self) + + @property + def weighted_edge_normals(self): + """:class:`numpy.ndarray`: Get the weighted normals of edges. + + The normals point outwards from the polyhedron. + """ + return get_weighted_edge_normals(self) + + def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): """ Solves for the shortest distance (magnitude) between points and From 9907baa7b1ab8ff5969897ac287db48ff07acd2a Mon Sep 17 00:00:00 2001 From: Ryn Oliphant Date: Fri, 17 Oct 2025 13:59:14 -0400 Subject: [PATCH 3/8] starting to write tests --- coxeter/shapes/_distance2d.py | 50 ++++++++--------- coxeter/shapes/_distance3d.py | 21 ++++--- tests/test_polygon.py | 101 ++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 37 deletions(-) diff --git a/coxeter/shapes/_distance2d.py b/coxeter/shapes/_distance2d.py index 26fc61b7..3c209eef 100644 --- a/coxeter/shapes/_distance2d.py +++ b/coxeter/shapes/_distance2d.py @@ -1,5 +1,3 @@ -from .polygon import Polygon -from .convex_spheropolygon import ConvexSpheropolygon import numpy as np import numpy.linalg as LA @@ -90,7 +88,7 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: return disp #good input -def get_vert_zones (shape: Polygon): +def get_vert_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones where the shortest distance from any point that is within a vertex zone is the distance between the @@ -112,7 +110,7 @@ def get_vert_zones (shape: Polygon): return {"constraint": vert_constraint, "bounds":vert_bounds} #good input -def get_edge_zones (shape: Polygon): +def get_edge_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones where the shortest distance from any point that is within an edge zone is the distance between the @@ -140,7 +138,7 @@ def get_edge_zones (shape: Polygon): return {"constraint":edge_constraint, "bounds":edge_bounds} #good input -def get_face_zones (shape: Polygon): +def get_face_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones where the shortest distance from any point that is within a triangulated face zone is the distance between the @@ -180,7 +178,7 @@ def get_face_zones (shape: Polygon): # --- User Available Functions --- #good input def shortest_distance_to_surface ( - shape: Polygon, + shape, points: np.ndarray, translation_vector: np.ndarray, @@ -224,9 +222,9 @@ def shortest_distance_to_surface ( translation_vector = np.append(translation_vector, [0]) #Updating bounds with the position of the polyhedron - vert_bounds = shape._vertex_zones["bounds"] + (shape._vertex_zones["constraint"] @ translation_vector) - edge_bounds = shape._edge_zones["bounds"] + (shape._edge_zones["constraint"] @ translation_vector) - face_bounds = shape._face_zones["bounds"] + (shape._face_zones["constraint"] @ translation_vector) + vert_bounds = shape.vertex_zones["bounds"] + (shape.vertex_zones["constraint"] @ translation_vector) + edge_bounds = shape.edge_zones["bounds"] + (shape.edge_zones["constraint"] @ translation_vector) + face_bounds = shape.face_zones["bounds"] + (shape.face_zones["constraint"] @ translation_vector) points_trans = np.transpose(points) @@ -236,7 +234,7 @@ def shortest_distance_to_surface ( min_dist_arr = np.ones((len(points),1))*max_value #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shape._vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + vert_bool = np.all((shape.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) if np.any(vert_bool): #v--- shape = (number of True in vert_bool,) ---v @@ -254,7 +252,7 @@ def shortest_distance_to_surface ( # Solving for the distances between the points and any relevant edges - edge_bool = np.all((shape._edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + edge_bool = np.all((shape.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) if np.any(edge_bool): #v--- shape = (number of True in edge_bool,) ---v @@ -274,7 +272,7 @@ def shortest_distance_to_surface ( min_dist_arr = np.concatenate((min_dist_arr, edge_dist), axis=1) - face_bool = np.all((shape._face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + face_bool = np.all((shape.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) if np.any(face_bool): vert_on_face = shape.vertices[0] + translation_vector @@ -290,7 +288,7 @@ def shortest_distance_to_surface ( #good input def shortest_displacement_to_surface ( - shape: Polygon, + shape, points: np.ndarray, translation_vector: np.ndarray, ) -> np.ndarray: @@ -333,9 +331,9 @@ def shortest_displacement_to_surface ( translation_vector = np.append(translation_vector, [0]) #Updating bounds with the position of the polyhedron - vert_bounds = shape._vertex_zones["bounds"] + (shape._vertex_zones["constraint"] @ translation_vector) - edge_bounds = shape._edge_zones["bounds"] + (shape._edge_zones["constraint"] @ translation_vector) - face_bounds = shape._face_zones["bounds"] + (shape._face_zones["constraint"] @ translation_vector) + vert_bounds = shape.vertex_zones["bounds"] + (shape.vertex_zones["constraint"] @ translation_vector) + edge_bounds = shape.edge_zones["bounds"] + (shape.edge_zones["constraint"] @ translation_vector) + face_bounds = shape.face_zones["bounds"] + (shape.face_zones["constraint"] @ translation_vector) points_trans = np.transpose(points) @@ -345,7 +343,7 @@ def shortest_displacement_to_surface ( min_disp_arr = np.ones((n_points,1, 3))*max_value #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shape._vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + vert_bool = np.all((shape.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) if np.any(vert_bool): #v--- shape = (number of True in vert_bool,) ---v @@ -363,7 +361,7 @@ def shortest_displacement_to_surface ( # Solving for the distances between the points and any relevant edges - edge_bool = np.all((shape._edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + edge_bool = np.all((shape.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) if np.any(edge_bool): #v--- shape = (number of True in edge_bool,) ---v @@ -383,7 +381,7 @@ def shortest_displacement_to_surface ( min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) - face_bool = np.all((shape._face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + face_bool = np.all((shape.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) if np.any(face_bool): face_disp = point_to_face_displacement(points, shape.vertices[0]+translation_vector, shape.normal) + np.repeat(np.expand_dims((max_value*(np.any(face_bool,axis=0) == False).astype(int)), axis=1), 3, axis=1) @@ -398,7 +396,7 @@ def shortest_displacement_to_surface ( #think it is right/will work correctly? def spheropolygon_shortest_displacement_to_surface ( - shape: ConvexSpheropolygon, + shape, points: np.ndarray, translation_vector: np.ndarray, ) -> np.ndarray: @@ -441,9 +439,9 @@ def spheropolygon_shortest_displacement_to_surface ( translation_vector = np.append(translation_vector, [0]) #Updating bounds with the position of the polyhedron - vert_bounds = shape._vertex_zones["bounds"] + (shape._vertex_zones["constraint"] @ translation_vector) - edge_bounds = shape._edge_zones["bounds"] + (shape._edge_zones["constraint"] @ translation_vector) - face_bounds = shape._face_zones["bounds"] + (shape._face_zones["constraint"] @ translation_vector) + vert_bounds = shape.vertex_zones["bounds"] + (shape.vertex_zones["constraint"] @ translation_vector) + edge_bounds = shape.edge_zones["bounds"] + (shape.edge_zones["constraint"] @ translation_vector) + face_bounds = shape.face_zones["bounds"] + (shape.face_zones["constraint"] @ translation_vector) points_trans = np.transpose(points) @@ -453,7 +451,7 @@ def spheropolygon_shortest_displacement_to_surface ( min_disp_arr = np.ones((n_points,1, 3))*max_value #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shape._vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + vert_bool = np.all((shape.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) if np.any(vert_bool): #v--- shape = (number of True in vert_bool,) ---v @@ -477,7 +475,7 @@ def spheropolygon_shortest_displacement_to_surface ( # Solving for the distances between the points and any relevant edges - edge_bool = np.all((shape._edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + edge_bool = np.all((shape.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) if np.any(edge_bool): #v--- shape = (number of True in edge_bool,) ---v @@ -503,7 +501,7 @@ def spheropolygon_shortest_displacement_to_surface ( min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) - face_bool = np.all((shape._face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + face_bool = np.all((shape.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) if np.any(face_bool): face_disp = point_to_face_displacement(points, shape.vertices[0]+translation_vector, shape.normal) + np.repeat(np.expand_dims((max_value*(np.any(face_bool,axis=0) == False).astype(int)), axis=1), 3, axis=1) diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py index eeceb17f..c1bb035b 100644 --- a/coxeter/shapes/_distance3d.py +++ b/coxeter/shapes/_distance3d.py @@ -1,11 +1,10 @@ -from .polyhedron import Polyhedron import numpy as np import numpy.linalg as LA #TODO: update docstrings #good? -def get_edge_face_neighbors (shape: Polyhedron) -> np.ndarray: +def get_edge_face_neighbors (shape) -> np.ndarray: ''' Gets the indices of the faces that are adjacent to each edge. @@ -158,7 +157,7 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: return disp #good? -def get_vert_zones (shape: Polyhedron): +def get_vert_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones where the shortest distance from any point that is within a vertex zone is the distance between the @@ -202,7 +201,7 @@ def get_vert_zones (shape: Polyhedron): return {"constraint":vert_constraint, "bounds":vert_bounds} #good? -def get_edge_zones (shape: Polyhedron,): +def get_edge_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones where the shortest distance from any point that is within an edge zone is the distance between the @@ -238,7 +237,7 @@ def get_edge_zones (shape: Polyhedron,): return {"constraint":edge_constraint, "bounds":edge_bounds} #good? -def get_face_zones (shape: Polyhedron): +def get_face_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones where the shortest distance from any point that is within a triangulated face zone is the distance between the @@ -280,7 +279,7 @@ def get_face_zones (shape: Polyhedron): return {"constraint":face_constraint, "bounds":face_bounds, "face_points":face_one_vertex, "normals": tri_face_normals} #good? -def get_edge_normals(shape: Polyhedron) -> np.ndarray: +def get_edge_normals(shape) -> np.ndarray: ''' Gets the analogous normals of the edges of the polyhedron. The normals point outwards from the polyhedron and are used to determine whether an edge zone is outside or inside the polyhedron. @@ -301,7 +300,7 @@ def get_edge_normals(shape: Polyhedron) -> np.ndarray: return edge_normals / np.expand_dims(LA.norm(edge_normals, axis=1), axis=1) #good? -def get_vert_normals(shape: Polyhedron) -> np.ndarray: +def get_vert_normals(shape) -> np.ndarray: ''' Gets the analogous normals of the vertices of the polyhedron. The normals point outwards from the polyhedron and are used to determine whether a vertex zone is outside or inside the polyhedron. @@ -334,7 +333,7 @@ def get_vert_normals(shape: Polyhedron) -> np.ndarray: return vert_normals / np.expand_dims(LA.norm(vert_normals, axis=1), axis=1) -def get_weighted_edge_normals(shape: Polyhedron) -> np.ndarray: +def get_weighted_edge_normals(shape) -> np.ndarray: ''' Gets the weighted normals of the edges of the polyhedron. The normals point outwards from the polyhedron. @@ -351,7 +350,7 @@ def get_weighted_edge_normals(shape: Polyhedron) -> np.ndarray: return edge_normals -def get_weighted_vert_normals(shape: Polyhedron) -> np.ndarray: +def get_weighted_vert_normals(shape) -> np.ndarray: ''' Gets the weighted normals of the vertices of the polyhedron. The normals point outwards from the polyhedron. @@ -385,7 +384,7 @@ def get_weighted_vert_normals(shape: Polyhedron) -> np.ndarray: #good input def shortest_distance_to_surface ( - shp: Polyhedron, + shp, points: np.ndarray, translation_vector: np.ndarray, ) -> np.ndarray: @@ -506,7 +505,7 @@ def shortest_distance_to_surface ( #good input def shortest_displacement_to_surface ( - shp: Polyhedron, + shp, points: np.ndarray, translation_vector: np.ndarray ) -> np.ndarray: diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 40205d8c..4ec9a174 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -641,3 +641,104 @@ def test_edge_lengths(poly): np.diff(poly.vertices[expected_edges], axis=1).squeeze(), axis=1 ) np.testing.assert_allclose(poly.edge_lengths, lengths) + + + +def test_shortest_distance_convex(): + tri_verts = np.array([[0, 0.5], [-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25]]) + triangle = ConvexPolygon(vertices=tri_verts) + + x_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1]])#, [3+0.25*np.sqrt(3),4,0]]) + + distances = triangle.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,0])) + displacements = triangle.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,0])) + + true_distances = np.array([0.3080127018, 0.25, 0, 1, 1.0231690965]) + true_displacements = np.array([[-0.2667468246, -0.1540063509, 0], [0,-0.25,0], [0,0,0],[0,0,-1],[-0.1875, -0.1082531755, 1]]) + + np.testing.assert_allclose(distances, true_distances) + np.testing.assert_allclose(displacements, true_displacements) + +@pytest.mark.skip +def test_shortest_distance_concave(): + verts = np.array([[0,0.5],[-0.125,0.75],[-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25],[0.25*np.sqrt(3),0.75]]) + concave_poly = Polygon(vertices=verts) + + x_points = np.array([]) + + distances = concave_poly.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,0])) + displacements = concave_poly.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,0])) + + true_distances = np.array([]) + true_displacements = np.array([]) + + np.testing.assert_allclose(distances, true_distances) + np.testing.assert_allclose(displacements, true_displacements) + +def test_shortest_distance_general(): + """ + seed 2 works + seed 3 is flipped about [-1,1,1] + """ + np.random.seed(4) + random_angles = np.random.rand(15)*2*np.pi #angles + sorted_angles = np.sort(random_angles) + random_dist = np.random.rand(15)*10 #from origin + + vertices = np.zeros((15,2)) + vertices[:,0] = random_dist * np.cos(sorted_angles) #x + vertices[:,1] = random_dist * np.sin(sorted_angles) #y + + poly = Polygon(vertices=vertices) + points = np.random.rand(50, 2)*20 -10 + points = points[~poly.is_inside(points)] + + import matplotlib.pyplot as plt + fig, ax = plt.subplots() + poly.plot(ax=ax) + + + distances = poly.shortest_distance_to_surface(points) + displacements = poly.shortest_displacement_to_surface(points) + + np.testing.assert_allclose(distances, np.linalg.norm(displacements, axis=1)) + + triangle_verts =[] + for tri in poly._triangulation(): + triangle_verts.append(list(tri)) + + triangle_verts = np.asarray(triangle_verts) + for t in triangle_verts: + ax.plot(*(np.array([*t, t[0]]) )[:, :2].T, c="k", alpha=0.5, linestyle="dashed") + tri_edges = np.append(triangle_verts[:,1:], np.expand_dims(triangle_verts[:,0], axis=1), axis=1) - triangle_verts #edges point counterclockwise + + edges_90 = np.cross(tri_edges, poly.normal) #point outwards (n_triangles, 3, 3) + upper_bounds = np.sum(edges_90*triangle_verts, axis=2) #(n_triangles, 3) + + def scipy_closest_point(point, edges_90, upper_bounds): + point = np.append(point, [0]) + from scipy.optimize import LinearConstraint, minimize + all_tri_distances = [] + tmps = [] + for triangle in zip(edges_90, upper_bounds, strict=True): + tri_min_point = minimize( + fun=lambda pt: np.linalg.norm(pt - point), # Function to optimize + x0=np.zeros(3), # Initial guess + constraints=[LinearConstraint(triangle[0].squeeze(), -np.inf, triangle[1].squeeze())], + tol=1e-8 + ) + tmps.append(tri_min_point.x) + triangle_distance = np.linalg.norm(tri_min_point.x - point) + all_tri_distances.append(triangle_distance) + ax.scatter(*tmps[np.argmin(all_tri_distances)][:2], c = "r", marker="x") + return np.min(all_tri_distances) + + scipy_distances = [] + for point in points: + + scipy_dist = scipy_closest_point(point, edges_90, upper_bounds) + scipy_distances.append(scipy_dist) + scipy_distances = np.asarray(scipy_distances) + ax.scatter(*(displacements[:, :2]+ points).T,c="b") + plt.show() + np.testing.assert_allclose(distances, scipy_distances, atol=2e-8) \ No newline at end of file From a03cdb151cb2a9894c839446061932b462616f62 Mon Sep 17 00:00:00 2001 From: Ryn Oliphant Date: Thu, 4 Dec 2025 13:29:57 -0500 Subject: [PATCH 4/8] polyhedron tests done, some bug fixes, and improved runtime --- coxeter/shapes/_distance2d.py | 14 +- coxeter/shapes/_distance3d.py | 101 +++++++++----- coxeter/shapes/convex_polyhedron.py | 8 ++ coxeter/shapes/convex_spheropolygon.py | 30 ++++ tests/test_polygon.py | 112 +++++++++++---- tests/test_polyhedron.py | 182 +++++++++++++++++++++++++ tests/test_spheropolygon.py | 153 +++++++++++++++++++++ 7 files changed, 527 insertions(+), 73 deletions(-) diff --git a/coxeter/shapes/_distance2d.py b/coxeter/shapes/_distance2d.py index 3c209eef..5be31722 100644 --- a/coxeter/shapes/_distance2d.py +++ b/coxeter/shapes/_distance2d.py @@ -62,7 +62,7 @@ def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np. np.ndarray: distances [shape = (n_points,)] ''' - vert_point_vect = -1*vert + point + vert_point_vect = vert - point face_unit = face_normal / LA.norm(face_normal) #unit vector of the normal of the polygon dist = abs(vert_point_vect@np.transpose(face_unit)) @@ -81,9 +81,9 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: Returns: np.ndarray: displacements (n_points, 3) ''' - vert_point_vect = -1*vert + point + vert_point_vect = vert - point face_unit = face_normal / LA.norm(face_normal) #unit vector of the normal of the polygon - disp = np.expand_dims(np.sum(vert_point_vect*face_unit, axis=1), axis=1) * face_unit *(-1) + disp = np.expand_dims(np.sum(vert_point_vect*face_unit, axis=1), axis=1) * face_unit #*(-1) return disp @@ -466,9 +466,9 @@ def spheropolygon_shortest_displacement_to_surface ( vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) #for spheropolygon - vert_projection = vert_disp - np.expand_dims(vert_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)) + vert_projection = np.squeeze(vert_disp - np.expand_dims(vert_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)), axis=1) v_projection_bool = np.linalg.norm(vert_projection, axis=1) > shape.radius - vert_projection[v_projection_bool] = shape.radius * vert_projection[v_projection_bool]/np.linalg.norm(vert_projection[v_projection_bool], axis=1) + vert_projection[v_projection_bool] = shape.radius * vert_projection[v_projection_bool]/np.expand_dims(np.linalg.norm(vert_projection[v_projection_bool], axis=1), axis=1) vert_disp = vert_disp - vert_projection min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) @@ -493,9 +493,9 @@ def spheropolygon_shortest_displacement_to_surface ( edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) #for spheropolygon - edge_projection = edge_disp - np.expand_dims(edge_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)) + edge_projection = np.squeeze(edge_disp - np.expand_dims(edge_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)), axis=1) e_projection_bool = np.linalg.norm(edge_projection, axis=1) > shape.radius - edge_projection[e_projection_bool] = shape.radius * edge_projection[e_projection_bool]/np.linalg.norm(edge_projection[e_projection_bool], axis=1) + edge_projection[e_projection_bool] = shape.radius * edge_projection[e_projection_bool]/np.expand_dims(np.linalg.norm(edge_projection[e_projection_bool], axis=1), axis=1) edge_disp = edge_disp - edge_projection min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py index c1bb035b..b871edb0 100644 --- a/coxeter/shapes/_distance3d.py +++ b/coxeter/shapes/_distance3d.py @@ -124,10 +124,10 @@ def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np. Returns: np.ndarray: distances [shape = (n,)] ''' - vert_point_vect = point - vert #displacements between the points and relevent vertices + vert_point_vect = vert - point #displacements between the points and relevent vertices face_unit = face_normal / np.expand_dims(LA.norm(face_normal, axis=1), axis=1) #unit vectors of the normals of the faces - dist = np.sum(vert_point_vect*face_unit, axis=1) #distances + dist = np.sum(vert_point_vect*face_unit, axis=1) * (-1) #distances return dist @@ -149,10 +149,10 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: Returns: np.ndarray: displacements [shape = (n, 3)] ''' - vert_point_vect = point - vert #displacements between the points and relevent vertices + vert_point_vect = vert - point #displacements between the points and relevent vertices face_units = face_normal / np.expand_dims(LA.norm(face_normal, axis=1), axis=1) #unit vectors of the normals of the faces - disp = np.expand_dims(np.sum(vert_point_vect*face_units, axis=1), axis=1) * face_units *(-1) #displacements + disp = np.expand_dims(np.sum(vert_point_vect*face_units, axis=1), axis=1) * face_units #*(-1) #displacements return disp @@ -425,7 +425,7 @@ def shortest_distance_to_surface ( #arrays consisting of 1 or -1, and used to determine if a point is inside the polyhedron vert_inside_mult = np.diag(np.all((shp.vertex_zones["constraint"] @ np.transpose(shp.vertex_normals+shp.vertices)) <= np.expand_dims(shp.vertex_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 - edge_inside_mult = np.diag(np.all((shp.edge_zones["constraint"] @ np.transpose(shp.edge_normals+shp.vertices[shp.edges[:,0]])) <= np.expand_dims(shp.edge_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 + edge_inside_mult = np.diag(np.all((shp.edge_zones["constraint"] @ np.transpose(shp.edge_normals+0.5*(shp.vertices[shp.edges[:,0]]+shp.vertices[shp.edges[:,1]]))) <= np.expand_dims(shp.edge_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 #Updating bounds with the position of the polyhedron @@ -435,29 +435,47 @@ def shortest_distance_to_surface ( points_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ points_trans' returns the right shape and values max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - min_dist_arr = np.ones((len(points),1))*max_value #Initial min_dist_arr + # min_dist_arr = np.ones((len(points),1))*max_value #Initial min_dist_arr - #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shp.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) - if np.any(vert_bool): - #v--- shape = (number of True in vert_bool,) ---v - vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + + #Calculating the distances + + + vert_dist=LA.norm(np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1) - np.expand_dims(shp.vertices + translation_vector, axis=0), axis=2)*np.expand_dims(vert_inside_mult, axis=0) #Distances between two points + # vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) + + #Taking the minimum of the distances for each point + vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) + min_dist_arr = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) + + + # print(min_dist_arr) + atol = 1e-8 + + + # Solving for the distances between the points and any relevant vertices + # vert_bool = np.all((shp.vertex_zones["constraint"] @ points_trans) <= (np.expand_dims(vert_bounds, axis=2)+atol), axis=1) #<--- shape = (n_verts, n_points) + # if np.any(vert_bool): + + # #v--- shape = (number of True in vert_bool,) ---v + # vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + # v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - #Calculating the distances - vert_dist = np.ones((n_verts,n_points))*max_value - vert_dist[vert_bool]=LA.norm(points[v_points_used] - (shp.vertices[vert_used] + translation_vector), axis=1)*vert_inside_mult[vert_used] #Distances between two points - vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) + # #Calculating the distances + # vert_dist = np.ones((n_verts,n_points))*max_value + # vert_dist[vert_bool]=LA.norm(points[v_points_used] - (shp.vertices[vert_used] + translation_vector), axis=1)*vert_inside_mult[vert_used] #Distances between two points + # vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) - #Taking the minimum of the distances for each point - vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) - vert_dist = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) + # #Taking the minimum of the distances for each point + # vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) + # vert_dist = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) - min_dist_arr = np.concatenate((min_dist_arr, vert_dist), axis=1) + # min_dist_arr = np.concatenate((min_dist_arr, vert_dist), axis=1) #Solving for the distances between the points and any relevant edges - edge_bool = np.all((shp.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (n_edges, n_points) + edge_bool = np.all((shp.edge_zones["constraint"] @ points_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) + # edge_bool = edge_bool + np.allclose((shp.edge_zones["constraint"] @ points_trans), np.expand_dims(edge_bounds, axis=2), atol=1e-6) if np.any(edge_bool): #v--- shape = (number of True in edge_bool,) ---v @@ -478,7 +496,8 @@ def shortest_distance_to_surface ( min_dist_arr = np.concatenate((min_dist_arr, edge_dist), axis=1) #Solving for the distances between the points and any relevant faces - face_bool = np.all((shp.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (n_tri_faces, n_points) + face_bool = np.all((shp.face_zones["constraint"] @ points_trans) <= (np.expand_dims(face_bounds, axis=2)+atol), axis=1) #<--- shape = (n_tri_faces, n_points) + # face_bool = face_bool + np.allclose((shp.face_zones["constraint"] @ points_trans), np.expand_dims(face_bounds, axis=2), atol=1e-6) if np.any(face_bool): #v--- shape = (number of True in face_bool,) ---v @@ -549,26 +568,36 @@ def shortest_displacement_to_surface ( coord_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - min_disp_arr = np.ones((n_points,1, 3))*max_value #Initial min_disp_arr + # min_disp_arr = np.ones((n_points,1, 3))*max_value #Initial min_disp_arr + + + #Calculating the displacements + vert_disp=(-1*np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1)) + np.expand_dims(shp.vertices + translation_vector, axis=0) #Displacements between two points + # vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + + #Taking the minimum of the displacements for each point + vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + min_disp_arr = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + #Solving for the displacements between the points and any relevant vertices - vert_bool = np.all((shp.vertex_zones["constraint"] @ coord_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) - if np.any(vert_bool): + # vert_bool = np.all((shp.vertex_zones["constraint"] @ coord_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) + # if np.any(vert_bool): - #v--- shape = (number of True in vert_bool,) ---v - vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - vcoords_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + # #v--- shape = (number of True in vert_bool,) ---v + # vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + # vcoords_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - #Calculating the displacements - vert_disp = np.ones((n_verts,n_points,3))*max_value - vert_disp[vert_bool]=(shp.vertices[vert_used] + translation_vector) - points[vcoords_used] #Displacements between two points - vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + # #Calculating the displacements + # vert_disp = np.ones((n_verts,n_points,3))*max_value + # vert_disp[vert_bool]=(shp.vertices[vert_used] + translation_vector) - points[vcoords_used] #Displacements between two points + # vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) - #Taking the minimum of the displacements for each point - vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) - vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + # #Taking the minimum of the displacements for each point + # vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + # vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) - min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) + # min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) #Solving for the displacements between the points and any relevant edges edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (n_edges, n_points) diff --git a/coxeter/shapes/convex_polyhedron.py b/coxeter/shapes/convex_polyhedron.py index 12afc3b7..8b5b57fd 100644 --- a/coxeter/shapes/convex_polyhedron.py +++ b/coxeter/shapes/convex_polyhedron.py @@ -118,6 +118,14 @@ def __init__(self, vertices): self._sort_simplices() self.sort_faces() + # For shortest distance functions + self._edge_face_neighbors = None + self._vertex_zones = None + self._edge_zones = None + self._face_zones = None + self._vertex_normals = None + self._edge_normals = None + def _consume_hull(self, hull): """Extract data from ConvexHull. diff --git a/coxeter/shapes/convex_spheropolygon.py b/coxeter/shapes/convex_spheropolygon.py index 1e61d01a..29830bee 100644 --- a/coxeter/shapes/convex_spheropolygon.py +++ b/coxeter/shapes/convex_spheropolygon.py @@ -318,6 +318,36 @@ def to_hoomd(self): return hoomd_dict + + + + #TODO: Make internal?? + + @property + def edges(self): + """:class:`numpy.ndarray`: Get the polygon's edges. + + Results returned as vertex index pairs `in counterclockwise order`. In contrast + to the same method for polyhedra, results are not sorted `i Date: Fri, 5 Dec 2025 15:00:55 -0500 Subject: [PATCH 5/8] all min distance tests are done, still some cleaning up comments to do --- coxeter/shapes/_distance2d.py | 9 +- coxeter/shapes/_distance3d.py | 197 ++++++++++++++++------ coxeter/shapes/convex_spheropolygon.py | 4 +- coxeter/shapes/convex_spheropolyhedron.py | 30 +++- tests/test_polyhedron.py | 84 +++++---- tests/test_spheropolyhedron.py | 94 +++++++++++ 6 files changed, 306 insertions(+), 112 deletions(-) diff --git a/coxeter/shapes/_distance2d.py b/coxeter/shapes/_distance2d.py index 5be31722..c9d9625c 100644 --- a/coxeter/shapes/_distance2d.py +++ b/coxeter/shapes/_distance2d.py @@ -397,6 +397,7 @@ def shortest_displacement_to_surface ( #think it is right/will work correctly? def spheropolygon_shortest_displacement_to_surface ( shape, + radius, points: np.ndarray, translation_vector: np.ndarray, ) -> np.ndarray: @@ -467,8 +468,8 @@ def spheropolygon_shortest_displacement_to_surface ( #for spheropolygon vert_projection = np.squeeze(vert_disp - np.expand_dims(vert_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)), axis=1) - v_projection_bool = np.linalg.norm(vert_projection, axis=1) > shape.radius - vert_projection[v_projection_bool] = shape.radius * vert_projection[v_projection_bool]/np.expand_dims(np.linalg.norm(vert_projection[v_projection_bool], axis=1), axis=1) + v_projection_bool = np.linalg.norm(vert_projection, axis=1) > radius + vert_projection[v_projection_bool] = radius * vert_projection[v_projection_bool]/np.expand_dims(np.linalg.norm(vert_projection[v_projection_bool], axis=1), axis=1) vert_disp = vert_disp - vert_projection min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) @@ -494,8 +495,8 @@ def spheropolygon_shortest_displacement_to_surface ( #for spheropolygon edge_projection = np.squeeze(edge_disp - np.expand_dims(edge_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)), axis=1) - e_projection_bool = np.linalg.norm(edge_projection, axis=1) > shape.radius - edge_projection[e_projection_bool] = shape.radius * edge_projection[e_projection_bool]/np.expand_dims(np.linalg.norm(edge_projection[e_projection_bool], axis=1), axis=1) + e_projection_bool = np.linalg.norm(edge_projection, axis=1) > radius + edge_projection[e_projection_bool] = radius * edge_projection[e_projection_bool]/np.expand_dims(np.linalg.norm(edge_projection[e_projection_bool], axis=1), axis=1) edge_disp = edge_disp - edge_projection min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py index b871edb0..373379f9 100644 --- a/coxeter/shapes/_distance3d.py +++ b/coxeter/shapes/_distance3d.py @@ -417,10 +417,10 @@ def shortest_distance_to_surface ( if points.shape == (3,): points = points.reshape(1, 3) - + atol = 1e-8 n_points = len(points) #number of inputted points - n_verts = shp.num_vertices #number of vertices = number of vertex zones - n_edges = shp.num_edges #number of edges = number of edge zones + n_verts = len(shp.vertices) #number of vertices = number of vertex zones + n_edges = len(shp.edges) #number of edges = number of edge zones n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones #arrays consisting of 1 or -1, and used to determine if a point is inside the polyhedron @@ -435,44 +435,16 @@ def shortest_distance_to_surface ( points_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ points_trans' returns the right shape and values max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - # min_dist_arr = np.ones((len(points),1))*max_value #Initial min_dist_arr - - #Calculating the distances - + # Solving for the distances between the points and any relevant vertices vert_dist=LA.norm(np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1) - np.expand_dims(shp.vertices + translation_vector, axis=0), axis=2)*np.expand_dims(vert_inside_mult, axis=0) #Distances between two points - # vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) #Taking the minimum of the distances for each point vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) min_dist_arr = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) - - # print(min_dist_arr) - atol = 1e-8 - - - # Solving for the distances between the points and any relevant vertices - # vert_bool = np.all((shp.vertex_zones["constraint"] @ points_trans) <= (np.expand_dims(vert_bounds, axis=2)+atol), axis=1) #<--- shape = (n_verts, n_points) - # if np.any(vert_bool): - - # #v--- shape = (number of True in vert_bool,) ---v - # vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - # v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - - # #Calculating the distances - # vert_dist = np.ones((n_verts,n_points))*max_value - # vert_dist[vert_bool]=LA.norm(points[v_points_used] - (shp.vertices[vert_used] + translation_vector), axis=1)*vert_inside_mult[vert_used] #Distances between two points - # vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) - - # #Taking the minimum of the distances for each point - # vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) - # vert_dist = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) - - # min_dist_arr = np.concatenate((min_dist_arr, vert_dist), axis=1) - #Solving for the distances between the points and any relevant edges edge_bool = np.all((shp.edge_zones["constraint"] @ points_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) # edge_bool = edge_bool + np.allclose((shp.edge_zones["constraint"] @ points_trans), np.expand_dims(edge_bounds, axis=2), atol=1e-6) @@ -556,9 +528,10 @@ def shortest_displacement_to_surface ( if points.shape == (3,): points = points.reshape(1, 3) + atol = 1e-8 n_points = len(points) #number of inputted points - n_verts = shp.num_vertices #number of vertices = number of vertex zones - n_edges = shp.num_edges #number of edges = number of edge zones + n_verts = len(shp.vertices) #number of vertices = number of vertex zones + n_edges = len(shp.edges) #number of edges = number of edge zones n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones #Updating bounds with the position of the polyhedron @@ -568,39 +541,141 @@ def shortest_displacement_to_surface ( coord_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - # min_disp_arr = np.ones((n_points,1, 3))*max_value #Initial min_disp_arr - #Calculating the displacements - vert_disp=(-1*np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1)) + np.expand_dims(shp.vertices + translation_vector, axis=0) #Displacements between two points - # vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + + #Solving for the displacements between the points and any relevant vertices + vert_disp=(-1*np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1)) + np.expand_dims(shp.vertices + translation_vector, axis=0) #Displacements between two point #Taking the minimum of the displacements for each point vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) min_disp_arr = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + #Solving for the displacements between the points and any relevant edges + edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) + if np.any(edge_bool): + + #v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool + ecoords_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool + + vert_on_edge = shp.vertices[shp.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges + + #Calculating the displacements + edge_disp = np.ones((n_edges,n_points,3))*max_value + edge_disp[edge_bool]=point_to_edge_displacement(points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used]) #Displacements between a point and a line + edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) + + #Taking the minimum of the displacements for each point + edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) + + #Solving for the displacements between the points and any relevant faces + face_bool = np.all((shp.face_zones["constraint"] @ coord_trans) <= (np.expand_dims(face_bounds, axis=2)+atol), axis=1) #<--- shape = (n_tri_faces, n_points) + if np.any(face_bool): + + #v--- shape = (number of True in face_bool,) ---v + face_used = np.transpose(np.tile(np.arange(0,n_tri_faces,1), (n_points,1)))[face_bool] #Contains the indices of the triangulated faces that hold True for face_bool + fcoords_used = np.tile(np.arange(0,n_points,1), (n_tri_faces,1))[face_bool] #Contains the indices of the points that hold True for face_bool + + vert_on_face = (shp.face_zones["face_points"][face_used]) + translation_vector #Vertices that lie on the needed faces + + #Calculating the displacements + face_disp = np.ones((n_tri_faces,n_points,3))*max_value + face_disp[face_bool]=point_to_face_displacement(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) #Displacements between a point and a plane + face_disp = np.transpose(face_disp, (1, 0, 2)) #<--- shape = (n_points, n_tri_faces, 3) + + #Taking the minimum of the displacements for each point + face_disp_arg = np.expand_dims(np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1,2)) + face_disp = np.take_along_axis(face_disp, face_disp_arg, axis=1) + + min_disp_arr = np.concatenate((min_disp_arr, face_disp), axis=1) + + disp_arr_bool = np.expand_dims(np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1), axis=(1,2)) #determining the displacements that are shortest + true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_arr_bool, axis=1), axis=1) + + return true_min_disp + + +def spheropolyhedron_shortest_displacement_to_surface ( + shp, + radius, + points: np.ndarray, + translation_vector: np.ndarray +) -> np.ndarray: + ''' + Solves for the shortest displacement between points and the surface of a polyhedron. + + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + point lies in, determines the displacement calculation(s) done. For a vertex zone, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, + and points can be in more than one zone. By taking the minimum of all the distances of + the calculated displacements, the shortest displacements are found. + + Args: + points (list or np.ndarray): positions of the points [shape = (n_points, 3)] + translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,)] + + Returns: + np.ndarray: shortest displacements [shape = (n_points, 3)] + ''' + points = np.asarray(points) + translation_vector = np.asarray(translation_vector) + + if translation_vector.shape[0]!=3 or len(translation_vector.shape)>1: + raise ValueError(f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}") + + if points.shape == (3,): + points = points.reshape(1, 3) + + atol = 1e-8 + n_points = len(points) #number of inputted points + n_verts = len(shp.vertices) #number of vertices = number of vertex zones + n_edges = len(shp.edges) #number of edges = number of edge zones + n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones + + #Updating bounds with the position of the polyhedron + vert_bounds = shp.vertex_zones["bounds"] + (shp.vertex_zones["constraint"] @ translation_vector) + edge_bounds = shp.edge_zones["bounds"] + (shp.edge_zones["constraint"] @ translation_vector) + face_bounds = shp.face_zones["bounds"] + (shp.face_zones["constraint"] @ translation_vector) + + coord_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values + max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances + min_disp_arr = np.ones((n_points,1, 3))*max_value #Initial min_disp_arr + + #Calculating the displacements #Solving for the displacements between the points and any relevant vertices - # vert_bool = np.all((shp.vertex_zones["constraint"] @ coord_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) - # if np.any(vert_bool): + vert_bool = np.all((shp.vertex_zones["constraint"] @ coord_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) + if np.any(vert_bool): - # #v--- shape = (number of True in vert_bool,) ---v - # vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - # vcoords_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool + #v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool + vcoords_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - # #Calculating the displacements - # vert_disp = np.ones((n_verts,n_points,3))*max_value - # vert_disp[vert_bool]=(shp.vertices[vert_used] + translation_vector) - points[vcoords_used] #Displacements between two points - # vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + #Calculating the displacements + vert_disp = np.ones((n_verts,n_points,3))*max_value + vert_disp[vert_bool]=(shp.vertices[vert_used] + translation_vector) - points[vcoords_used] #Displacements between two points + vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) + + #TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal + vert_zero_disp_bool = np.all(vert_disp == 0, axis=2) + vert_disp[vert_zero_disp_bool] = vert_disp[vert_zero_disp_bool] + radius*(np.repeat(np.expand_dims(shp.vertex_normals,axis=0),n_points,axis=0)[vert_zero_disp_bool]) + vert_disp[np.invert(vert_zero_disp_bool)] = vert_disp[np.invert(vert_zero_disp_bool)] - radius*(vert_disp[np.invert(vert_zero_disp_bool)]/np.expand_dims(np.linalg.norm(vert_disp[np.invert(vert_zero_disp_bool)],axis=1),axis=1)) - # #Taking the minimum of the displacements for each point - # vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) - # vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) + #Taking the minimum of the displacements for each point + vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) - # min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) + min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) #Solving for the displacements between the points and any relevant edges - edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (n_edges, n_points) + edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) if np.any(edge_bool): #v--- shape = (number of True in edge_bool,) ---v @@ -614,6 +689,11 @@ def shortest_displacement_to_surface ( edge_disp[edge_bool]=point_to_edge_displacement(points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used]) #Displacements between a point and a line edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) + #TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal + edge_zero_disp_bool = np.all(edge_disp == 0, axis=2) + edge_disp[edge_zero_disp_bool] = edge_disp[edge_zero_disp_bool] + radius*(np.repeat(np.expand_dims(shp.edge_normals,axis=0),n_points,axis=0)[edge_zero_disp_bool]) + edge_disp[np.invert(edge_zero_disp_bool)] = edge_disp[np.invert(edge_zero_disp_bool)] - radius*(edge_disp[np.invert(edge_zero_disp_bool)]/np.expand_dims(np.linalg.norm(edge_disp[np.invert(edge_zero_disp_bool)],axis=1),axis=1)) + #Taking the minimum of the displacements for each point edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) @@ -621,7 +701,7 @@ def shortest_displacement_to_surface ( min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) #Solving for the displacements between the points and any relevant faces - face_bool = np.all((shp.face_zones["constraint"] @ coord_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (n_tri_faces, n_points) + face_bool = np.all((shp.face_zones["constraint"] @ coord_trans) <= (np.expand_dims(face_bounds, axis=2)+atol), axis=1) #<--- shape = (n_tri_faces, n_points) if np.any(face_bool): #v--- shape = (number of True in face_bool,) ---v @@ -633,8 +713,17 @@ def shortest_displacement_to_surface ( #Calculating the displacements face_disp = np.ones((n_tri_faces,n_points,3))*max_value face_disp[face_bool]=point_to_face_displacement(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) #Displacements between a point and a plane - face_disp = np.transpose(face_disp, (1, 0, 2)) #<--- shape = (n_points, n_tri_faces, 3) + #TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal + #TODO: if point is inside, add radius*unit_displacement instead + point_inside = (-1)*np.ones((n_tri_faces, n_points)) + point_inside[face_bool] = (point_to_face_distance(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) < 0).astype(int)*2 -1 #(+1) outside, (-1) inside + + face_zero_disp_bool = np.all(face_disp == 0, axis=2) + face_disp[face_zero_disp_bool] = face_disp[face_zero_disp_bool] + radius*(np.repeat(np.expand_dims((shp.face_zones["normals"]/np.expand_dims(np.linalg.norm(shp.face_zones["normals"],axis=1),axis=1)),axis=1),n_points,axis=1)[face_zero_disp_bool]) + face_disp[np.invert(face_zero_disp_bool)] = face_disp[np.invert(face_zero_disp_bool)] + radius*np.expand_dims(point_inside[np.invert(face_zero_disp_bool)],axis=1)*(face_disp[np.invert(face_zero_disp_bool)]/np.expand_dims(np.linalg.norm(face_disp[np.invert(face_zero_disp_bool)],axis=1),axis=1)) + + face_disp = np.transpose(face_disp, (1, 0, 2)) #<--- shape = (n_points, n_tri_faces, 3) #Taking the minimum of the displacements for each point face_disp_arg = np.expand_dims(np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1,2)) face_disp = np.take_along_axis(face_disp, face_disp_arg, axis=1) diff --git a/coxeter/shapes/convex_spheropolygon.py b/coxeter/shapes/convex_spheropolygon.py index 29830bee..3228c4f6 100644 --- a/coxeter/shapes/convex_spheropolygon.py +++ b/coxeter/shapes/convex_spheropolygon.py @@ -410,7 +410,7 @@ def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0, the shortest distance of each point to the surface [shape = (n_points,)] """ - return np.linalg.norm(spheropolygon_shortest_displacement_to_surface(self, points, translation_vector), axis=1) + return np.linalg.norm(spheropolygon_shortest_displacement_to_surface(self._polygon, self.radius, points, translation_vector), axis=1) def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): """ @@ -441,4 +441,4 @@ def shortest_displacement_to_surface(self, points, translation_vector=np.array([ the shortest displacement of each point to the surface [shape = (n_points, 3)] """ - return spheropolygon_shortest_displacement_to_surface(self, points, translation_vector) \ No newline at end of file + return spheropolygon_shortest_displacement_to_surface(self._polygon, self.radius, points, translation_vector) \ No newline at end of file diff --git a/coxeter/shapes/convex_spheropolyhedron.py b/coxeter/shapes/convex_spheropolyhedron.py index 5bd70912..a625ba0d 100644 --- a/coxeter/shapes/convex_spheropolyhedron.py +++ b/coxeter/shapes/convex_spheropolyhedron.py @@ -23,7 +23,8 @@ get_weighted_vert_normals, get_weighted_edge_normals, shortest_displacement_to_surface, - shortest_distance_to_surface + shortest_distance_to_surface, + spheropolyhedron_shortest_displacement_to_surface ) @@ -81,6 +82,7 @@ class ConvexSpheropolyhedron(Shape3D): def __init__(self, vertices, radius): self._polyhedron = ConvexPolyhedron(vertices) self.radius = radius + self._edge_face_neighbors = None self._vertex_zones = None self._edge_zones = None @@ -388,7 +390,11 @@ def to_hoomd(self): return hoomd_dict - @property + + + + + '''@property def edge_face_neighbors(self): """:class:`numpy.ndarray`: Get the indices of the faces that are adjacent to each edge. @@ -473,7 +479,7 @@ def weighted_edge_normals(self): The normals point outwards from the polyhedron. """ - return get_weighted_edge_normals(self) + return get_weighted_edge_normals(self)''' def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): """ @@ -505,7 +511,7 @@ def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0, the shortest distance of each point to the surface [shape = (n_points,)] """ - return shortest_distance_to_surface(self, points, translation_vector) - self.radius + return shortest_distance_to_surface(self._polyhedron, points, translation_vector) - self.radius def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): """ @@ -536,7 +542,17 @@ def shortest_displacement_to_surface(self, points, translation_vector=np.array([ the shortest displacement of each point to the surface [shape = (n_points, 3)] """ - displacement = shortest_displacement_to_surface(self, points, translation_vector) - unit_displacement = displacement / np.linalg.norm(displacement, axis=1) - return displacement - self.radius*unit_displacement + # displacement = shortest_displacement_to_surface(self._polyhedron, points, translation_vector) + + # #TODO: if statement for displacement==[0,0,0] + # unit_displacement = displacement / np.expand_dims(np.linalg.norm(displacement, axis=1),axis=1) + + # is_outside = np.expand_dims((shortest_distance_to_surface(self._polyhedron, points, translation_vector) < 0).astype(int) *2 -1, axis=1) + # #(-1) if outside, (+1) if inside + + # print('initial',displacement) + # print('subtracted (-)', -1*self.radius*unit_displacement) + + return spheropolyhedron_shortest_displacement_to_surface(self._polyhedron, self.radius, points, translation_vector) + #displacement + is_outside*self.radius*unit_displacement diff --git a/tests/test_polyhedron.py b/tests/test_polyhedron.py index 92dcd789..04429f58 100644 --- a/tests/test_polyhedron.py +++ b/tests/test_polyhedron.py @@ -958,8 +958,8 @@ def test_to_hoomd(poly): #TODO: # - test on convex cube [DONE] -# - test on concave shapes (pyramid-pointed, prism-pointed, indented) -# - test on general convex shape, compare with scipy.minimize distance +# - test on concave shapes (pyramid-pointed, prism-pointed, indented) [DONE] +# - test on general convex shape, compare with scipy.minimize distance [DONE] # - (maybe? probably not) test on general concave shape made from tetrahedrons, compare with scipy.minimize distance def test_shortest_distance_convex(): @@ -971,6 +971,12 @@ def test_shortest_distance_convex(): distances = cube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) displacements = cube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) + + cube_surface_distance = cube.shortest_distance_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) + cube_surface_displacement = cube.shortest_displacement_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(cube_surface_distance, np.zeros((len(x_points)))) + np.testing.assert_allclose(cube_surface_displacement, np.zeros((len(x_points),3))) true_distances = np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2]) true_displacements = np.array([[0,0,-1], [-1,-1,1], [-1,0,0], [0,-1,-1], [0,0,-1], [0,0,-2]]) @@ -990,10 +996,12 @@ def test_shortest_distance_concave(): pyramid_distances = pyramidcube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) pyramid_displacements = pyramidcube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(np.abs(pyramid_distances), np.linalg.norm(pyramid_displacements, axis=1)) - pyramid_surface = pyramidcube.shortest_distance_to_surface(x_points + pyramid_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(pyramid_surface, np.zeros((len(x_points)))) + + pyramid_surface_distance = pyramidcube.shortest_distance_to_surface(x_points + pyramid_displacements, translation_vector=np.array([3,3,3])) + pyramid_surface_displacement = pyramidcube.shortest_displacement_to_surface(x_points + pyramid_displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(pyramid_surface_distance, np.zeros((len(x_points)))) + np.testing.assert_allclose(pyramid_surface_displacement, np.zeros((len(x_points),3))) pyramid_true_distances = np.array([np.sqrt(3), 1, 3/np.sqrt(5), 1/np.sqrt(5), 0, -0.5, 2/np.sqrt(5), -1, -1*np.sqrt(0.5), -0.25]) @@ -1006,10 +1014,12 @@ def test_shortest_distance_concave(): prism_distances = prismcube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) prism_displacements = prismcube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(np.abs(prism_distances), np.linalg.norm(prism_displacements, axis=1)) - prism_surface = prismcube.shortest_distance_to_surface(x_points + prism_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(prism_surface, np.zeros((len(x_points)))) + + prism_surface_distance = prismcube.shortest_distance_to_surface(x_points + prism_displacements, translation_vector=np.array([3,3,3])) + prism_surface_displacement = prismcube.shortest_displacement_to_surface(x_points + prism_displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(prism_surface_distance, np.zeros((len(x_points)))) + np.testing.assert_allclose(prism_surface_displacement, np.zeros((len(x_points),3))) prism_true_distances = np.array([np.sqrt(70)/5, 1, 3/np.sqrt(5), 1/np.sqrt(5), 0, -0.5, 0.5, -1, -1*np.sqrt(0.5), -0.25]) @@ -1022,23 +1032,18 @@ def test_shortest_distance_concave(): indented_distances = indented_cube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) indented_displacements = indented_cube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(np.abs(indented_distances), np.linalg.norm(indented_displacements, axis=1)) - indented_surface = indented_cube.shortest_distance_to_surface(x_points + indented_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(indented_surface, np.zeros((len(x_points))), atol=2e-10) + + indented_surface_distance = indented_cube.shortest_distance_to_surface(x_points + indented_displacements, translation_vector=np.array([3,3,3])) + indented_surface_displacement = indented_cube.shortest_displacement_to_surface(x_points + indented_displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(indented_surface_distance, np.zeros((len(x_points))), atol=2e-10) + np.testing.assert_allclose(indented_surface_displacement, np.zeros((len(x_points),3)), atol=2e-10) indented_true_distances = np.array([np.sqrt(3), 1, np.sqrt(2), 1, np.sqrt(5), -0.1714985851, np.sqrt(1.25), 1/np.sqrt(13), 0.5/np.sqrt(13), -0.25]) np.testing.assert_allclose(indented_distances, indented_true_distances) - -#Do displacements by checking that (x_points + displacements) gives 0 distance to the surface -#scipy for inside points -#scipy for outside points -#add scipy_outside and -1*scipy_inside together to get distances? - -#TODO: Tests not working... something is wrong def test_shortest_distance_convex_general(): ''' 3 does NOT work @@ -1059,10 +1064,8 @@ def test_shortest_distance_convex_general(): RESOLVED: a tolerance needed to be added for the zone bools ''' # np.random.seed(6) - random_theta = np.random.rand(20)*np.pi #theta - random_phi = np.random.rand(20)*2*np.pi #phi - # sorted_angles = np.sort(random_angles) - # random_dist = np.random.rand(1)*10 #from origin + random_theta = np.random.rand(20)*np.pi + random_phi = np.random.rand(20)*2*np.pi radius = np.random.rand(1)*5 vertices = np.zeros((20,3)) @@ -1072,22 +1075,18 @@ def test_shortest_distance_convex_general(): poly = ConvexPolyhedron(vertices=vertices) - points = np.random.rand(150, 3)*20 -10 + points = np.random.rand(1500, 3)*20 -10 distances = poly.shortest_distance_to_surface(points) displacements = poly.shortest_displacement_to_surface(points) #displacements are correct, issue with the distance calculation of points on the surface - poly_surface = poly.shortest_distance_to_surface(points+displacements) - - - # import matplotlib.pyplot as plt - # fig = plt.figure() - # ax = fig.add_subplot(projection='3d') - # poly.plot(ax=ax, plot_verts=True)#, label_verts=True) - # ax.scatter3D(xs=(points[7]+displacements[7])[0],ys=(points[7]+displacements[7])[1],zs=(points[7]+displacements[7])[2], color='k') - # # plt.show() - + np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - np.testing.assert_allclose(poly_surface, np.zeros((len(points))), atol=2e-8) + + poly_surface_distance = poly.shortest_distance_to_surface(points+displacements) + poly_surface_displacement = poly.shortest_displacement_to_surface(points+displacements) + + np.testing.assert_allclose(poly_surface_distance, np.zeros((len(points))), atol=2e-8) + np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(points), 3)), atol=2e-8) def scipy_closest_point(point, surface_constraint, surface_bounds): @@ -1099,28 +1098,23 @@ def scipy_closest_point(point, surface_constraint, surface_bounds): constraints=[LinearConstraint(surface_constraint, -np.inf, surface_bounds)], tol=1e-12 ) - # tmps.append(tri_min_point.x) + distance = np.linalg.norm(tri_min_point.x - point) displacement = tri_min_point.x - point - # ax.scatter(*tmps[np.argmin(all_tri_distances)][:2], c = "r", marker="x") return distance, displacement - outside_surface_constraint = poly.normals - outside_surface_bounds = np.sum(outside_surface_constraint * poly.face_centroids, axis=1) - - inside_surface_constraint = -1*poly.normals - inside_surface_bounds = np.sum(inside_surface_constraint * poly.face_centroids, axis=1) + poly_constraint = poly.normals + poly_bounds = np.sum(poly_constraint * poly.face_centroids, axis=1) scipy_distances = [] scipy_displacements = [] for point in points: - outside_distance, outside_displacement = scipy_closest_point(point, outside_surface_constraint, outside_surface_bounds) - # inside_distance, inside_displacement = scipy_closest_point(point, inside_surface_constraint, inside_surface_bounds) + outside_distance, outside_displacement = scipy_closest_point(point, poly_constraint, poly_bounds) - scipy_distances.append( outside_distance)#-1* inside_distance) #outside_distance)# - scipy_displacements.append(outside_displacement )#+ inside_displacement) + scipy_distances.append( outside_distance) + scipy_displacements.append(outside_displacement ) scipy_distances = np.asarray(scipy_distances) scipy_displacements = np.asarray(scipy_displacements) diff --git a/tests/test_spheropolyhedron.py b/tests/test_spheropolyhedron.py index b51b1916..911f8788 100644 --- a/tests/test_spheropolyhedron.py +++ b/tests/test_spheropolyhedron.py @@ -158,3 +158,97 @@ def test_to_hoomd(poly, r): hoomd_dict = poly.to_hoomd() for key, val in zip(dict_keys, dict_vals): assert np.allclose(hoomd_dict[key], val), f"{key}" + + +def test_shortest_distance_convex(): + + radius = 0.5#np.random.rand(1) + verts = np.array([[1,1,1], [-1,1,1], [1,-1,1], [1,1,-1], [-1,-1,1], [-1,1,-1],[1,-1,-1],[-1,-1,-1]]) + poly = ConvexSpheropolyhedron(vertices=verts, radius = radius) + + x_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[4,4,4],[4,4,3],[4,3,3]]) + + distances = poly.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) + displacements = poly.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + + print(distances) + print(displacements) + + np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) + + poly_surface_distance = poly.shortest_distance_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) + poly_surface_displacement = poly.shortest_displacement_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(poly_surface_distance, np.zeros((len(x_points))), atol=1e-10) + np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(x_points),3)), atol=1e-10) + + true_distances = np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2, 0, 0, 0]) - radius + true_displacements = np.array([[0,0,-1], [-1,-1,1], [-1,0,0], [0,-1,-1], [0,0,-1], [0,0,-2]]) + true_displacements = true_displacements - radius*(true_displacements/np.expand_dims(np.linalg.norm(true_displacements, axis=1),axis=1)) + + np.testing.assert_allclose(distances, true_distances) + np.testing.assert_allclose(displacements[1:7], true_displacements) + + +def test_shortest_distance_convex_general(): + + # np.random.seed(6) + random_theta = np.random.rand(20)*np.pi + random_phi = np.random.rand(20)*2*np.pi + radius = np.random.rand(1)*5 #radius for the convex polyhedron + sph_poly_radius = np.random.rand(1)*2 #radius for the spheropolyhedron + + vertices = np.zeros((20,3)) + vertices[:,0] = radius * np.sin(random_theta) * np.cos(random_phi) #x + vertices[:,1] = radius * np.sin(random_theta) * np.sin(random_phi) #y + vertices[:,2] = radius * np.cos(random_theta) + + poly = ConvexSpheropolyhedron(vertices=vertices, radius=sph_poly_radius) + + points = np.random.rand(1500, 3)*20 -10 + + distances = poly.shortest_distance_to_surface(points) + displacements = poly.shortest_displacement_to_surface(points) #displacements are correct, issue with the distance calculation of points on the surface + + np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) + + poly_surface_distance = poly.shortest_distance_to_surface(points+displacements) + poly_surface_displacement = poly.shortest_displacement_to_surface(points+displacements) + + np.testing.assert_allclose(poly_surface_distance, np.zeros((len(points))), atol=2e-8) + np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(points), 3)), atol=2e-8) + + + def scipy_closest_point(point, surface_constraint, surface_bounds): + from scipy.optimize import LinearConstraint, minimize + + tri_min_point = minimize( + fun=lambda pt: np.linalg.norm(pt - point), # Function to optimize + x0=np.zeros(3), # Initial guess + constraints=[LinearConstraint(surface_constraint, -np.inf, surface_bounds)], + tol=1e-12 + ) + + distance = np.linalg.norm(tri_min_point.x - point) + + return distance + + + poly_constraint = poly._polyhedron.normals + poly_bounds = np.sum(poly_constraint * poly._polyhedron.face_centroids, axis=1) + + scipy_distances = [] + for point in points: + outside_distance = scipy_closest_point(point, poly_constraint, poly_bounds) + + scipy_distances.append( outside_distance) + + scipy_distances = np.asarray(scipy_distances) - sph_poly_radius + scipy_zero = scipy_distances < 0 + scipy_distances[scipy_zero] = 0 + + is_zero_bool = distances <= 0 + + zero_inside_distances = distances + zero_inside_distances[is_zero_bool] = 0 + + np.testing.assert_allclose(zero_inside_distances, scipy_distances, atol=2e-8) From 6582750bbebca38925d7a50377100a4256ae2d1e Mon Sep 17 00:00:00 2001 From: Ryn Oliphant Date: Fri, 5 Dec 2025 15:27:58 -0500 Subject: [PATCH 6/8] Cleaned up comments (I think) --- coxeter/shapes/_distance2d.py | 15 +--- coxeter/shapes/_distance3d.py | 15 +--- coxeter/shapes/convex_spheropolygon.py | 63 ------------- coxeter/shapes/convex_spheropolyhedron.py | 103 +--------------------- tests/test_polygon.py | 46 +--------- tests/test_polyhedron.py | 29 +----- tests/test_spheropolygon.py | 54 +----------- tests/test_spheropolyhedron.py | 5 +- 8 files changed, 17 insertions(+), 313 deletions(-) diff --git a/coxeter/shapes/_distance2d.py b/coxeter/shapes/_distance2d.py index c9d9625c..a86103fd 100644 --- a/coxeter/shapes/_distance2d.py +++ b/coxeter/shapes/_distance2d.py @@ -1,8 +1,8 @@ import numpy as np import numpy.linalg as LA -# --- "Hidden" Functions --- -#good? +#TODO: update docstrings? + def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: ''' Calculates the distances between several points and several varying lines. @@ -25,7 +25,6 @@ def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np dist = LA.norm(((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)), axis=1) #distances return dist -#good? def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: ''' Calculates the displacements between several points and several varying lines. @@ -48,7 +47,6 @@ def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector disp = ((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)) #displacements return disp -#good? def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: ''' Calculates the distances between a single point and the plane of the polygon. @@ -68,7 +66,6 @@ def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np. return dist -#good? def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: ''' Calculates the displacements between a single point and the plane of the polygon. @@ -87,7 +84,6 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: return disp -#good input def get_vert_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones @@ -109,7 +105,6 @@ def get_vert_zones (shape): return {"constraint": vert_constraint, "bounds":vert_bounds} -#good input def get_edge_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones @@ -137,7 +132,6 @@ def get_edge_zones (shape): return {"constraint":edge_constraint, "bounds":edge_bounds} -#good input def get_face_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones @@ -175,8 +169,6 @@ def get_face_zones (shape): return {"constraint":face_constraint, "bounds":face_bounds} -# --- User Available Functions --- -#good input def shortest_distance_to_surface ( shape, points: np.ndarray, @@ -286,7 +278,6 @@ def shortest_distance_to_surface ( return true_min_dist -#good input def shortest_displacement_to_surface ( shape, points: np.ndarray, @@ -393,8 +384,6 @@ def shortest_displacement_to_surface ( return true_min_disp - -#think it is right/will work correctly? def spheropolygon_shortest_displacement_to_surface ( shape, radius, diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py index 373379f9..b5dbc461 100644 --- a/coxeter/shapes/_distance3d.py +++ b/coxeter/shapes/_distance3d.py @@ -1,9 +1,8 @@ import numpy as np import numpy.linalg as LA -#TODO: update docstrings +#TODO: update docstrings? -#good? def get_edge_face_neighbors (shape) -> np.ndarray: ''' Gets the indices of the faces that are adjacent to each edge. @@ -60,7 +59,6 @@ def get_edge_face_neighbors (shape) -> np.ndarray: return ef_neighbor -#good? def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: ''' Calculates the distances between several points and several varying lines. @@ -83,7 +81,6 @@ def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np dist = LA.norm(((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)), axis=1) #distances return dist -#good? def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: ''' Calculates the displacements between several points and several varying lines. @@ -106,7 +103,6 @@ def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector disp = ((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)) #displacements return disp -#good? def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: ''' Calculates the distances between several points and several varying planes. @@ -131,7 +127,6 @@ def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np. return dist -#good? def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: ''' Calculates the displacements between several points and several varying planes. @@ -156,7 +151,6 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: return disp -#good? def get_vert_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones @@ -200,7 +194,6 @@ def get_vert_zones (shape): return {"constraint":vert_constraint, "bounds":vert_bounds} -#good? def get_edge_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones @@ -236,7 +229,6 @@ def get_edge_zones (shape): return {"constraint":edge_constraint, "bounds":edge_bounds} -#good? def get_face_zones (shape): ''' Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones @@ -278,7 +270,6 @@ def get_face_zones (shape): return {"constraint":face_constraint, "bounds":face_bounds, "face_points":face_one_vertex, "normals": tri_face_normals} -#good? def get_edge_normals(shape) -> np.ndarray: ''' Gets the analogous normals of the edges of the polyhedron. The normals point outwards from the polyhedron @@ -299,7 +290,6 @@ def get_edge_normals(shape) -> np.ndarray: #returning the unit vectors of the edge normals return edge_normals / np.expand_dims(LA.norm(edge_normals, axis=1), axis=1) -#good? def get_vert_normals(shape) -> np.ndarray: ''' Gets the analogous normals of the vertices of the polyhedron. The normals point outwards from the polyhedron @@ -382,7 +372,6 @@ def get_weighted_vert_normals(shape) -> np.ndarray: -#good input def shortest_distance_to_surface ( shp, points: np.ndarray, @@ -494,7 +483,6 @@ def shortest_distance_to_surface ( return true_min_dist -#good input def shortest_displacement_to_surface ( shp, points: np.ndarray, @@ -598,7 +586,6 @@ def shortest_displacement_to_surface ( return true_min_disp - def spheropolyhedron_shortest_displacement_to_surface ( shp, radius, diff --git a/coxeter/shapes/convex_spheropolygon.py b/coxeter/shapes/convex_spheropolygon.py index 3228c4f6..aacd9838 100644 --- a/coxeter/shapes/convex_spheropolygon.py +++ b/coxeter/shapes/convex_spheropolygon.py @@ -318,69 +318,6 @@ def to_hoomd(self): return hoomd_dict - - - - #TODO: Make internal?? - - @property - def edges(self): - """:class:`numpy.ndarray`: Get the polygon's edges. - - Results returned as vertex index pairs `in counterclockwise order`. In contrast - to the same method for polyhedra, results are not sorted `i Date: Thu, 18 Dec 2025 15:47:31 -0500 Subject: [PATCH 7/8] x_points -> test_points --- tests/test_polygon.py | 12 ++++---- tests/test_polyhedron.py | 52 +++++++++++++++++----------------- tests/test_spheropolygon.py | 6 ++-- tests/test_spheropolyhedron.py | 14 ++++----- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 3311d012..603ef6a1 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -648,10 +648,10 @@ def test_shortest_distance_convex(): tri_verts = np.array([[0, 0.5], [-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25]]) triangle = ConvexPolygon(vertices=tri_verts) - x_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1]]) + test_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1]]) - distances = triangle.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,0])) - displacements = triangle.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,0])) + distances = triangle.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,0])) + displacements = triangle.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,0])) true_distances = np.array([0.3080127018, 0.25, 0, 1, 1.0231690965]) true_displacements = np.array([[-0.2667468246, -0.1540063509, 0], [0,-0.25,0], [0,0,0],[0,0,-1],[-0.1875, -0.1082531755, 1]]) @@ -663,10 +663,10 @@ def test_shortest_distance_concave(): verts = np.array([[0,0.5],[-0.125,0.75],[-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25],[0.25*np.sqrt(3),0.75]]) concave_poly = Polygon(vertices=verts) - x_points = np.array([[3.5, 3.25,0],[3,3.75,0],[3,3.25,0],[3,3,-1],[3.25,3.5,-1],[3+0.25*np.sqrt(3),4,0]]) + test_points = np.array([[3.5, 3.25,0],[3,3.75,0],[3,3.25,0],[3,3,-1],[3.25,3.5,-1],[3+0.25*np.sqrt(3),4,0]]) - distances = concave_poly.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,0])) - displacements = concave_poly.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,0])) + distances = concave_poly.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,0])) + displacements = concave_poly.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,0])) true_distances = np.array([abs(0.25*np.sqrt(3)-0.5), np.sqrt(0.0125), 0, 1, 1, 0.25]) true_displacements = np.array([[0.25*np.sqrt(3)-0.5,0,0],[-0.1,-0.05,0],[0,0,0],[0,0,1],[0,0,1],[0,-0.25,0]]) diff --git a/tests/test_polyhedron.py b/tests/test_polyhedron.py index 2be8d127..c8d1383a 100644 --- a/tests/test_polyhedron.py +++ b/tests/test_polyhedron.py @@ -961,16 +961,16 @@ def test_shortest_distance_convex(): cube_verts = np.array([[1,1,1], [-1,1,1], [1,-1,1], [1,1,-1], [-1,-1,1], [-1,1,-1],[1,-1,-1],[-1,-1,-1]]) cube = ConvexPolyhedron(vertices=cube_verts)#, faces=[[1,5,7,4],[0,2,6,3],[2,4,7,6],[3,6,7,5],[0,1,4,2],[1,0,3,5]]) - x_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6]]) + test_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6]]) - distances = cube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) - displacements = cube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + distances = cube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) + displacements = cube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - cube_surface_distance = cube.shortest_distance_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) - cube_surface_displacement = cube.shortest_displacement_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(cube_surface_distance, np.zeros((len(x_points)))) - np.testing.assert_allclose(cube_surface_displacement, np.zeros((len(x_points),3))) + cube_surface_distance = cube.shortest_distance_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) + cube_surface_displacement = cube.shortest_displacement_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(cube_surface_distance, np.zeros((len(test_points)))) + np.testing.assert_allclose(cube_surface_displacement, np.zeros((len(test_points),3))) true_distances = np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2]) true_displacements = np.array([[0,0,-1], [-1,-1,1], [-1,0,0], [0,-1,-1], [0,0,-1], [0,0,-2]]) @@ -981,21 +981,21 @@ def test_shortest_distance_convex(): def test_shortest_distance_concave(): - x_points = np.array([[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[3.5,2.5,3],[4.5,3,5],[3,3,3],[3,3.5,3.5],[3,3,2.25]]) + test_points = np.array([[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[3.5,2.5,3],[4.5,3,5],[3,3,3],[3,3.5,3.5],[3,3,2.25]]) #--- PYRAMID Point on Cube Case --- pyramidcube_verts = np.array([[1,1,1],[-1,1,1],[1,-1,1],[1,1,-1],[-1,-1,1],[-1,1,-1],[1,-1,-1],[-1,-1,-1],[0,0,3],[0,3,0]]) pyramid_faces = [[0,1,8],[1,4,8],[4,2,8],[2,0,8],[1,0,9],[5,1,9],[3,5,9],[0,3,9],[1,5,7,4],[0,2,6,3],[2,4,7,6],[3,6,7,5]] pyramidcube = Polyhedron(vertices=pyramidcube_verts, faces=pyramid_faces) - pyramid_distances = pyramidcube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) - pyramid_displacements = pyramidcube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + pyramid_distances = pyramidcube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) + pyramid_displacements = pyramidcube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) np.testing.assert_allclose(np.abs(pyramid_distances), np.linalg.norm(pyramid_displacements, axis=1)) - pyramid_surface_distance = pyramidcube.shortest_distance_to_surface(x_points + pyramid_displacements, translation_vector=np.array([3,3,3])) - pyramid_surface_displacement = pyramidcube.shortest_displacement_to_surface(x_points + pyramid_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(pyramid_surface_distance, np.zeros((len(x_points)))) - np.testing.assert_allclose(pyramid_surface_displacement, np.zeros((len(x_points),3))) + pyramid_surface_distance = pyramidcube.shortest_distance_to_surface(test_points + pyramid_displacements, translation_vector=np.array([3,3,3])) + pyramid_surface_displacement = pyramidcube.shortest_displacement_to_surface(test_points + pyramid_displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(pyramid_surface_distance, np.zeros((len(test_points)))) + np.testing.assert_allclose(pyramid_surface_displacement, np.zeros((len(test_points),3))) pyramid_true_distances = np.array([np.sqrt(3), 1, 3/np.sqrt(5), 1/np.sqrt(5), 0, -0.5, 2/np.sqrt(5), -1, -1*np.sqrt(0.5), -0.25]) @@ -1006,14 +1006,14 @@ def test_shortest_distance_concave(): prism_faces = [[0,1,9,8],[2,8,9,4],[2,4,7,6],[3,6,7,5],[3,5,11,10],[1,0,10,11],[0,8,2,6,3,10],[1,11,5,7,4,9]] prismcube = Polyhedron(vertices=prismcube_verts, faces=prism_faces) - prism_distances = prismcube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) - prism_displacements = prismcube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + prism_distances = prismcube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) + prism_displacements = prismcube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) np.testing.assert_allclose(np.abs(prism_distances), np.linalg.norm(prism_displacements, axis=1)) - prism_surface_distance = prismcube.shortest_distance_to_surface(x_points + prism_displacements, translation_vector=np.array([3,3,3])) - prism_surface_displacement = prismcube.shortest_displacement_to_surface(x_points + prism_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(prism_surface_distance, np.zeros((len(x_points)))) - np.testing.assert_allclose(prism_surface_displacement, np.zeros((len(x_points),3))) + prism_surface_distance = prismcube.shortest_distance_to_surface(test_points + prism_displacements, translation_vector=np.array([3,3,3])) + prism_surface_displacement = prismcube.shortest_displacement_to_surface(test_points + prism_displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(prism_surface_distance, np.zeros((len(test_points)))) + np.testing.assert_allclose(prism_surface_displacement, np.zeros((len(test_points),3))) prism_true_distances = np.array([np.sqrt(70)/5, 1, 3/np.sqrt(5), 1/np.sqrt(5), 0, -0.5, 0.5, -1, -1*np.sqrt(0.5), -0.25]) @@ -1024,14 +1024,14 @@ def test_shortest_distance_concave(): indented_faces = [[0,3,5,1],[0,2,6,3],[1,5,7,4],[2,4,7,6],[3,6,7,5],[0,1,8],[0,8,2],[2,8,4],[1,4,8]] indented_cube = Polyhedron(vertices=indented_cube_verts, faces=indented_faces) - indented_distances = indented_cube.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) - indented_displacements = indented_cube.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + indented_distances = indented_cube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) + indented_displacements = indented_cube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) np.testing.assert_allclose(np.abs(indented_distances), np.linalg.norm(indented_displacements, axis=1)) - indented_surface_distance = indented_cube.shortest_distance_to_surface(x_points + indented_displacements, translation_vector=np.array([3,3,3])) - indented_surface_displacement = indented_cube.shortest_displacement_to_surface(x_points + indented_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(indented_surface_distance, np.zeros((len(x_points))), atol=2e-10) - np.testing.assert_allclose(indented_surface_displacement, np.zeros((len(x_points),3)), atol=2e-10) + indented_surface_distance = indented_cube.shortest_distance_to_surface(test_points + indented_displacements, translation_vector=np.array([3,3,3])) + indented_surface_displacement = indented_cube.shortest_displacement_to_surface(test_points + indented_displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(indented_surface_distance, np.zeros((len(test_points))), atol=2e-10) + np.testing.assert_allclose(indented_surface_displacement, np.zeros((len(test_points),3)), atol=2e-10) indented_true_distances = np.array([np.sqrt(3), 1, np.sqrt(2), 1, np.sqrt(5), -0.1714985851, np.sqrt(1.25), 1/np.sqrt(13), 0.5/np.sqrt(13), -0.25]) diff --git a/tests/test_spheropolygon.py b/tests/test_spheropolygon.py index 2b3a0ddf..88bba452 100644 --- a/tests/test_spheropolygon.py +++ b/tests/test_spheropolygon.py @@ -243,10 +243,10 @@ def test_shortest_distance_convex(): tri_verts = np.array([[0, 0.5], [-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25]]) triangle = ConvexSpheropolygon(vertices=tri_verts, radius = 0.25) - x_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1], [3.5,3.75,1], [3-0.25*np.sqrt(3), 2.65,0], [3,4,-1]]) + test_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1], [3.5,3.75,1], [3-0.25*np.sqrt(3), 2.65,0], [3,4,-1]]) - distances = triangle.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,0])) - displacements = triangle.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,0])) + distances = triangle.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,0])) + displacements = triangle.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,0])) true_distances = np.array([0.0580127018, 0, 0, 1, 1, 1.0463612304, 0, 1.0307764064]) true_displacements = np.array([[-0.0502404735, -0.0290063509, 0], [0,0,0], [0,0,0], [0,0,-1], [0,0,1], [-0.2667468244, -0.1540063509, -1], [0,0,0], [0,-0.25,1]]) diff --git a/tests/test_spheropolyhedron.py b/tests/test_spheropolyhedron.py index c3350d1e..290db43a 100644 --- a/tests/test_spheropolyhedron.py +++ b/tests/test_spheropolyhedron.py @@ -166,20 +166,20 @@ def test_shortest_distance_convex(): verts = np.array([[1,1,1], [-1,1,1], [1,-1,1], [1,1,-1], [-1,-1,1], [-1,1,-1],[1,-1,-1],[-1,-1,-1]]) poly = ConvexSpheropolyhedron(vertices=verts, radius = radius) - x_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[4,4,4],[4,4,3],[4,3,3]]) + test_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[4,4,4],[4,4,3],[4,3,3]]) - distances = poly.shortest_distance_to_surface(x_points, translation_vector=np.array([3,3,3])) - displacements = poly.shortest_displacement_to_surface(x_points, translation_vector=np.array([3,3,3])) + distances = poly.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) + displacements = poly.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) print(distances) print(displacements) np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - poly_surface_distance = poly.shortest_distance_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) - poly_surface_displacement = poly.shortest_displacement_to_surface(x_points + displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(poly_surface_distance, np.zeros((len(x_points))), atol=1e-10) - np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(x_points),3)), atol=1e-10) + poly_surface_distance = poly.shortest_distance_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) + poly_surface_displacement = poly.shortest_displacement_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) + np.testing.assert_allclose(poly_surface_distance, np.zeros((len(test_points))), atol=1e-10) + np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(test_points),3)), atol=1e-10) true_distances = np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2, 0, 0, 0]) - radius true_displacements = np.array([[0,0,-1], [-1,-1,1], [-1,0,0], [0,-1,-1], [0,0,-1], [0,0,-2]]) From f9c2f6354aabcd89ae4fff0cef6216a0c372470c Mon Sep 17 00:00:00 2001 From: janbridley Date: Thu, 18 Dec 2025 15:48:10 -0500 Subject: [PATCH 8/8] prek --- coxeter/shapes/_distance2d.py | 838 +++++++++----- coxeter/shapes/_distance3d.py | 1230 ++++++++++++++------- coxeter/shapes/convex_spheropolygon.py | 36 +- coxeter/shapes/convex_spheropolyhedron.py | 45 +- coxeter/shapes/polygon.py | 48 +- coxeter/shapes/polyhedron.py | 85 +- tests/test_polygon.py | 141 ++- tests/test_polyhedron.py | 338 ++++-- tests/test_spheropolygon.py | 119 +- tests/test_spheropolyhedron.py | 120 +- 10 files changed, 2026 insertions(+), 974 deletions(-) diff --git a/coxeter/shapes/_distance2d.py b/coxeter/shapes/_distance2d.py index a86103fd..428f70e1 100644 --- a/coxeter/shapes/_distance2d.py +++ b/coxeter/shapes/_distance2d.py @@ -1,54 +1,83 @@ +# Copyright (c) 2015-2025 The Regents of the University of Michigan. +# This file is from the coxeter project, released under the BSD 3-Clause License. + import numpy as np import numpy.linalg as LA -#TODO: update docstrings? +# TODO: update docstrings? + -def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: - ''' +def point_to_edge_distance( + point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray +) -> np.ndarray: + """ Calculates the distances between several points and several varying lines. - n is the total number of distance calculations that are being made. For example, let's say + n is the total number of distance calculations that are being made. For example, let's say we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the distances between: - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] Args: - point (np.ndarray): the positions of the points [shape = (n, 3)] + point (np.ndarray): the positions of the points [shape = (n, 3)] vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] - Returns: + Returns + ------- np.ndarray: distances [shape = (n,)] - ''' - edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges - - dist = LA.norm(((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)), axis=1) #distances + """ + edge_unit = edge_vector / np.expand_dims( + LA.norm(edge_vector, axis=1), axis=1 + ) # unit vectors of the edges + + dist = LA.norm( + ( + (vert - point) + - ( + np.expand_dims(np.sum((vert - point) * edge_unit, axis=1), axis=1) + * edge_unit + ) + ), + axis=1, + ) # distances return dist -def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: - ''' + +def point_to_edge_displacement( + point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray +) -> np.ndarray: + """ Calculates the displacements between several points and several varying lines. - n is the total number of displacement calculations that are being made. For example, let's say + n is the total number of displacement calculations that are being made. For example, let's say we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the displacements between: - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] Args: - point (np.ndarray): the positions of the points [shape = (n, 3)] + point (np.ndarray): the positions of the points [shape = (n, 3)] vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] - Returns: + Returns + ------- np.ndarray: displacements [shape = (n, 3)] - ''' - edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges - - disp = ((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)) #displacements + """ + edge_unit = edge_vector / np.expand_dims( + LA.norm(edge_vector, axis=1), axis=1 + ) # unit vectors of the edges + + disp = (vert - point) - ( + np.expand_dims(np.sum((vert - point) * edge_unit, axis=1), axis=1) * edge_unit + ) # displacements return disp -def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: - ''' + +def point_to_face_distance( + point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray +) -> np.ndarray: + """ Calculates the distances between a single point and the plane of the polygon. Args: @@ -56,18 +85,23 @@ def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np. vert (np.ndarray): a point that lies on the plane of the polygon [shape=(3,)] face_normal (np.ndarray): the normal that describes the plane of the polygon [shape=(3,)] - Returns: + Returns + ------- np.ndarray: distances [shape = (n_points,)] - ''' - + """ vert_point_vect = vert - point - face_unit = face_normal / LA.norm(face_normal) #unit vector of the normal of the polygon - dist = abs(vert_point_vect@np.transpose(face_unit)) + face_unit = face_normal / LA.norm( + face_normal + ) # unit vector of the normal of the polygon + dist = abs(vert_point_vect @ np.transpose(face_unit)) return dist -def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: - ''' + +def point_to_face_displacement( + point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray +) -> np.ndarray: + """ Calculates the displacements between a single point and the plane of the polygon. Args: @@ -75,114 +109,176 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: vert (np.ndarray): a point that lies on the plane of the polygon (shape=(3,)) face_normal (np.ndarray): the normal that describes the plane of the polygon (shape=(3,)) - Returns: + Returns + ------- np.ndarray: displacements (n_points, 3) - ''' + """ vert_point_vect = vert - point - face_unit = face_normal / LA.norm(face_normal) #unit vector of the normal of the polygon - disp = np.expand_dims(np.sum(vert_point_vect*face_unit, axis=1), axis=1) * face_unit #*(-1) + face_unit = face_normal / LA.norm( + face_normal + ) # unit vector of the normal of the polygon + disp = ( + np.expand_dims(np.sum(vert_point_vect * face_unit, axis=1), axis=1) * face_unit + ) # *(-1) return disp -def get_vert_zones (shape): - ''' - Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones - where the shortest distance from any point that is within a vertex zone is the distance between the + +def get_vert_zones(shape): + """ + Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones + where the shortest distance from any point that is within a vertex zone is the distance between the point and the corresponding vertex. Args: #will be just `self` once added into coxeter - Returns: + Returns + ------- dict: "constraint": np.ndarray [shape = (n_verts, 2, 3)], "bounds": np.ndarray [shape = (n_verts, 2)] - ''' - vert_constraint = np.append( - np.expand_dims(shape.edge_vectors,axis=1), - -1*np.expand_dims(np.append(np.expand_dims(shape.edge_vectors[-1],axis=0), shape.edge_vectors[:-1], axis=0), axis=1), - axis=1) - - vert_bounds = np.sum(vert_constraint * np.expand_dims(shape.vertices, axis=1), axis=2) - - return {"constraint": vert_constraint, "bounds":vert_bounds} - -def get_edge_zones (shape): - ''' - Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones - where the shortest distance from any point that is within an edge zone is the distance between the + """ + vert_constraint = np.append( + np.expand_dims(shape.edge_vectors, axis=1), + -1 + * np.expand_dims( + np.append( + np.expand_dims(shape.edge_vectors[-1], axis=0), + shape.edge_vectors[:-1], + axis=0, + ), + axis=1, + ), + axis=1, + ) + + vert_bounds = np.sum( + vert_constraint * np.expand_dims(shape.vertices, axis=1), axis=2 + ) + + return {"constraint": vert_constraint, "bounds": vert_bounds} + + +def get_edge_zones(shape): + """ + Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones + where the shortest distance from any point that is within an edge zone is the distance between the point and the corresponding edge. Args: #will be just `self` once added into coxeter - Returns: + Returns + ------- dict: "constraint": np.ndarray [shape = (n_edges, 3, 3)], "bounds": np.ndarray [shape = (n_edges, 3)] - ''' - #vectors that are 90 degrees from the edges and point inwards - edges_90 = -1*np.expand_dims(np.cross(shape.edge_vectors, shape.normal), axis=1) - - #Calculating the constraint [shape = (n_edges, 3, 3)] - edge_constraint = np.append( -1*np.expand_dims(shape.edge_vectors, axis=1) , np.expand_dims(shape.edge_vectors, axis=1), axis=1 ) - edge_constraint = np.append( edge_constraint, edges_90 , axis=1) - - #Bounds [shape = (n_edges, 3)] + """ + # vectors that are 90 degrees from the edges and point inwards + edges_90 = -1 * np.expand_dims(np.cross(shape.edge_vectors, shape.normal), axis=1) + + # Calculating the constraint [shape = (n_edges, 3, 3)] + edge_constraint = np.append( + -1 * np.expand_dims(shape.edge_vectors, axis=1), + np.expand_dims(shape.edge_vectors, axis=1), + axis=1, + ) + edge_constraint = np.append(edge_constraint, edges_90, axis=1) + + # Bounds [shape = (n_edges, 3)] edge_bounds = np.zeros((shape.num_vertices, 3)) - edge_bounds[:,0] = np.sum(edge_constraint[:,0] *(shape.vertices), axis=1) - edge_bounds[:,1] = np.sum(edge_constraint[:,1] *(np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0],axis=0), axis=0)), axis=1) - edge_bounds[:,2] = np.sum(edge_constraint[:,2] *(np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0],axis=0), axis=0)), axis=1) - - return {"constraint":edge_constraint, "bounds":edge_bounds} - -def get_face_zones (shape): - ''' - Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones - where the shortest distance from any point that is within a triangulated face zone is the distance between the + edge_bounds[:, 0] = np.sum(edge_constraint[:, 0] * (shape.vertices), axis=1) + edge_bounds[:, 1] = np.sum( + edge_constraint[:, 1] + * ( + np.append( + shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0 + ) + ), + axis=1, + ) + edge_bounds[:, 2] = np.sum( + edge_constraint[:, 2] + * ( + np.append( + shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0 + ) + ), + axis=1, + ) + + return {"constraint": edge_constraint, "bounds": edge_bounds} + + +def get_face_zones(shape): + """ + Gets the constraints and bounds needed to partition the volume surrounding a polygon into zones + where the shortest distance from any point that is within a triangulated face zone is the distance between the point and the corresponding triangulated face. Args: #will be just `self` once added into coxeter - Returns: - dict: "constraint": np.ndarray , "bounds": np.ndarray - ''' - face_constraint = np.cross(shape.edge_vectors, shape.normal) #only one face zone for a polygon | shape = (n_edges, 3) - face_bounds = np.sum(face_constraint * shape.vertices, axis=1) #shape = (n_edges,) - - #Checking to see if all the vertices are in the face zone. If not, the polygon is nonconvex. - if np.all(face_constraint @ np.transpose(shape.vertices) <= np.expand_dims(face_bounds, axis=1)+5e-6) == False: - #--Polygon is nonconvex and needs to be triangulated-- - triangle_verts =[] + Returns + ------- + dict: "constraint": np.ndarray , "bounds": np.ndarray + """ + face_constraint = np.cross( + shape.edge_vectors, shape.normal + ) # only one face zone for a polygon | shape = (n_edges, 3) + face_bounds = np.sum(face_constraint * shape.vertices, axis=1) # shape = (n_edges,) + + # Checking to see if all the vertices are in the face zone. If not, the polygon is nonconvex. + if ( + np.all( + face_constraint @ np.transpose(shape.vertices) + <= np.expand_dims(face_bounds, axis=1) + 5e-6 + ) + == False + ): + # --Polygon is nonconvex and needs to be triangulated-- + triangle_verts = [] for tri in shape._triangulation(): triangle_verts.append(list(tri)) - - triangle_verts = np.asarray(triangle_verts) - tri_edges = np.append(triangle_verts[:,1:], np.expand_dims(triangle_verts[:,0], axis=1), axis=1) - triangle_verts #edges point counterclockwise - face_constraint = np.cross(tri_edges, shape.normal) #shape = (n_triangles, 3, 3) - face_bounds = np.sum(face_constraint*triangle_verts, axis=2) #shape = (n_triangles, 3) + triangle_verts = np.asarray(triangle_verts) + tri_edges = ( + np.append( + triangle_verts[:, 1:], + np.expand_dims(triangle_verts[:, 0], axis=1), + axis=1, + ) + - triangle_verts + ) # edges point counterclockwise + + face_constraint = np.cross( + tri_edges, shape.normal + ) # shape = (n_triangles, 3, 3) + face_bounds = np.sum( + face_constraint * triangle_verts, axis=2 + ) # shape = (n_triangles, 3) else: - #--Polygon is convex-- - face_constraint = np.expand_dims(face_constraint, axis=0) #shape = (1, n_edges, 3) - face_bounds = np.expand_dims(face_bounds, axis=0) #shape = (1, n_edges) - - return {"constraint":face_constraint, "bounds":face_bounds} + # --Polygon is convex-- + face_constraint = np.expand_dims( + face_constraint, axis=0 + ) # shape = (1, n_edges, 3) + face_bounds = np.expand_dims(face_bounds, axis=0) # shape = (1, n_edges) + return {"constraint": face_constraint, "bounds": face_bounds} -def shortest_distance_to_surface ( - shape, - points: np.ndarray, - translation_vector: np.ndarray, +def shortest_distance_to_surface( + shape, + points: np.ndarray, + translation_vector: np.ndarray, ) -> np.ndarray: - ''' - Solves for the shortest distance between points and the surface of a polygon. + """ + Solves for the shortest distance between points and the surface of a polygon. If the point lies inside the polyhedron, the distance is negative. - This function calculates the shortest distance by partitioning the space around - a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + This function calculates the shortest distance by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a point lies in, determines the distance calculation(s) done. For a vertex zone, - the distance is calculated between a point and the vertex. For an edge zone, the + the distance is calculated between a point and the vertex. For an edge zone, the distance is calculated between a point and the edge. For a face zone, the distance is calculated between a point and the face. Zones are allowed to overlap, and points can be in more than one zone. By taking the minimum of all the calculated distances, @@ -192,84 +288,128 @@ def shortest_distance_to_surface ( points (list or np.ndarray): positions of the points translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,) or (2,)] - Returns: + Returns + ------- np.ndarray: shortest distances [shape = (n_points,)] - ''' - + """ points = np.asarray(points) translation_vector = np.asarray(translation_vector) if len(points.shape) == 1: points = np.expand_dims(points, axis=0) - n_points = len(points) #number of inputted points + n_points = len(points) # number of inputted points if points.shape[1] == 2: - points = np.append(points, np.zeros((n_points,1)), axis=1) + points = np.append(points, np.zeros((n_points, 1)), axis=1) - if translation_vector.shape[0]>3 or len(translation_vector.shape)>1 or translation_vector.shape[0]<2: - raise ValueError(f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}") + if ( + translation_vector.shape[0] > 3 + or len(translation_vector.shape) > 1 + or translation_vector.shape[0] < 2 + ): + raise ValueError( + f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}" + ) if translation_vector.shape[0] == 2: translation_vector = np.append(translation_vector, [0]) - #Updating bounds with the position of the polyhedron - vert_bounds = shape.vertex_zones["bounds"] + (shape.vertex_zones["constraint"] @ translation_vector) - edge_bounds = shape.edge_zones["bounds"] + (shape.edge_zones["constraint"] @ translation_vector) - face_bounds = shape.face_zones["bounds"] + (shape.face_zones["constraint"] @ translation_vector) - + # Updating bounds with the position of the polyhedron + vert_bounds = shape.vertex_zones["bounds"] + ( + shape.vertex_zones["constraint"] @ translation_vector + ) + edge_bounds = shape.edge_zones["bounds"] + ( + shape.edge_zones["constraint"] @ translation_vector + ) + face_bounds = shape.face_zones["bounds"] + ( + shape.face_zones["constraint"] @ translation_vector + ) points_trans = np.transpose(points) - max_value = 3*np.max(LA.norm(points - (translation_vector+shape.vertices[0]), axis=1)) + max_value = 3 * np.max( + LA.norm(points - (translation_vector + shape.vertices[0]), axis=1) + ) - min_dist_arr = np.ones((len(points),1))*max_value + min_dist_arr = np.ones((len(points), 1)) * max_value - #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shape.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + # Solving for the distances between the points and any relevant vertices + vert_bool = np.all( + (shape.vertex_zones["constraint"] @ points_trans) + <= np.expand_dims(vert_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_vertex_zones, number_of_points) if np.any(vert_bool): - - #v--- shape = (number of True in vert_bool,) ---v - vert_used = np.transpose(np.tile(np.arange(0,shape.num_vertices,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - v_points_used = np.tile(np.arange(0,n_points,1), (shape.num_vertices,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - - vert_dist = np.ones((shape.num_vertices,n_points))*max_value - vert_dist[vert_bool]=LA.norm(points[v_points_used] - (shape.vertices[vert_used] + translation_vector), axis=1) #Distances between two points - vert_dist = np.transpose(vert_dist) #<--- shape = (n_points, n_verts) + # v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose( + np.tile(np.arange(0, shape.num_vertices, 1), (n_points, 1)) + )[ + vert_bool + ] # Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0, n_points, 1), (shape.num_vertices, 1))[ + vert_bool + ] # Contains the indices of the points that hold True for vert_bool + + vert_dist = np.ones((shape.num_vertices, n_points)) * max_value + vert_dist[vert_bool] = LA.norm( + points[v_points_used] - (shape.vertices[vert_used] + translation_vector), + axis=1, + ) # Distances between two points + vert_dist = np.transpose(vert_dist) # <--- shape = (n_points, n_verts) vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) vert_dist = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) min_dist_arr = np.concatenate((min_dist_arr, vert_dist), axis=1) - -# Solving for the distances between the points and any relevant edges - edge_bool = np.all((shape.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + # Solving for the distances between the points and any relevant edges + edge_bool = np.all( + (shape.edge_zones["constraint"] @ points_trans) + <= np.expand_dims(edge_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_edge_zones, number_of_points) if np.any(edge_bool): - - #v--- shape = (number of True in edge_bool,) ---v - edge_used = np.transpose(np.tile(np.arange(0,shape.num_vertices,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool - e_points_used = np.tile(np.arange(0,n_points,1), (shape.num_vertices,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool - - vert_on_edge = shape.vertices[shape.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges - edge_vectors = np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0) - shape.vertices - - edge_dist = np.ones((shape.num_vertices,n_points))*max_value - edge_dist[edge_bool]=point_to_edge_distance(points[e_points_used], vert_on_edge, edge_vectors[edge_used]) #Distances between a point and a line - edge_dist = np.transpose(edge_dist) #<--- shape = (n_points, n_edges) + # v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose( + np.tile(np.arange(0, shape.num_vertices, 1), (n_points, 1)) + )[edge_bool] # Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0, n_points, 1), (shape.num_vertices, 1))[ + edge_bool + ] # Contains the indices of the points that hold True for edge_bool + + vert_on_edge = ( + shape.vertices[shape.edges[edge_used][:, 0]] + translation_vector + ) # Vertices that lie on the needed edges + edge_vectors = ( + np.append( + shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0 + ) + - shape.vertices + ) + + edge_dist = np.ones((shape.num_vertices, n_points)) * max_value + edge_dist[edge_bool] = point_to_edge_distance( + points[e_points_used], vert_on_edge, edge_vectors[edge_used] + ) # Distances between a point and a line + edge_dist = np.transpose(edge_dist) # <--- shape = (n_points, n_edges) edge_dist_arg = np.expand_dims(np.argmin(abs(edge_dist), axis=1), axis=1) edge_dist = np.take_along_axis(edge_dist, edge_dist_arg, axis=1) min_dist_arr = np.concatenate((min_dist_arr, edge_dist), axis=1) - - face_bool = np.all((shape.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + face_bool = np.all( + (shape.face_zones["constraint"] @ points_trans) + <= np.expand_dims(face_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_face_zones, number_of_points) if np.any(face_bool): - vert_on_face = shape.vertices[0] + translation_vector face_dist = point_to_face_distance(points, vert_on_face, shape.normal) - face_dist = face_dist + max_value*(np.any(face_bool,axis=0) == False).astype(int) + face_dist = face_dist + max_value * (np.any(face_bool, axis=0) == False).astype( + int + ) face_dist = np.expand_dims(face_dist, axis=1) min_dist_arr = np.concatenate((min_dist_arr, face_dist), axis=1) @@ -278,20 +418,21 @@ def shortest_distance_to_surface ( return true_min_dist -def shortest_displacement_to_surface ( - shape, - points: np.ndarray, - translation_vector: np.ndarray, + +def shortest_displacement_to_surface( + shape, + points: np.ndarray, + translation_vector: np.ndarray, ) -> np.ndarray: - ''' + """ Solves for the shortest displacement between points and the surface of a polyhedron. - This function calculates the shortest displacement by partitioning the space around - a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a point lies in, determines the displacement calculation(s) done. For a vertex zone, - the displacement is calculated between a point and the vertex. For an edge zone, the - displacement is calculated between a point and the edge. For a face zone, the - displacement is calculated between a point and the face. Zones are allowed to overlap, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, and points can be in more than one zone. By taking the minimum of all the distances of the calculated displacements, the shortest displacements are found. @@ -299,106 +440,169 @@ def shortest_displacement_to_surface ( points (list or np.ndarray): positions of the points translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,) or (2,)] - Returns: + Returns + ------- np.ndarray: shortest displacements [shape = (n_points, 3)] - ''' + """ points = np.asarray(points) translation_vector = np.asarray(translation_vector) if points.shape == (3,) or points.shape == (2,): points = np.expand_dims(points, axis=0) - n_points = len(points) #number of inputted points - n_verts = shape.num_vertices #number of vertices = number of vertex zones - n_edges = n_verts #number of edges = number of edge zones + n_points = len(points) # number of inputted points + n_verts = shape.num_vertices # number of vertices = number of vertex zones + n_edges = n_verts # number of edges = number of edge zones if points.shape[1] == 2: - points = np.append(points, np.zeros((n_points,1)), axis=1) + points = np.append(points, np.zeros((n_points, 1)), axis=1) - if translation_vector.shape[0]>3 or len(translation_vector.shape)>1 or translation_vector.shape[0]<2: - raise ValueError(f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}") + if ( + translation_vector.shape[0] > 3 + or len(translation_vector.shape) > 1 + or translation_vector.shape[0] < 2 + ): + raise ValueError( + f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}" + ) if translation_vector.shape[0] == 2: translation_vector = np.append(translation_vector, [0]) - #Updating bounds with the position of the polyhedron - vert_bounds = shape.vertex_zones["bounds"] + (shape.vertex_zones["constraint"] @ translation_vector) - edge_bounds = shape.edge_zones["bounds"] + (shape.edge_zones["constraint"] @ translation_vector) - face_bounds = shape.face_zones["bounds"] + (shape.face_zones["constraint"] @ translation_vector) - + # Updating bounds with the position of the polyhedron + vert_bounds = shape.vertex_zones["bounds"] + ( + shape.vertex_zones["constraint"] @ translation_vector + ) + edge_bounds = shape.edge_zones["bounds"] + ( + shape.edge_zones["constraint"] @ translation_vector + ) + face_bounds = shape.face_zones["bounds"] + ( + shape.face_zones["constraint"] @ translation_vector + ) points_trans = np.transpose(points) - max_value = 3*np.max(LA.norm(points - (translation_vector+shape.vertices[0]), axis=1)) + max_value = 3 * np.max( + LA.norm(points - (translation_vector + shape.vertices[0]), axis=1) + ) - min_disp_arr = np.ones((n_points,1, 3))*max_value + min_disp_arr = np.ones((n_points, 1, 3)) * max_value - #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shape.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + # Solving for the distances between the points and any relevant vertices + vert_bool = np.all( + (shape.vertex_zones["constraint"] @ points_trans) + <= np.expand_dims(vert_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_vertex_zones, number_of_points) if np.any(vert_bool): - - #v--- shape = (number of True in vert_bool,) ---v - vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - - vert_disp = np.ones((n_verts,n_points,3))*max_value - vert_disp[vert_bool]=(shape.vertices[vert_used] + translation_vector) - points[v_points_used] #Displacements between two points - vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) - - vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + # v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0, n_verts, 1), (n_points, 1)))[ + vert_bool + ] # Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0, n_points, 1), (n_verts, 1))[ + vert_bool + ] # Contains the indices of the points that hold True for vert_bool + + vert_disp = np.ones((n_verts, n_points, 3)) * max_value + vert_disp[vert_bool] = ( + shape.vertices[vert_used] + translation_vector + ) - points[v_points_used] # Displacements between two points + vert_disp = np.transpose( + vert_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_verts, 3) + + vert_disp_min = np.expand_dims( + np.argmin(LA.norm(vert_disp, axis=2), axis=1), axis=(1, 2) + ) vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) - -# Solving for the distances between the points and any relevant edges - edge_bool = np.all((shape.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + # Solving for the distances between the points and any relevant edges + edge_bool = np.all( + (shape.edge_zones["constraint"] @ points_trans) + <= np.expand_dims(edge_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_edge_zones, number_of_points) if np.any(edge_bool): - - #v--- shape = (number of True in edge_bool,) ---v - edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool - e_points_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool - - vert_on_edge = shape.vertices[shape.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges - edge_vectors = np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0) - shape.vertices - - edge_disp = np.ones((n_edges,n_points,3))*max_value - edge_disp[edge_bool]=point_to_edge_displacement(points[e_points_used], vert_on_edge, edge_vectors[edge_used]) #Displacements between a point and a line - edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) - - edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + # v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0, n_edges, 1), (n_points, 1)))[ + edge_bool + ] # Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0, n_points, 1), (n_edges, 1))[ + edge_bool + ] # Contains the indices of the points that hold True for edge_bool + + vert_on_edge = ( + shape.vertices[shape.edges[edge_used][:, 0]] + translation_vector + ) # Vertices that lie on the needed edges + edge_vectors = ( + np.append( + shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0 + ) + - shape.vertices + ) + + edge_disp = np.ones((n_edges, n_points, 3)) * max_value + edge_disp[edge_bool] = point_to_edge_displacement( + points[e_points_used], vert_on_edge, edge_vectors[edge_used] + ) # Displacements between a point and a line + edge_disp = np.transpose( + edge_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_edges, 3) + + edge_disp_arg = np.expand_dims( + np.argmin(LA.norm(edge_disp, axis=2), axis=1), axis=(1, 2) + ) edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) - - face_bool = np.all((shape.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + face_bool = np.all( + (shape.face_zones["constraint"] @ points_trans) + <= np.expand_dims(face_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_face_zones, number_of_points) if np.any(face_bool): - - face_disp = point_to_face_displacement(points, shape.vertices[0]+translation_vector, shape.normal) + np.repeat(np.expand_dims((max_value*(np.any(face_bool,axis=0) == False).astype(int)), axis=1), 3, axis=1) - - min_disp_arr = np.concatenate((min_disp_arr, np.expand_dims(face_disp, axis=1)), axis=1) - - disp_list_bool = np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1).reshape(n_points, 1, 1) - true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_list_bool, axis=1), axis=1) + face_disp = point_to_face_displacement( + points, shape.vertices[0] + translation_vector, shape.normal + ) + np.repeat( + np.expand_dims( + (max_value * (np.any(face_bool, axis=0) == False).astype(int)), axis=1 + ), + 3, + axis=1, + ) + + min_disp_arr = np.concatenate( + (min_disp_arr, np.expand_dims(face_disp, axis=1)), axis=1 + ) + + disp_list_bool = np.argmin((LA.norm(min_disp_arr, axis=2)), axis=1).reshape( + n_points, 1, 1 + ) + true_min_disp = np.squeeze( + np.take_along_axis(min_disp_arr, disp_list_bool, axis=1), axis=1 + ) return true_min_disp -def spheropolygon_shortest_displacement_to_surface ( - shape, - radius, - points: np.ndarray, - translation_vector: np.ndarray, + +def spheropolygon_shortest_displacement_to_surface( + shape, + radius, + points: np.ndarray, + translation_vector: np.ndarray, ) -> np.ndarray: - ''' + """ Solves for the shortest displacement between points and the surface of a polyhedron. - This function calculates the shortest displacement by partitioning the space around - a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a point lies in, determines the displacement calculation(s) done. For a vertex zone, - the displacement is calculated between a point and the vertex. For an edge zone, the - displacement is calculated between a point and the edge. For a face zone, the - displacement is calculated between a point and the face. Zones are allowed to overlap, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, and points can be in more than one zone. By taking the minimum of all the distances of the calculated displacements, the shortest displacements are found. @@ -406,99 +610,187 @@ def spheropolygon_shortest_displacement_to_surface ( points (list or np.ndarray): positions of the points translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,) or (2,)] - Returns: + Returns + ------- np.ndarray: shortest displacements [shape = (n_points, 3)] - ''' + """ points = np.asarray(points) translation_vector = np.asarray(translation_vector) if points.shape == (3,) or points.shape == (2,): points = np.expand_dims(points, axis=0) - n_points = len(points) #number of inputted points - n_verts = shape.num_vertices #number of vertices = number of vertex zones - n_edges = n_verts #number of edges = number of edge zones + n_points = len(points) # number of inputted points + n_verts = shape.num_vertices # number of vertices = number of vertex zones + n_edges = n_verts # number of edges = number of edge zones if points.shape[1] == 2: - points = np.append(points, np.zeros((n_points,1)), axis=1) + points = np.append(points, np.zeros((n_points, 1)), axis=1) - if translation_vector.shape[0]>3 or len(translation_vector.shape)>1 or translation_vector.shape[0]<2: - raise ValueError(f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}") + if ( + translation_vector.shape[0] > 3 + or len(translation_vector.shape) > 1 + or translation_vector.shape[0] < 2 + ): + raise ValueError( + f"Expected the shape of the polygon's position to be either (2,) or (3,), instead it got {translation_vector.shape}" + ) if translation_vector.shape[0] == 2: translation_vector = np.append(translation_vector, [0]) - #Updating bounds with the position of the polyhedron - vert_bounds = shape.vertex_zones["bounds"] + (shape.vertex_zones["constraint"] @ translation_vector) - edge_bounds = shape.edge_zones["bounds"] + (shape.edge_zones["constraint"] @ translation_vector) - face_bounds = shape.face_zones["bounds"] + (shape.face_zones["constraint"] @ translation_vector) - + # Updating bounds with the position of the polyhedron + vert_bounds = shape.vertex_zones["bounds"] + ( + shape.vertex_zones["constraint"] @ translation_vector + ) + edge_bounds = shape.edge_zones["bounds"] + ( + shape.edge_zones["constraint"] @ translation_vector + ) + face_bounds = shape.face_zones["bounds"] + ( + shape.face_zones["constraint"] @ translation_vector + ) points_trans = np.transpose(points) - max_value = 3*np.max(LA.norm(points - (translation_vector+shape.vertices[0]), axis=1)) + max_value = 3 * np.max( + LA.norm(points - (translation_vector + shape.vertices[0]), axis=1) + ) - min_disp_arr = np.ones((n_points,1, 3))*max_value + min_disp_arr = np.ones((n_points, 1, 3)) * max_value - #Solving for the distances between the points and any relevant vertices - vert_bool = np.all((shape.vertex_zones["constraint"] @ points_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (number_of_vertex_zones, number_of_points) + # Solving for the distances between the points and any relevant vertices + vert_bool = np.all( + (shape.vertex_zones["constraint"] @ points_trans) + <= np.expand_dims(vert_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_vertex_zones, number_of_points) if np.any(vert_bool): - - #v--- shape = (number of True in vert_bool,) ---v - vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - v_points_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - - vert_disp = np.ones((n_verts,n_points,3))*max_value - vert_disp[vert_bool]=(shape.vertices[vert_used] + translation_vector) - points[v_points_used] #Displacements between two points - vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) - - vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + # v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0, n_verts, 1), (n_points, 1)))[ + vert_bool + ] # Contains the indices of the vertices that hold True for vert_bool + v_points_used = np.tile(np.arange(0, n_points, 1), (n_verts, 1))[ + vert_bool + ] # Contains the indices of the points that hold True for vert_bool + + vert_disp = np.ones((n_verts, n_points, 3)) * max_value + vert_disp[vert_bool] = ( + shape.vertices[vert_used] + translation_vector + ) - points[v_points_used] # Displacements between two points + vert_disp = np.transpose( + vert_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_verts, 3) + + vert_disp_min = np.expand_dims( + np.argmin(LA.norm(vert_disp, axis=2), axis=1), axis=(1, 2) + ) vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) - #for spheropolygon - vert_projection = np.squeeze(vert_disp - np.expand_dims(vert_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)), axis=1) + # for spheropolygon + vert_projection = np.squeeze( + vert_disp + - np.expand_dims( + vert_disp @ (shape.normal / np.linalg.norm(shape.normal)), axis=1 + ) + * (shape.normal / np.linalg.norm(shape.normal)), + axis=1, + ) v_projection_bool = np.linalg.norm(vert_projection, axis=1) > radius - vert_projection[v_projection_bool] = radius * vert_projection[v_projection_bool]/np.expand_dims(np.linalg.norm(vert_projection[v_projection_bool], axis=1), axis=1) + vert_projection[v_projection_bool] = ( + radius + * vert_projection[v_projection_bool] + / np.expand_dims( + np.linalg.norm(vert_projection[v_projection_bool], axis=1), axis=1 + ) + ) vert_disp = vert_disp - vert_projection min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) - -# Solving for the distances between the points and any relevant edges - edge_bool = np.all((shape.edge_zones["constraint"] @ points_trans) <= np.expand_dims(edge_bounds, axis=2), axis=1) #<--- shape = (number_of_edge_zones, number_of_points) + # Solving for the distances between the points and any relevant edges + edge_bool = np.all( + (shape.edge_zones["constraint"] @ points_trans) + <= np.expand_dims(edge_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_edge_zones, number_of_points) if np.any(edge_bool): - - #v--- shape = (number of True in edge_bool,) ---v - edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool - e_points_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool - - vert_on_edge = shape.vertices[shape.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges - edge_vectors = np.append(shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0) - shape.vertices - - edge_disp = np.ones((n_edges,n_points,3))*max_value - edge_disp[edge_bool]=point_to_edge_displacement(points[e_points_used], vert_on_edge, edge_vectors[edge_used]) #Displacements between a point and a line - edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) - - edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + # v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0, n_edges, 1), (n_points, 1)))[ + edge_bool + ] # Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0, n_points, 1), (n_edges, 1))[ + edge_bool + ] # Contains the indices of the points that hold True for edge_bool + + vert_on_edge = ( + shape.vertices[shape.edges[edge_used][:, 0]] + translation_vector + ) # Vertices that lie on the needed edges + edge_vectors = ( + np.append( + shape.vertices[1:], np.expand_dims(shape.vertices[0], axis=0), axis=0 + ) + - shape.vertices + ) + + edge_disp = np.ones((n_edges, n_points, 3)) * max_value + edge_disp[edge_bool] = point_to_edge_displacement( + points[e_points_used], vert_on_edge, edge_vectors[edge_used] + ) # Displacements between a point and a line + edge_disp = np.transpose( + edge_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_edges, 3) + + edge_disp_arg = np.expand_dims( + np.argmin(LA.norm(edge_disp, axis=2), axis=1), axis=(1, 2) + ) edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) - #for spheropolygon - edge_projection = np.squeeze(edge_disp - np.expand_dims(edge_disp @ (shape.normal/np.linalg.norm(shape.normal)), axis=1) * (shape.normal/np.linalg.norm(shape.normal)), axis=1) + # for spheropolygon + edge_projection = np.squeeze( + edge_disp + - np.expand_dims( + edge_disp @ (shape.normal / np.linalg.norm(shape.normal)), axis=1 + ) + * (shape.normal / np.linalg.norm(shape.normal)), + axis=1, + ) e_projection_bool = np.linalg.norm(edge_projection, axis=1) > radius - edge_projection[e_projection_bool] = radius * edge_projection[e_projection_bool]/np.expand_dims(np.linalg.norm(edge_projection[e_projection_bool], axis=1), axis=1) + edge_projection[e_projection_bool] = ( + radius + * edge_projection[e_projection_bool] + / np.expand_dims( + np.linalg.norm(edge_projection[e_projection_bool], axis=1), axis=1 + ) + ) edge_disp = edge_disp - edge_projection min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) - - face_bool = np.all((shape.face_zones["constraint"] @ points_trans) <= np.expand_dims(face_bounds, axis=2), axis=1) #<--- shape = (number_of_face_zones, number_of_points) + face_bool = np.all( + (shape.face_zones["constraint"] @ points_trans) + <= np.expand_dims(face_bounds, axis=2), + axis=1, + ) # <--- shape = (number_of_face_zones, number_of_points) if np.any(face_bool): - - face_disp = point_to_face_displacement(points, shape.vertices[0]+translation_vector, shape.normal) + np.repeat(np.expand_dims((max_value*(np.any(face_bool,axis=0) == False).astype(int)), axis=1), 3, axis=1) - - min_disp_arr = np.concatenate((min_disp_arr, np.expand_dims(face_disp, axis=1)), axis=1) - - disp_list_bool = np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1).reshape(n_points, 1, 1) - true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_list_bool, axis=1), axis=1) + face_disp = point_to_face_displacement( + points, shape.vertices[0] + translation_vector, shape.normal + ) + np.repeat( + np.expand_dims( + (max_value * (np.any(face_bool, axis=0) == False).astype(int)), axis=1 + ), + 3, + axis=1, + ) + + min_disp_arr = np.concatenate( + (min_disp_arr, np.expand_dims(face_disp, axis=1)), axis=1 + ) + + disp_list_bool = np.argmin((LA.norm(min_disp_arr, axis=2)), axis=1).reshape( + n_points, 1, 1 + ) + true_min_disp = np.squeeze( + np.take_along_axis(min_disp_arr, disp_list_bool, axis=1), axis=1 + ) return true_min_disp diff --git a/coxeter/shapes/_distance3d.py b/coxeter/shapes/_distance3d.py index b5dbc461..9df0bb09 100644 --- a/coxeter/shapes/_distance3d.py +++ b/coxeter/shapes/_distance3d.py @@ -1,22 +1,27 @@ +# Copyright (c) 2015-2025 The Regents of the University of Michigan. +# This file is from the coxeter project, released under the BSD 3-Clause License. + import numpy as np import numpy.linalg as LA -#TODO: update docstrings? +# TODO: update docstrings? + -def get_edge_face_neighbors (shape) -> np.ndarray: - ''' +def get_edge_face_neighbors(shape) -> np.ndarray: + """ Gets the indices of the faces that are adjacent to each edge. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- np.ndarray: the indices of the nearest faces for each edge [shape = (n_edges, 2)] - ''' + """ faces_len = shape.num_faces num_edges = shape.num_edges - #appending the vertex list of each face and a -1 to the end of each list of vertices, then flattening the awkward array (the -1 indicates a change in face) + # appending the vertex list of each face and a -1 to the end of each list of vertices, then flattening the awkward array (the -1 indicates a change in face) faces_flat = [] for face in shape.faces: @@ -27,87 +32,138 @@ def get_edge_face_neighbors (shape) -> np.ndarray: faces_flat = np.asarray(faces_flat) - #creating a matrix where each row corresponds to an edge that contains the indices of its two corresponding vertices (a -1 index indicates a change in face) + # creating a matrix where each row corresponds to an edge that contains the indices of its two corresponding vertices (a -1 index indicates a change in face) list_len = len(faces_flat) - face_edge_mat = np.block([np.expand_dims(faces_flat[:-1],axis=1), np.expand_dims(faces_flat[1:],axis=1)]) - - #finding the number of edges associated with each face - fe_mat_inds = np.arange(0,list_len-1,1) - find_num_edges = fe_mat_inds[(fe_mat_inds==0) + (np.any(face_edge_mat==-1, axis=1))] + face_edge_mat = np.block( + [ + np.expand_dims(faces_flat[:-1], axis=1), + np.expand_dims(faces_flat[1:], axis=1), + ] + ) + + # finding the number of edges associated with each face + fe_mat_inds = np.arange(0, list_len - 1, 1) + find_num_edges = fe_mat_inds[ + (fe_mat_inds == 0) + (np.any(face_edge_mat == -1, axis=1)) + ] find_num_edges[:][0] = -1 - find_num_edges = find_num_edges.reshape(faces_len,2) - face_num_edges = find_num_edges[:,1] - find_num_edges[:,0] -1 - - #repeating each face index for the number of edges that are associated with it; length equals num_edges * 2 - face_correspond_inds = np.repeat(np.arange(0,faces_len,1), face_num_edges) - - #shape.edges lists the indices of the edge vertices lowest to highest. edges1_reshape lists the indices of the edge vertices highest to lowest - edges1_reshape = np.hstack((np.expand_dims(shape.edges[:,1], axis=1), np.expand_dims(shape.edges[:,0], axis=1))) - - #For the new_edge_ind_bool: rows correspond with the face_correspond_inds and columns correpond with the edge index; finding the neighboring faces for each edge - true_face_edge_mat = np.tile(np.expand_dims(face_edge_mat[np.all(face_edge_mat!=-1, axis=1)],axis=1), (1, num_edges,1)) - new_edge_ind_bool0 = np.all(true_face_edge_mat == np.expand_dims(shape.edges, axis=0), axis=2).astype(int) #faces to the LEFT of edges if edges are oriented pointing upwards - new_edge_ind_bool1 = np.all(true_face_edge_mat == np.expand_dims(edges1_reshape, axis=0), axis=2).astype(int) #faces to the RIGHT of edges if edges are oriented pointing upwards - - #tiling face_correspond_inds so it can be multiplied to the new_edge_ind_bool0 and new_edge_ind_bool1 - new_face_corr_inds = np.tile(np.expand_dims(face_correspond_inds, axis=1), (1,num_edges)) - - #getting the face indices and completing the edge-face neighbors - ef_neighbor0 = np.expand_dims(np.sum(new_face_corr_inds*new_edge_ind_bool0, axis=0), axis=1) #faces to the LEFT - ef_neighbor1 = np.expand_dims(np.sum(new_face_corr_inds*new_edge_ind_bool1, axis=0), axis=1) #faces to the RIGHT + find_num_edges = find_num_edges.reshape(faces_len, 2) + face_num_edges = find_num_edges[:, 1] - find_num_edges[:, 0] - 1 + + # repeating each face index for the number of edges that are associated with it; length equals num_edges * 2 + face_correspond_inds = np.repeat(np.arange(0, faces_len, 1), face_num_edges) + + # shape.edges lists the indices of the edge vertices lowest to highest. edges1_reshape lists the indices of the edge vertices highest to lowest + edges1_reshape = np.hstack( + ( + np.expand_dims(shape.edges[:, 1], axis=1), + np.expand_dims(shape.edges[:, 0], axis=1), + ) + ) + + # For the new_edge_ind_bool: rows correspond with the face_correspond_inds and columns correpond with the edge index; finding the neighboring faces for each edge + true_face_edge_mat = np.tile( + np.expand_dims(face_edge_mat[np.all(face_edge_mat != -1, axis=1)], axis=1), + (1, num_edges, 1), + ) + new_edge_ind_bool0 = np.all( + true_face_edge_mat == np.expand_dims(shape.edges, axis=0), axis=2 + ).astype(int) # faces to the LEFT of edges if edges are oriented pointing upwards + new_edge_ind_bool1 = np.all( + true_face_edge_mat == np.expand_dims(edges1_reshape, axis=0), axis=2 + ).astype(int) # faces to the RIGHT of edges if edges are oriented pointing upwards + + # tiling face_correspond_inds so it can be multiplied to the new_edge_ind_bool0 and new_edge_ind_bool1 + new_face_corr_inds = np.tile( + np.expand_dims(face_correspond_inds, axis=1), (1, num_edges) + ) + + # getting the face indices and completing the edge-face neighbors + ef_neighbor0 = np.expand_dims( + np.sum(new_face_corr_inds * new_edge_ind_bool0, axis=0), axis=1 + ) # faces to the LEFT + ef_neighbor1 = np.expand_dims( + np.sum(new_face_corr_inds * new_edge_ind_bool1, axis=0), axis=1 + ) # faces to the RIGHT ef_neighbor = np.hstack((ef_neighbor0, ef_neighbor1)) return ef_neighbor -def point_to_edge_distance (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: - ''' + +def point_to_edge_distance( + point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray +) -> np.ndarray: + """ Calculates the distances between several points and several varying lines. - n is the total number of distance calculations that are being made. For example, let's say + n is the total number of distance calculations that are being made. For example, let's say we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the distances between: - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] Args: - point (np.ndarray): the positions of the points [shape = (n, 3)] + point (np.ndarray): the positions of the points [shape = (n, 3)] vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] - Returns: + Returns + ------- np.ndarray: distances [shape = (n,)] - ''' - edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges - - dist = LA.norm(((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)), axis=1) #distances + """ + edge_unit = edge_vector / np.expand_dims( + LA.norm(edge_vector, axis=1), axis=1 + ) # unit vectors of the edges + + dist = LA.norm( + ( + (vert - point) + - ( + np.expand_dims(np.sum((vert - point) * edge_unit, axis=1), axis=1) + * edge_unit + ) + ), + axis=1, + ) # distances return dist -def point_to_edge_displacement (point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray) -> np.ndarray: - ''' + +def point_to_edge_displacement( + point: np.ndarray, vert: np.ndarray, edge_vector: np.ndarray +) -> np.ndarray: + """ Calculates the displacements between several points and several varying lines. - n is the total number of displacement calculations that are being made. For example, let's say + n is the total number of displacement calculations that are being made. For example, let's say we have points: A, B, C & D, and edges: U, V & W, and we want to calculate the displacements between: - A & U, A & W, B & U, C & V, C & W, D & U, D & V, and D & W n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [U,W,U,V,W,U,V,W] Args: - point (np.ndarray): the positions of the points [shape = (n, 3)] + point (np.ndarray): the positions of the points [shape = (n, 3)] vert (np.ndarray): positions of the points that lie on each corresponding line [shape = (n, 3)] edge_vector (np.ndarray): the vectors that describe each line [shape = (n, 3)] - Returns: + Returns + ------- np.ndarray: displacements [shape = (n, 3)] - ''' - edge_unit = edge_vector / np.expand_dims(LA.norm(edge_vector, axis=1), axis=1) #unit vectors of the edges - - disp = ((vert - point) - (np.expand_dims(np.sum((vert-point)*edge_unit, axis=1),axis=1) *edge_unit)) #displacements + """ + edge_unit = edge_vector / np.expand_dims( + LA.norm(edge_vector, axis=1), axis=1 + ) # unit vectors of the edges + + disp = (vert - point) - ( + np.expand_dims(np.sum((vert - point) * edge_unit, axis=1), axis=1) * edge_unit + ) # displacements return disp -def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: - ''' + +def point_to_face_distance( + point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray +) -> np.ndarray: + """ Calculates the distances between several points and several varying planes. - n is the total number of distance calculations that are being made. For example, let's say + n is the total number of distance calculations that are being made. For example, let's say we have points: A, B, C & D, and faces: P, Q & R, and we want to calculate the distances between: - A & P, A & R, B & P, C & Q, C & R, D & P, D & Q, and D & R n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [P,R,P,Q,R,P,Q.R] @@ -117,21 +173,29 @@ def point_to_face_distance(point: np.ndarray, vert: np.ndarray, face_normal: np. vert (np.ndarray): points that lie on each corresponding plane [shape = (n, 3)] face_normal (np.ndarray): the normals that describe each plane [shape = (n, 3)] - Returns: + Returns + ------- np.ndarray: distances [shape = (n,)] - ''' - vert_point_vect = vert - point #displacements between the points and relevent vertices - face_unit = face_normal / np.expand_dims(LA.norm(face_normal, axis=1), axis=1) #unit vectors of the normals of the faces + """ + vert_point_vect = ( + vert - point + ) # displacements between the points and relevent vertices + face_unit = face_normal / np.expand_dims( + LA.norm(face_normal, axis=1), axis=1 + ) # unit vectors of the normals of the faces - dist = np.sum(vert_point_vect*face_unit, axis=1) * (-1) #distances + dist = np.sum(vert_point_vect * face_unit, axis=1) * (-1) # distances return dist -def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray) -> np.ndarray: - ''' + +def point_to_face_displacement( + point: np.ndarray, vert: np.ndarray, face_normal: np.ndarray +) -> np.ndarray: + """ Calculates the displacements between several points and several varying planes. - n is the total number of displacement calculations that are being made. For example, let's say + n is the total number of displacement calculations that are being made. For example, let's say we have points: A, B, C & D, and faces: P, Q & R, and we want to calculate the displacements between: - A & P, A & R, B & P, C & Q, C & R, D & P, D & Q, and D & R n = 8 for this example, and point = [A,A,B,C,C,D,D,D] and edge_vector = [P,R,P,Q,R,P,Q.R] @@ -141,250 +205,332 @@ def point_to_face_displacement(point: np.ndarray, vert: np.ndarray, face_normal: vert (np.ndarray): points that lie on each corresponding plane [shape = (n, 3)] face_normal (np.ndarray): the normals that describe each plane [shape = (n, 3)] - Returns: + Returns + ------- np.ndarray: displacements [shape = (n, 3)] - ''' - vert_point_vect = vert - point #displacements between the points and relevent vertices - face_units = face_normal / np.expand_dims(LA.norm(face_normal, axis=1), axis=1) #unit vectors of the normals of the faces - - disp = np.expand_dims(np.sum(vert_point_vect*face_units, axis=1), axis=1) * face_units #*(-1) #displacements + """ + vert_point_vect = ( + vert - point + ) # displacements between the points and relevent vertices + face_units = face_normal / np.expand_dims( + LA.norm(face_normal, axis=1), axis=1 + ) # unit vectors of the normals of the faces + + disp = ( + np.expand_dims(np.sum(vert_point_vect * face_units, axis=1), axis=1) + * face_units + ) # *(-1) #displacements return disp -def get_vert_zones (shape): - ''' - Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones - where the shortest distance from any point that is within a vertex zone is the distance between the + +def get_vert_zones(shape): + """ + Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones + where the shortest distance from any point that is within a vertex zone is the distance between the point and the corresponding vertex. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- dict: "constraint": np.ndarray [shape = (n_verts, n_edges, 3)], "bounds": np.ndarray [shape = (n_verts, n_edges)] - ''' - #For a generalized shape, we cannot assume that every vertex has the same number of edges connected to it - #(EX:vertices in a cube have 3 connected edges each, and for an icosahedron, vertices have 5 conncected edges). - #This would result in a ragged list for the constraint and bounds, which is not ideal. - + """ + # For a generalized shape, we cannot assume that every vertex has the same number of edges connected to it + # (EX:vertices in a cube have 3 connected edges each, and for an icosahedron, vertices have 5 conncected edges). + # This would result in a ragged list for the constraint and bounds, which is not ideal. - - #v--- This `for`` loop is used to build and fill that ragged list with zeros, so that it makes an easy to work with array. ---v + # v--- This `for`` loop is used to build and fill that ragged list with zeros, so that it makes an easy to work with array. ---v for v_i in range(len(shape.vertices)): - pos_adj_edges = shape.edge_vectors[shape.edges[:,0] == v_i] #edges that point away from v_i - neg_adj_edges = (-1)*shape.edge_vectors[shape.edges[:,1] == v_i] #edges that point towards v_i, so have to multiply by -1 + pos_adj_edges = shape.edge_vectors[ + shape.edges[:, 0] == v_i + ] # edges that point away from v_i + neg_adj_edges = (-1) * shape.edge_vectors[ + shape.edges[:, 1] == v_i + ] # edges that point towards v_i, so have to multiply by -1 adjacent_edges = np.append(pos_adj_edges, neg_adj_edges, axis=0) - if v_i == 0: #initial vertex, start of building the constraint + if v_i == 0: # initial vertex, start of building the constraint vert_constraint = np.asarray([adjacent_edges]) - - else: - difference = len(adjacent_edges) - vert_constraint.shape[1] - #^---difference between the # of edges v_i has and the max # of edges from a previous vertex - - if difference < 0: #adjacent_edges needs to be filled with zeros to match the length of axis=1 for vert_constraint - adjacent_edges = np.append(adjacent_edges, np.zeros((abs(difference), 3)), axis=0) - - if difference > 0: #vert_constraint needs to be filled with zeros to match the # of edges for v_i - vert_constraint = np.append(vert_constraint, np.zeros((len(vert_constraint), difference, 3)), axis=1) - - vert_constraint = np.append(vert_constraint, np.asarray([adjacent_edges]), axis=0) - - vert_bounds = np.sum(vert_constraint * np.expand_dims(shape.vertices, axis=1), axis=2) - - return {"constraint":vert_constraint, "bounds":vert_bounds} -def get_edge_zones (shape): - ''' - Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones - where the shortest distance from any point that is within an edge zone is the distance between the + else: + difference = len(adjacent_edges) - vert_constraint.shape[1] + # ^---difference between the # of edges v_i has and the max # of edges from a previous vertex + + if ( + difference < 0 + ): # adjacent_edges needs to be filled with zeros to match the length of axis=1 for vert_constraint + adjacent_edges = np.append( + adjacent_edges, np.zeros((abs(difference), 3)), axis=0 + ) + + if ( + difference > 0 + ): # vert_constraint needs to be filled with zeros to match the # of edges for v_i + vert_constraint = np.append( + vert_constraint, + np.zeros((len(vert_constraint), difference, 3)), + axis=1, + ) + + vert_constraint = np.append( + vert_constraint, np.asarray([adjacent_edges]), axis=0 + ) + + vert_bounds = np.sum( + vert_constraint * np.expand_dims(shape.vertices, axis=1), axis=2 + ) + + return {"constraint": vert_constraint, "bounds": vert_bounds} + + +def get_edge_zones(shape): + """ + Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones + where the shortest distance from any point that is within an edge zone is the distance between the point and the corresponding edge. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- dict: "constraint": np.ndarray [shape = (n_edges, 4, 3)], "bounds": np.ndarray [shape = (n_edges, 4)] - ''' - #Set up + """ + # Set up edge_constraint = np.zeros((shape.num_edges, 4, 3)) edge_bounds = np.zeros((shape.num_edges, 4)) - #Calculating the normals of the plane boundaries - edge_constraint[:,0] = shape.edge_vectors - edge_constraint[:,1] = -1*shape.edge_vectors - edge_constraint[:,2] = np.cross(shape.edge_vectors, shape.normals[shape.edge_face_neighbors[:,1]]) - edge_constraint[:,3] = -1*np.cross(shape.edge_vectors, shape.normals[shape.edge_face_neighbors[:,0]]) - #Constraint shape = (n_edges, 4, 3) - - #Bounds [shape = (n_edges, 4)] + # Calculating the normals of the plane boundaries + edge_constraint[:, 0] = shape.edge_vectors + edge_constraint[:, 1] = -1 * shape.edge_vectors + edge_constraint[:, 2] = np.cross( + shape.edge_vectors, shape.normals[shape.edge_face_neighbors[:, 1]] + ) + edge_constraint[:, 3] = -1 * np.cross( + shape.edge_vectors, shape.normals[shape.edge_face_neighbors[:, 0]] + ) + # Constraint shape = (n_edges, 4, 3) + + # Bounds [shape = (n_edges, 4)] edge_verts = np.zeros((shape.num_edges, 2, 3)) - edge_verts[:,0] = shape.vertices[shape.edges[:,0]] - edge_verts[:,1] = shape.vertices[shape.edges[:,1]] + edge_verts[:, 0] = shape.vertices[shape.edges[:, 0]] + edge_verts[:, 1] = shape.vertices[shape.edges[:, 1]] + + edge_bounds[:, 0] = np.sum(edge_constraint[:, 0] * (edge_verts[:, 1]), axis=1) + edge_bounds[:, 1] = np.sum(edge_constraint[:, 1] * (edge_verts[:, 0]), axis=1) + edge_bounds[:, 2] = np.sum(edge_constraint[:, 2] * (edge_verts[:, 0]), axis=1) + edge_bounds[:, 3] = np.sum(edge_constraint[:, 3] * (edge_verts[:, 0]), axis=1) - edge_bounds[:,0] = np.sum(edge_constraint[:,0] *(edge_verts[:,1]), axis=1) - edge_bounds[:,1] = np.sum(edge_constraint[:,1] *(edge_verts[:,0]), axis=1) - edge_bounds[:,2] = np.sum(edge_constraint[:,2] *(edge_verts[:,0]), axis=1) - edge_bounds[:,3] = np.sum(edge_constraint[:,3] *(edge_verts[:,0]), axis=1) + return {"constraint": edge_constraint, "bounds": edge_bounds} - return {"constraint":edge_constraint, "bounds":edge_bounds} -def get_face_zones (shape): - ''' - Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones - where the shortest distance from any point that is within a triangulated face zone is the distance between the +def get_face_zones(shape): + """ + Gets the constraints and bounds needed to partition the volume surrounding a polyhedron into zones + where the shortest distance from any point that is within a triangulated face zone is the distance between the point and the corresponding triangulated face. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- dict: "constraint": np.ndarray [shape = (n_tri_faces, 3, 3)], "bounds": np.ndarray [shape = (n_tri_faces, 3)], "face_points": np.ndarray [shape= (n_tri_faces, 3)], "normals": np.ndarray [shape=(n_tri_faces, 3)] - ''' - #----- Triangulating the surface of the shape ----- + """ + # ----- Triangulating the surface of the shape ----- try: - #checking to see if faces are already triangulated - something = np.asarray(shape.faces).reshape(shape.num_faces,3) + # checking to see if faces are already triangulated + something = np.asarray(shape.faces).reshape(shape.num_faces, 3) except: - #triangulating faces + # triangulating faces triangle_verts = [] for triangle in shape._surface_triangulation(): triangle_verts.append(list(triangle)) - triangle_verts = np.asarray(triangle_verts) #vertices of the triangulated faces - tri_edges = np.append(triangle_verts[:,1:], triangle_verts[:,0].reshape(len(triangle_verts),1,3), axis=1) - triangle_verts #edges point counterclockwise - tri_face_normals = np.cross(tri_edges[:,0], tri_edges[:,1]) #normals of the triangulated faces + triangle_verts = np.asarray( + triangle_verts + ) # vertices of the triangulated faces + tri_edges = ( + np.append( + triangle_verts[:, 1:], + triangle_verts[:, 0].reshape(len(triangle_verts), 1, 3), + axis=1, + ) + - triangle_verts + ) # edges point counterclockwise + tri_face_normals = np.cross( + tri_edges[:, 0], tri_edges[:, 1] + ) # normals of the triangulated faces else: - triangle_verts = shape.vertices[shape.faces] #vertices of the triangulated faces - tri_edges = np.append(triangle_verts[:,1:], triangle_verts[:,0].reshape(len(triangle_verts),1,3), axis=1) - triangle_verts #edges point counterclockwise - tri_face_normals = shape.normals #normals of the triangulated faces - - - face_constraint = np.cross(tri_edges, np.expand_dims(tri_face_normals, axis=1)) #shape = (n_tri_faces, 3, 3) - face_bounds = np.sum(face_constraint*triangle_verts, axis=2) #shape = (n_tri_faces, 3) + triangle_verts = shape.vertices[ + shape.faces + ] # vertices of the triangulated faces + tri_edges = ( + np.append( + triangle_verts[:, 1:], + triangle_verts[:, 0].reshape(len(triangle_verts), 1, 3), + axis=1, + ) + - triangle_verts + ) # edges point counterclockwise + tri_face_normals = shape.normals # normals of the triangulated faces + + face_constraint = np.cross( + tri_edges, np.expand_dims(tri_face_normals, axis=1) + ) # shape = (n_tri_faces, 3, 3) + face_bounds = np.sum( + face_constraint * triangle_verts, axis=2 + ) # shape = (n_tri_faces, 3) + + face_one_vertex = triangle_verts[ + :, 0 + ] # a point (vertex) that lies on each of the planes of the triangulated faces + + return { + "constraint": face_constraint, + "bounds": face_bounds, + "face_points": face_one_vertex, + "normals": tri_face_normals, + } - face_one_vertex = triangle_verts[:,0] #a point (vertex) that lies on each of the planes of the triangulated faces - - return {"constraint":face_constraint, "bounds":face_bounds, "face_points":face_one_vertex, "normals": tri_face_normals} def get_edge_normals(shape) -> np.ndarray: - ''' - Gets the analogous normals of the edges of the polyhedron. The normals point outwards from the polyhedron + """ + Gets the analogous normals of the edges of the polyhedron. The normals point outwards from the polyhedron and are used to determine whether an edge zone is outside or inside the polyhedron. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- np.ndarray: analogous edge normals [shape = (n_edges, 3)] - ''' - face_unit = shape.normals / np.expand_dims(LA.norm(shape.normals, axis=1), axis=1) #unit vectors of the face normals - face_unit1 = face_unit[shape.edge_face_neighbors[:,0]] - face_unit2 = face_unit[shape.edge_face_neighbors[:,1]] - - edge_normals = face_unit1 + face_unit2 #sum of the adjacent face normals for each edge - - #returning the unit vectors of the edge normals + """ + face_unit = shape.normals / np.expand_dims( + LA.norm(shape.normals, axis=1), axis=1 + ) # unit vectors of the face normals + face_unit1 = face_unit[shape.edge_face_neighbors[:, 0]] + face_unit2 = face_unit[shape.edge_face_neighbors[:, 1]] + + edge_normals = ( + face_unit1 + face_unit2 + ) # sum of the adjacent face normals for each edge + + # returning the unit vectors of the edge normals return edge_normals / np.expand_dims(LA.norm(edge_normals, axis=1), axis=1) + def get_vert_normals(shape) -> np.ndarray: - ''' - Gets the analogous normals of the vertices of the polyhedron. The normals point outwards from the polyhedron + """ + Gets the analogous normals of the vertices of the polyhedron. The normals point outwards from the polyhedron and are used to determine whether a vertex zone is outside or inside the polyhedron. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- np.ndarray: analogous vertex normals [shape = (n_verts, 3)] - ''' + """ n_edges = len(shape.edge_normals) - n_verts = np.max(shape.edges) +1 + n_verts = np.max(shape.edges) + 1 - #Tiling for set up - nverts_edge_vert0 = np.tile(shape.edges[:,0], (n_verts, 1)) - nverts_edge_vert1 = np.tile(shape.edges[:,1], (n_verts, 1)) + # Tiling for set up + nverts_edge_vert0 = np.tile(shape.edges[:, 0], (n_verts, 1)) + nverts_edge_vert1 = np.tile(shape.edges[:, 1], (n_verts, 1)) vert_inds = np.arange(0, n_verts, 1).reshape((n_verts, 1)) - nverts_tile_edges = np.tile(shape.edge_normals, (n_verts, 1)).reshape((n_verts, n_edges, 3)) + nverts_tile_edges = np.tile(shape.edge_normals, (n_verts, 1)).reshape( + (n_verts, n_edges, 3) + ) - #Creating the bools needed to get the edges that correspond to each vertex + # Creating the bools needed to get the edges that correspond to each vertex evbool0 = (np.expand_dims(nverts_edge_vert0 == vert_inds, axis=2)).astype(int) evbool1 = (np.expand_dims(nverts_edge_vert1 == vert_inds, axis=2)).astype(int) - #Applying the bools to find the corresponding edges + # Applying the bools to find the corresponding edges vert_edges = nverts_tile_edges * evbool0 + nverts_tile_edges * evbool1 - vert_normals = np.sum(vert_edges, axis=1) #sum of the adjacent edge normals for each vertex + vert_normals = np.sum( + vert_edges, axis=1 + ) # sum of the adjacent edge normals for each vertex - #returning the unit vectors of the vertex normals + # returning the unit vectors of the vertex normals return vert_normals / np.expand_dims(LA.norm(vert_normals, axis=1), axis=1) def get_weighted_edge_normals(shape) -> np.ndarray: - ''' + """ Gets the weighted normals of the edges of the polyhedron. The normals point outwards from the polyhedron. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- np.ndarray: analogous edge normals [shape = (n_edges, 3)] - ''' - face_1 = shape.normals[shape.edge_face_neighbors[:,0]] - face_2 = shape.normals[shape.edge_face_neighbors[:,1]] + """ + face_1 = shape.normals[shape.edge_face_neighbors[:, 0]] + face_2 = shape.normals[shape.edge_face_neighbors[:, 1]] - edge_normals = face_1 + face_2 #sum of the adjacent face normals for each edge + edge_normals = face_1 + face_2 # sum of the adjacent face normals for each edge + + return edge_normals - return edge_normals def get_weighted_vert_normals(shape) -> np.ndarray: - ''' + """ Gets the weighted normals of the vertices of the polyhedron. The normals point outwards from the polyhedron. Args: shape (Polyhedron): the polyhedron that is being looked at (can be convex or concave) - Returns: + Returns + ------- np.ndarray: analogous vertex normals [shape = (n_verts, 3)] - ''' + """ n_edges = len(shape.edge_normals) - n_verts = np.max(shape.edges) +1 + n_verts = np.max(shape.edges) + 1 - #Tiling for set up - nverts_edge_vert0 = np.tile(shape.edges[:,0], (n_verts, 1)) - nverts_edge_vert1 = np.tile(shape.edges[:,1], (n_verts, 1)) + # Tiling for set up + nverts_edge_vert0 = np.tile(shape.edges[:, 0], (n_verts, 1)) + nverts_edge_vert1 = np.tile(shape.edges[:, 1], (n_verts, 1)) vert_inds = np.arange(0, n_verts, 1).reshape((n_verts, 1)) - nverts_tile_edges = np.tile(shape.weighted_edge_normals, (n_verts, 1)).reshape((n_verts, n_edges, 3)) + nverts_tile_edges = np.tile(shape.weighted_edge_normals, (n_verts, 1)).reshape( + (n_verts, n_edges, 3) + ) - #Creating the bools needed to get the edges that correspond to each vertex + # Creating the bools needed to get the edges that correspond to each vertex evbool0 = (np.expand_dims(nverts_edge_vert0 == vert_inds, axis=2)).astype(int) evbool1 = (np.expand_dims(nverts_edge_vert1 == vert_inds, axis=2)).astype(int) - #Applying the bools to find the corresponding edges + # Applying the bools to find the corresponding edges vert_edges = nverts_tile_edges * evbool0 + nverts_tile_edges * evbool1 - vert_normals = np.sum(vert_edges, axis=1) #sum of the adjacent weighted edge normals for each vertex + vert_normals = np.sum( + vert_edges, axis=1 + ) # sum of the adjacent weighted edge normals for each vertex return vert_normals - -def shortest_distance_to_surface ( - shp, - points: np.ndarray, - translation_vector: np.ndarray, +def shortest_distance_to_surface( + shp, + points: np.ndarray, + translation_vector: np.ndarray, ) -> np.ndarray: - ''' - Solves for the shortest distance between points and the surface of a polyhedron. + """ + Solves for the shortest distance between points and the surface of a polyhedron. If the point lies inside the polyhedron, the distance is negative. - This function calculates the shortest distance by partitioning the space around - a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + This function calculates the shortest distance by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a point lies in, determines the distance calculation(s) done. For a vertex zone, - the distance is calculated between a point and the vertex. For an edge zone, the + the distance is calculated between a point and the vertex. For an edge zone, the distance is calculated between a point and the edge. For a face zone, the distance is calculated between a point and the face. Zones are allowed to overlap, and points can be in more than one zone. By taking the minimum of all the calculated distances, @@ -394,109 +540,186 @@ def shortest_distance_to_surface ( points (list or np.ndarray): positions of the points [shape = (n_points, 3)] translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,)] - Returns: + Returns + ------- np.ndarray: shortest distances [shape = (n_points,)] - ''' + """ points = np.asarray(points) translation_vector = np.asarray(translation_vector) - if translation_vector.shape[0]!=3 or len(translation_vector.shape)>1: - raise ValueError(f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}") + if translation_vector.shape[0] != 3 or len(translation_vector.shape) > 1: + raise ValueError( + f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}" + ) if points.shape == (3,): points = points.reshape(1, 3) atol = 1e-8 - n_points = len(points) #number of inputted points - n_verts = len(shp.vertices) #number of vertices = number of vertex zones - n_edges = len(shp.edges) #number of edges = number of edge zones - n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones - - #arrays consisting of 1 or -1, and used to determine if a point is inside the polyhedron - vert_inside_mult = np.diag(np.all((shp.vertex_zones["constraint"] @ np.transpose(shp.vertex_normals+shp.vertices)) <= np.expand_dims(shp.vertex_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 - edge_inside_mult = np.diag(np.all((shp.edge_zones["constraint"] @ np.transpose(shp.edge_normals+0.5*(shp.vertices[shp.edges[:,0]]+shp.vertices[shp.edges[:,1]]))) <= np.expand_dims(shp.edge_zones["bounds"], axis=2), axis=1)).astype(int)*2 -1 - - - #Updating bounds with the position of the polyhedron - vert_bounds = shp.vertex_zones["bounds"] + (shp.vertex_zones["constraint"] @ translation_vector) - edge_bounds = shp.edge_zones["bounds"] + (shp.edge_zones["constraint"] @ translation_vector) - face_bounds = shp.face_zones["bounds"] + (shp.face_zones["constraint"] @ translation_vector) - - points_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ points_trans' returns the right shape and values - max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - - #Calculating the distances + n_points = len(points) # number of inputted points + n_verts = len(shp.vertices) # number of vertices = number of vertex zones + n_edges = len(shp.edges) # number of edges = number of edge zones + n_tri_faces = len( + shp.face_zones["bounds"] + ) # number of triangulated faces = number of triangulated face zones + + # arrays consisting of 1 or -1, and used to determine if a point is inside the polyhedron + vert_inside_mult = ( + np.diag( + np.all( + ( + shp.vertex_zones["constraint"] + @ np.transpose(shp.vertex_normals + shp.vertices) + ) + <= np.expand_dims(shp.vertex_zones["bounds"], axis=2), + axis=1, + ) + ).astype(int) + * 2 + - 1 + ) + edge_inside_mult = ( + np.diag( + np.all( + ( + shp.edge_zones["constraint"] + @ np.transpose( + shp.edge_normals + + 0.5 + * ( + shp.vertices[shp.edges[:, 0]] + + shp.vertices[shp.edges[:, 1]] + ) + ) + ) + <= np.expand_dims(shp.edge_zones["bounds"], axis=2), + axis=1, + ) + ).astype(int) + * 2 + - 1 + ) + + # Updating bounds with the position of the polyhedron + vert_bounds = shp.vertex_zones["bounds"] + ( + shp.vertex_zones["constraint"] @ translation_vector + ) + edge_bounds = shp.edge_zones["bounds"] + ( + shp.edge_zones["constraint"] @ translation_vector + ) + face_bounds = shp.face_zones["bounds"] + ( + shp.face_zones["constraint"] @ translation_vector + ) + + points_trans = np.transpose( + points + ) # Have to take the transpose so that 'constraint @ points_trans' returns the right shape and values + max_value = ( + 3 * np.max(LA.norm(points - (translation_vector + shp.vertices[0]), axis=1)) + ) # Placeholder value, it is large so that it is not chosen when taking the min of the distances + + # Calculating the distances # Solving for the distances between the points and any relevant vertices - vert_dist=LA.norm(np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1) - np.expand_dims(shp.vertices + translation_vector, axis=0), axis=2)*np.expand_dims(vert_inside_mult, axis=0) #Distances between two points + vert_dist = LA.norm( + np.repeat(np.expand_dims(points, axis=1), n_verts, axis=1) + - np.expand_dims(shp.vertices + translation_vector, axis=0), + axis=2, + ) * np.expand_dims(vert_inside_mult, axis=0) # Distances between two points - #Taking the minimum of the distances for each point + # Taking the minimum of the distances for each point vert_dist_arg = np.expand_dims(np.argmin(abs(vert_dist), axis=1), axis=1) min_dist_arr = np.take_along_axis(vert_dist, vert_dist_arg, axis=1) - #Solving for the distances between the points and any relevant edges - edge_bool = np.all((shp.edge_zones["constraint"] @ points_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) + # Solving for the distances between the points and any relevant edges + edge_bool = np.all( + (shp.edge_zones["constraint"] @ points_trans) + <= (np.expand_dims(edge_bounds, axis=2) + atol), + axis=1, + ) # <--- shape = (n_edges, n_points) # edge_bool = edge_bool + np.allclose((shp.edge_zones["constraint"] @ points_trans), np.expand_dims(edge_bounds, axis=2), atol=1e-6) if np.any(edge_bool): - - #v--- shape = (number of True in edge_bool,) ---v - edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool - e_points_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool - - vert_on_edge = shp.vertices[shp.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges - - #Calculating the distances - edge_dist = np.ones((n_edges,n_points))*max_value - edge_dist[edge_bool]=point_to_edge_distance(points[e_points_used], vert_on_edge, shp.edge_vectors[edge_used])*edge_inside_mult[edge_used] #Distances between a point and a line - edge_dist = np.transpose(edge_dist) #<--- shape = (n_points, n_edges) - - #Taking the minimum of the distances for each point + # v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0, n_edges, 1), (n_points, 1)))[ + edge_bool + ] # Contains the indices of the edges that hold True for edge_bool + e_points_used = np.tile(np.arange(0, n_points, 1), (n_edges, 1))[ + edge_bool + ] # Contains the indices of the points that hold True for edge_bool + + vert_on_edge = ( + shp.vertices[shp.edges[edge_used][:, 0]] + translation_vector + ) # Vertices that lie on the needed edges + + # Calculating the distances + edge_dist = np.ones((n_edges, n_points)) * max_value + edge_dist[edge_bool] = ( + point_to_edge_distance( + points[e_points_used], vert_on_edge, shp.edge_vectors[edge_used] + ) + * edge_inside_mult[edge_used] + ) # Distances between a point and a line + edge_dist = np.transpose(edge_dist) # <--- shape = (n_points, n_edges) + + # Taking the minimum of the distances for each point edge_dist_arg = np.expand_dims(np.argmin(abs(edge_dist), axis=1), axis=1) edge_dist = np.take_along_axis(edge_dist, edge_dist_arg, axis=1) min_dist_arr = np.concatenate((min_dist_arr, edge_dist), axis=1) - #Solving for the distances between the points and any relevant faces - face_bool = np.all((shp.face_zones["constraint"] @ points_trans) <= (np.expand_dims(face_bounds, axis=2)+atol), axis=1) #<--- shape = (n_tri_faces, n_points) + # Solving for the distances between the points and any relevant faces + face_bool = np.all( + (shp.face_zones["constraint"] @ points_trans) + <= (np.expand_dims(face_bounds, axis=2) + atol), + axis=1, + ) # <--- shape = (n_tri_faces, n_points) # face_bool = face_bool + np.allclose((shp.face_zones["constraint"] @ points_trans), np.expand_dims(face_bounds, axis=2), atol=1e-6) if np.any(face_bool): - - #v--- shape = (number of True in face_bool,) ---v - face_used = np.transpose(np.tile(np.arange(0,n_tri_faces,1), (n_points,1)))[face_bool] #Contains the indices of the triangulated faces that hold True for face_bool - f_points_used = np.tile(np.arange(0,n_points,1), (n_tri_faces,1))[face_bool] #Contains the indices of the points that hold True for face_bool - - vert_on_face = (shp.face_zones["face_points"][face_used]) + translation_vector #Vertices that lie on the needed faces - - #Calculating the distances - face_dist = np.ones((n_tri_faces,n_points))*max_value - face_dist[face_bool]=point_to_face_distance(points[f_points_used], vert_on_face, shp.face_zones["normals"][face_used]) #Distances between a point and a plane - face_dist = np.transpose(face_dist) #<--- shape = (n_points, n_tri_faces) - - #Taking the minimum of the distances for each point + # v--- shape = (number of True in face_bool,) ---v + face_used = np.transpose(np.tile(np.arange(0, n_tri_faces, 1), (n_points, 1)))[ + face_bool + ] # Contains the indices of the triangulated faces that hold True for face_bool + f_points_used = np.tile(np.arange(0, n_points, 1), (n_tri_faces, 1))[ + face_bool + ] # Contains the indices of the points that hold True for face_bool + + vert_on_face = ( + (shp.face_zones["face_points"][face_used]) + translation_vector + ) # Vertices that lie on the needed faces + + # Calculating the distances + face_dist = np.ones((n_tri_faces, n_points)) * max_value + face_dist[face_bool] = point_to_face_distance( + points[f_points_used], vert_on_face, shp.face_zones["normals"][face_used] + ) # Distances between a point and a plane + face_dist = np.transpose(face_dist) # <--- shape = (n_points, n_tri_faces) + + # Taking the minimum of the distances for each point face_dist_arg = np.expand_dims(np.argmin(abs(face_dist), axis=1), axis=1) face_dist = np.take_along_axis(face_dist, face_dist_arg, axis=1) min_dist_arr = np.concatenate((min_dist_arr, face_dist), axis=1) - min_dist_arg = np.expand_dims(np.argmin(abs(min_dist_arr), axis=1), axis=1) #determining the distances that are the shortest + min_dist_arg = np.expand_dims( + np.argmin(abs(min_dist_arr), axis=1), axis=1 + ) # determining the distances that are the shortest true_min_dist = np.take_along_axis(min_dist_arr, min_dist_arg, axis=1).flatten() return true_min_dist -def shortest_displacement_to_surface ( - shp, - points: np.ndarray, - translation_vector: np.ndarray + +def shortest_displacement_to_surface( + shp, points: np.ndarray, translation_vector: np.ndarray ) -> np.ndarray: - ''' + """ Solves for the shortest displacement between points and the surface of a polyhedron. - This function calculates the shortest displacement by partitioning the space around - a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a point lies in, determines the displacement calculation(s) done. For a vertex zone, - the displacement is calculated between a point and the vertex. For an edge zone, the - displacement is calculated between a point and the edge. For a face zone, the - displacement is calculated between a point and the face. Zones are allowed to overlap, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, and points can be in more than one zone. By taking the minimum of all the distances of the calculated displacements, the shortest displacements are found. @@ -504,103 +727,156 @@ def shortest_displacement_to_surface ( points (list or np.ndarray): positions of the points [shape = (n_points, 3)] translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,)] - Returns: + Returns + ------- np.ndarray: shortest displacements [shape = (n_points, 3)] - ''' + """ points = np.asarray(points) translation_vector = np.asarray(translation_vector) - if translation_vector.shape[0]!=3 or len(translation_vector.shape)>1: - raise ValueError(f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}") + if translation_vector.shape[0] != 3 or len(translation_vector.shape) > 1: + raise ValueError( + f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}" + ) if points.shape == (3,): points = points.reshape(1, 3) atol = 1e-8 - n_points = len(points) #number of inputted points - n_verts = len(shp.vertices) #number of vertices = number of vertex zones - n_edges = len(shp.edges) #number of edges = number of edge zones - n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones - - #Updating bounds with the position of the polyhedron - vert_bounds = shp.vertex_zones["bounds"] + (shp.vertex_zones["constraint"] @ translation_vector) - edge_bounds = shp.edge_zones["bounds"] + (shp.edge_zones["constraint"] @ translation_vector) - face_bounds = shp.face_zones["bounds"] + (shp.face_zones["constraint"] @ translation_vector) - - coord_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values - max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - - #Calculating the displacements - - #Solving for the displacements between the points and any relevant vertices - vert_disp=(-1*np.repeat(np.expand_dims(points, axis=1),n_verts, axis=1)) + np.expand_dims(shp.vertices + translation_vector, axis=0) #Displacements between two point - - #Taking the minimum of the displacements for each point - vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + n_points = len(points) # number of inputted points + n_verts = len(shp.vertices) # number of vertices = number of vertex zones + n_edges = len(shp.edges) # number of edges = number of edge zones + n_tri_faces = len( + shp.face_zones["bounds"] + ) # number of triangulated faces = number of triangulated face zones + + # Updating bounds with the position of the polyhedron + vert_bounds = shp.vertex_zones["bounds"] + ( + shp.vertex_zones["constraint"] @ translation_vector + ) + edge_bounds = shp.edge_zones["bounds"] + ( + shp.edge_zones["constraint"] @ translation_vector + ) + face_bounds = shp.face_zones["bounds"] + ( + shp.face_zones["constraint"] @ translation_vector + ) + + coord_trans = np.transpose( + points + ) # Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values + max_value = ( + 3 * np.max(LA.norm(points - (translation_vector + shp.vertices[0]), axis=1)) + ) # Placeholder value, it is large so that it is not chosen when taking the min of the distances + + # Calculating the displacements + + # Solving for the displacements between the points and any relevant vertices + vert_disp = ( + -1 * np.repeat(np.expand_dims(points, axis=1), n_verts, axis=1) + ) + np.expand_dims( + shp.vertices + translation_vector, axis=0 + ) # Displacements between two point + + # Taking the minimum of the displacements for each point + vert_disp_min = np.expand_dims( + np.argmin(LA.norm(vert_disp, axis=2), axis=1), axis=(1, 2) + ) min_disp_arr = np.take_along_axis(vert_disp, vert_disp_min, axis=1) - #Solving for the displacements between the points and any relevant edges - edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) + # Solving for the displacements between the points and any relevant edges + edge_bool = np.all( + (shp.edge_zones["constraint"] @ coord_trans) + <= (np.expand_dims(edge_bounds, axis=2) + atol), + axis=1, + ) # <--- shape = (n_edges, n_points) if np.any(edge_bool): - - #v--- shape = (number of True in edge_bool,) ---v - edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool - ecoords_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool - - vert_on_edge = shp.vertices[shp.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges - - #Calculating the displacements - edge_disp = np.ones((n_edges,n_points,3))*max_value - edge_disp[edge_bool]=point_to_edge_displacement(points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used]) #Displacements between a point and a line - edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) - - #Taking the minimum of the displacements for each point - edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + # v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0, n_edges, 1), (n_points, 1)))[ + edge_bool + ] # Contains the indices of the edges that hold True for edge_bool + ecoords_used = np.tile(np.arange(0, n_points, 1), (n_edges, 1))[ + edge_bool + ] # Contains the indices of the points that hold True for edge_bool + + vert_on_edge = ( + shp.vertices[shp.edges[edge_used][:, 0]] + translation_vector + ) # Vertices that lie on the needed edges + + # Calculating the displacements + edge_disp = np.ones((n_edges, n_points, 3)) * max_value + edge_disp[edge_bool] = point_to_edge_displacement( + points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used] + ) # Displacements between a point and a line + edge_disp = np.transpose( + edge_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_edges, 3) + + # Taking the minimum of the displacements for each point + edge_disp_arg = np.expand_dims( + np.argmin(LA.norm(edge_disp, axis=2), axis=1), axis=(1, 2) + ) edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) - #Solving for the displacements between the points and any relevant faces - face_bool = np.all((shp.face_zones["constraint"] @ coord_trans) <= (np.expand_dims(face_bounds, axis=2)+atol), axis=1) #<--- shape = (n_tri_faces, n_points) + # Solving for the displacements between the points and any relevant faces + face_bool = np.all( + (shp.face_zones["constraint"] @ coord_trans) + <= (np.expand_dims(face_bounds, axis=2) + atol), + axis=1, + ) # <--- shape = (n_tri_faces, n_points) if np.any(face_bool): - - #v--- shape = (number of True in face_bool,) ---v - face_used = np.transpose(np.tile(np.arange(0,n_tri_faces,1), (n_points,1)))[face_bool] #Contains the indices of the triangulated faces that hold True for face_bool - fcoords_used = np.tile(np.arange(0,n_points,1), (n_tri_faces,1))[face_bool] #Contains the indices of the points that hold True for face_bool - - vert_on_face = (shp.face_zones["face_points"][face_used]) + translation_vector #Vertices that lie on the needed faces - - #Calculating the displacements - face_disp = np.ones((n_tri_faces,n_points,3))*max_value - face_disp[face_bool]=point_to_face_displacement(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) #Displacements between a point and a plane - face_disp = np.transpose(face_disp, (1, 0, 2)) #<--- shape = (n_points, n_tri_faces, 3) - - #Taking the minimum of the displacements for each point - face_disp_arg = np.expand_dims(np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1,2)) + # v--- shape = (number of True in face_bool,) ---v + face_used = np.transpose(np.tile(np.arange(0, n_tri_faces, 1), (n_points, 1)))[ + face_bool + ] # Contains the indices of the triangulated faces that hold True for face_bool + fcoords_used = np.tile(np.arange(0, n_points, 1), (n_tri_faces, 1))[ + face_bool + ] # Contains the indices of the points that hold True for face_bool + + vert_on_face = ( + (shp.face_zones["face_points"][face_used]) + translation_vector + ) # Vertices that lie on the needed faces + + # Calculating the displacements + face_disp = np.ones((n_tri_faces, n_points, 3)) * max_value + face_disp[face_bool] = point_to_face_displacement( + points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used] + ) # Displacements between a point and a plane + face_disp = np.transpose( + face_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_tri_faces, 3) + + # Taking the minimum of the displacements for each point + face_disp_arg = np.expand_dims( + np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1, 2) + ) face_disp = np.take_along_axis(face_disp, face_disp_arg, axis=1) min_disp_arr = np.concatenate((min_disp_arr, face_disp), axis=1) - disp_arr_bool = np.expand_dims(np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1), axis=(1,2)) #determining the displacements that are shortest - true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_arr_bool, axis=1), axis=1) + disp_arr_bool = np.expand_dims( + np.argmin((LA.norm(min_disp_arr, axis=2)), axis=1), axis=(1, 2) + ) # determining the displacements that are shortest + true_min_disp = np.squeeze( + np.take_along_axis(min_disp_arr, disp_arr_bool, axis=1), axis=1 + ) return true_min_disp -def spheropolyhedron_shortest_displacement_to_surface ( - shp, - radius, - points: np.ndarray, - translation_vector: np.ndarray + +def spheropolyhedron_shortest_displacement_to_surface( + shp, radius, points: np.ndarray, translation_vector: np.ndarray ) -> np.ndarray: - ''' + """ Solves for the shortest displacement between points and the surface of a polyhedron. - This function calculates the shortest displacement by partitioning the space around - a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a + This function calculates the shortest displacement by partitioning the space around + a polyhedron into zones: vertex, edge, and face. Determining the zone(s) a point lies in, determines the displacement calculation(s) done. For a vertex zone, - the displacement is calculated between a point and the vertex. For an edge zone, the - displacement is calculated between a point and the edge. For a face zone, the - displacement is calculated between a point and the face. Zones are allowed to overlap, + the displacement is calculated between a point and the vertex. For an edge zone, the + displacement is calculated between a point and the edge. For a face zone, the + displacement is calculated between a point and the face. Zones are allowed to overlap, and points can be in more than one zone. By taking the minimum of all the distances of the calculated displacements, the shortest displacements are found. @@ -608,116 +884,244 @@ def spheropolyhedron_shortest_displacement_to_surface ( points (list or np.ndarray): positions of the points [shape = (n_points, 3)] translation_vector (list or np.ndarray): translation vector of the polyhedron [shape = (3,)] - Returns: + Returns + ------- np.ndarray: shortest displacements [shape = (n_points, 3)] - ''' + """ points = np.asarray(points) translation_vector = np.asarray(translation_vector) - if translation_vector.shape[0]!=3 or len(translation_vector.shape)>1: - raise ValueError(f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}") + if translation_vector.shape[0] != 3 or len(translation_vector.shape) > 1: + raise ValueError( + f"Expected the shape of the polygon's position to be (3,), instead it got {translation_vector.shape}" + ) if points.shape == (3,): points = points.reshape(1, 3) atol = 1e-8 - n_points = len(points) #number of inputted points - n_verts = len(shp.vertices) #number of vertices = number of vertex zones - n_edges = len(shp.edges) #number of edges = number of edge zones - n_tri_faces = len(shp.face_zones["bounds"]) #number of triangulated faces = number of triangulated face zones - - #Updating bounds with the position of the polyhedron - vert_bounds = shp.vertex_zones["bounds"] + (shp.vertex_zones["constraint"] @ translation_vector) - edge_bounds = shp.edge_zones["bounds"] + (shp.edge_zones["constraint"] @ translation_vector) - face_bounds = shp.face_zones["bounds"] + (shp.face_zones["constraint"] @ translation_vector) - - coord_trans = np.transpose(points) #Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values - max_value = 3*np.max(LA.norm(points - (translation_vector+shp.vertices[0]), axis=1)) #Placeholder value, it is large so that it is not chosen when taking the min of the distances - min_disp_arr = np.ones((n_points,1, 3))*max_value #Initial min_disp_arr - - #Calculating the displacements - - #Solving for the displacements between the points and any relevant vertices - vert_bool = np.all((shp.vertex_zones["constraint"] @ coord_trans) <= np.expand_dims(vert_bounds, axis=2), axis=1) #<--- shape = (n_verts, n_points) + n_points = len(points) # number of inputted points + n_verts = len(shp.vertices) # number of vertices = number of vertex zones + n_edges = len(shp.edges) # number of edges = number of edge zones + n_tri_faces = len( + shp.face_zones["bounds"] + ) # number of triangulated faces = number of triangulated face zones + + # Updating bounds with the position of the polyhedron + vert_bounds = shp.vertex_zones["bounds"] + ( + shp.vertex_zones["constraint"] @ translation_vector + ) + edge_bounds = shp.edge_zones["bounds"] + ( + shp.edge_zones["constraint"] @ translation_vector + ) + face_bounds = shp.face_zones["bounds"] + ( + shp.face_zones["constraint"] @ translation_vector + ) + + coord_trans = np.transpose( + points + ) # Have to take the transpose so that 'constraint @ coord_trans' returns the right shape and values + max_value = ( + 3 * np.max(LA.norm(points - (translation_vector + shp.vertices[0]), axis=1)) + ) # Placeholder value, it is large so that it is not chosen when taking the min of the distances + min_disp_arr = np.ones((n_points, 1, 3)) * max_value # Initial min_disp_arr + + # Calculating the displacements + + # Solving for the displacements between the points and any relevant vertices + vert_bool = np.all( + (shp.vertex_zones["constraint"] @ coord_trans) + <= np.expand_dims(vert_bounds, axis=2), + axis=1, + ) # <--- shape = (n_verts, n_points) if np.any(vert_bool): - - #v--- shape = (number of True in vert_bool,) ---v - vert_used = np.transpose(np.tile(np.arange(0,n_verts,1), (n_points,1)))[vert_bool] #Contains the indices of the vertices that hold True for vert_bool - vcoords_used = np.tile(np.arange(0,n_points,1), (n_verts,1))[vert_bool] #Contains the indices of the points that hold True for vert_bool - - #Calculating the displacements - vert_disp = np.ones((n_verts,n_points,3))*max_value - vert_disp[vert_bool]=(shp.vertices[vert_used] + translation_vector) - points[vcoords_used] #Displacements between two points - vert_disp = np.transpose(vert_disp, (1,0,2)) #<--- shape = (n_points, n_verts, 3) - - #TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal + # v--- shape = (number of True in vert_bool,) ---v + vert_used = np.transpose(np.tile(np.arange(0, n_verts, 1), (n_points, 1)))[ + vert_bool + ] # Contains the indices of the vertices that hold True for vert_bool + vcoords_used = np.tile(np.arange(0, n_points, 1), (n_verts, 1))[ + vert_bool + ] # Contains the indices of the points that hold True for vert_bool + + # Calculating the displacements + vert_disp = np.ones((n_verts, n_points, 3)) * max_value + vert_disp[vert_bool] = (shp.vertices[vert_used] + translation_vector) - points[ + vcoords_used + ] # Displacements between two points + vert_disp = np.transpose( + vert_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_verts, 3) + + # TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal vert_zero_disp_bool = np.all(vert_disp == 0, axis=2) - vert_disp[vert_zero_disp_bool] = vert_disp[vert_zero_disp_bool] + radius*(np.repeat(np.expand_dims(shp.vertex_normals,axis=0),n_points,axis=0)[vert_zero_disp_bool]) - vert_disp[np.invert(vert_zero_disp_bool)] = vert_disp[np.invert(vert_zero_disp_bool)] - radius*(vert_disp[np.invert(vert_zero_disp_bool)]/np.expand_dims(np.linalg.norm(vert_disp[np.invert(vert_zero_disp_bool)],axis=1),axis=1)) - - #Taking the minimum of the displacements for each point - vert_disp_min = np.expand_dims(np.argmin( LA.norm(vert_disp, axis=2), axis=1), axis=(1,2)) + vert_disp[vert_zero_disp_bool] = ( + vert_disp[vert_zero_disp_bool] + + radius + * ( + np.repeat(np.expand_dims(shp.vertex_normals, axis=0), n_points, axis=0)[ + vert_zero_disp_bool + ] + ) + ) + vert_disp[np.invert(vert_zero_disp_bool)] = vert_disp[ + np.invert(vert_zero_disp_bool) + ] - radius * ( + vert_disp[np.invert(vert_zero_disp_bool)] + / np.expand_dims( + np.linalg.norm(vert_disp[np.invert(vert_zero_disp_bool)], axis=1), + axis=1, + ) + ) + + # Taking the minimum of the displacements for each point + vert_disp_min = np.expand_dims( + np.argmin(LA.norm(vert_disp, axis=2), axis=1), axis=(1, 2) + ) vert_disp = np.take_along_axis(vert_disp, vert_disp_min, axis=1) min_disp_arr = np.concatenate((min_disp_arr, vert_disp), axis=1) - #Solving for the displacements between the points and any relevant edges - edge_bool = np.all((shp.edge_zones["constraint"] @ coord_trans) <= (np.expand_dims(edge_bounds, axis=2)+atol), axis=1) #<--- shape = (n_edges, n_points) + # Solving for the displacements between the points and any relevant edges + edge_bool = np.all( + (shp.edge_zones["constraint"] @ coord_trans) + <= (np.expand_dims(edge_bounds, axis=2) + atol), + axis=1, + ) # <--- shape = (n_edges, n_points) if np.any(edge_bool): - - #v--- shape = (number of True in edge_bool,) ---v - edge_used = np.transpose(np.tile(np.arange(0,n_edges,1), (n_points,1)))[edge_bool] #Contains the indices of the edges that hold True for edge_bool - ecoords_used = np.tile(np.arange(0,n_points,1), (n_edges,1))[edge_bool] #Contains the indices of the points that hold True for edge_bool - - vert_on_edge = shp.vertices[shp.edges[edge_used][:,0]] + translation_vector #Vertices that lie on the needed edges - - #Calculating the displacements - edge_disp = np.ones((n_edges,n_points,3))*max_value - edge_disp[edge_bool]=point_to_edge_displacement(points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used]) #Displacements between a point and a line - edge_disp = np.transpose(edge_disp, (1, 0, 2)) #<--- shape = (n_points, n_edges, 3) - - #TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal + # v--- shape = (number of True in edge_bool,) ---v + edge_used = np.transpose(np.tile(np.arange(0, n_edges, 1), (n_points, 1)))[ + edge_bool + ] # Contains the indices of the edges that hold True for edge_bool + ecoords_used = np.tile(np.arange(0, n_points, 1), (n_edges, 1))[ + edge_bool + ] # Contains the indices of the points that hold True for edge_bool + + vert_on_edge = ( + shp.vertices[shp.edges[edge_used][:, 0]] + translation_vector + ) # Vertices that lie on the needed edges + + # Calculating the displacements + edge_disp = np.ones((n_edges, n_points, 3)) * max_value + edge_disp[edge_bool] = point_to_edge_displacement( + points[ecoords_used], vert_on_edge, shp.edge_vectors[edge_used] + ) # Displacements between a point and a line + edge_disp = np.transpose( + edge_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_edges, 3) + + # TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal edge_zero_disp_bool = np.all(edge_disp == 0, axis=2) - edge_disp[edge_zero_disp_bool] = edge_disp[edge_zero_disp_bool] + radius*(np.repeat(np.expand_dims(shp.edge_normals,axis=0),n_points,axis=0)[edge_zero_disp_bool]) - edge_disp[np.invert(edge_zero_disp_bool)] = edge_disp[np.invert(edge_zero_disp_bool)] - radius*(edge_disp[np.invert(edge_zero_disp_bool)]/np.expand_dims(np.linalg.norm(edge_disp[np.invert(edge_zero_disp_bool)],axis=1),axis=1)) - - #Taking the minimum of the displacements for each point - edge_disp_arg = np.expand_dims(np.argmin( LA.norm(edge_disp, axis=2), axis=1), axis=(1,2)) + edge_disp[edge_zero_disp_bool] = ( + edge_disp[edge_zero_disp_bool] + + radius + * ( + np.repeat(np.expand_dims(shp.edge_normals, axis=0), n_points, axis=0)[ + edge_zero_disp_bool + ] + ) + ) + edge_disp[np.invert(edge_zero_disp_bool)] = edge_disp[ + np.invert(edge_zero_disp_bool) + ] - radius * ( + edge_disp[np.invert(edge_zero_disp_bool)] + / np.expand_dims( + np.linalg.norm(edge_disp[np.invert(edge_zero_disp_bool)], axis=1), + axis=1, + ) + ) + + # Taking the minimum of the displacements for each point + edge_disp_arg = np.expand_dims( + np.argmin(LA.norm(edge_disp, axis=2), axis=1), axis=(1, 2) + ) edge_disp = np.take_along_axis(edge_disp, edge_disp_arg, axis=1) min_disp_arr = np.concatenate((min_disp_arr, edge_disp), axis=1) - #Solving for the displacements between the points and any relevant faces - face_bool = np.all((shp.face_zones["constraint"] @ coord_trans) <= (np.expand_dims(face_bounds, axis=2)+atol), axis=1) #<--- shape = (n_tri_faces, n_points) + # Solving for the displacements between the points and any relevant faces + face_bool = np.all( + (shp.face_zones["constraint"] @ coord_trans) + <= (np.expand_dims(face_bounds, axis=2) + atol), + axis=1, + ) # <--- shape = (n_tri_faces, n_points) if np.any(face_bool): - - #v--- shape = (number of True in face_bool,) ---v - face_used = np.transpose(np.tile(np.arange(0,n_tri_faces,1), (n_points,1)))[face_bool] #Contains the indices of the triangulated faces that hold True for face_bool - fcoords_used = np.tile(np.arange(0,n_points,1), (n_tri_faces,1))[face_bool] #Contains the indices of the points that hold True for face_bool - - vert_on_face = (shp.face_zones["face_points"][face_used]) + translation_vector #Vertices that lie on the needed faces - - #Calculating the displacements - face_disp = np.ones((n_tri_faces,n_points,3))*max_value - face_disp[face_bool]=point_to_face_displacement(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) #Displacements between a point and a plane - - #TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal - #TODO: if point is inside, add radius*unit_displacement instead - point_inside = (-1)*np.ones((n_tri_faces, n_points)) - point_inside[face_bool] = (point_to_face_distance(points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used]) < 0).astype(int)*2 -1 #(+1) outside, (-1) inside + # v--- shape = (number of True in face_bool,) ---v + face_used = np.transpose(np.tile(np.arange(0, n_tri_faces, 1), (n_points, 1)))[ + face_bool + ] # Contains the indices of the triangulated faces that hold True for face_bool + fcoords_used = np.tile(np.arange(0, n_points, 1), (n_tri_faces, 1))[ + face_bool + ] # Contains the indices of the points that hold True for face_bool + + vert_on_face = ( + (shp.face_zones["face_points"][face_used]) + translation_vector + ) # Vertices that lie on the needed faces + + # Calculating the displacements + face_disp = np.ones((n_tri_faces, n_points, 3)) * max_value + face_disp[face_bool] = point_to_face_displacement( + points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used] + ) # Displacements between a point and a plane + + # TODO: subtract radius*unit_displacement -- unless displacement is zero, then subtract radius*vert_normal + # TODO: if point is inside, add radius*unit_displacement instead + point_inside = (-1) * np.ones((n_tri_faces, n_points)) + point_inside[face_bool] = ( + point_to_face_distance( + points[fcoords_used], vert_on_face, shp.face_zones["normals"][face_used] + ) + < 0 + ).astype(int) * 2 - 1 # (+1) outside, (-1) inside face_zero_disp_bool = np.all(face_disp == 0, axis=2) - face_disp[face_zero_disp_bool] = face_disp[face_zero_disp_bool] + radius*(np.repeat(np.expand_dims((shp.face_zones["normals"]/np.expand_dims(np.linalg.norm(shp.face_zones["normals"],axis=1),axis=1)),axis=1),n_points,axis=1)[face_zero_disp_bool]) - face_disp[np.invert(face_zero_disp_bool)] = face_disp[np.invert(face_zero_disp_bool)] + radius*np.expand_dims(point_inside[np.invert(face_zero_disp_bool)],axis=1)*(face_disp[np.invert(face_zero_disp_bool)]/np.expand_dims(np.linalg.norm(face_disp[np.invert(face_zero_disp_bool)],axis=1),axis=1)) - - face_disp = np.transpose(face_disp, (1, 0, 2)) #<--- shape = (n_points, n_tri_faces, 3) - #Taking the minimum of the displacements for each point - face_disp_arg = np.expand_dims(np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1,2)) + face_disp[face_zero_disp_bool] = ( + face_disp[face_zero_disp_bool] + + radius + * ( + np.repeat( + np.expand_dims( + ( + shp.face_zones["normals"] + / np.expand_dims( + np.linalg.norm(shp.face_zones["normals"], axis=1), + axis=1, + ) + ), + axis=1, + ), + n_points, + axis=1, + )[face_zero_disp_bool] + ) + ) + face_disp[np.invert(face_zero_disp_bool)] = face_disp[ + np.invert(face_zero_disp_bool) + ] + radius * np.expand_dims( + point_inside[np.invert(face_zero_disp_bool)], axis=1 + ) * ( + face_disp[np.invert(face_zero_disp_bool)] + / np.expand_dims( + np.linalg.norm(face_disp[np.invert(face_zero_disp_bool)], axis=1), + axis=1, + ) + ) + + face_disp = np.transpose( + face_disp, (1, 0, 2) + ) # <--- shape = (n_points, n_tri_faces, 3) + # Taking the minimum of the displacements for each point + face_disp_arg = np.expand_dims( + np.argmin(LA.norm(face_disp, axis=2), axis=1), axis=(1, 2) + ) face_disp = np.take_along_axis(face_disp, face_disp_arg, axis=1) min_disp_arr = np.concatenate((min_disp_arr, face_disp), axis=1) - disp_arr_bool = np.expand_dims(np.argmin( (LA.norm(min_disp_arr, axis=2)), axis=1), axis=(1,2)) #determining the displacements that are shortest - true_min_disp = np.squeeze(np.take_along_axis(min_disp_arr, disp_arr_bool, axis=1), axis=1) + disp_arr_bool = np.expand_dims( + np.argmin((LA.norm(min_disp_arr, axis=2)), axis=1), axis=(1, 2) + ) # determining the displacements that are shortest + true_min_disp = np.squeeze( + np.take_along_axis(min_disp_arr, disp_arr_bool, axis=1), axis=1 + ) return true_min_disp diff --git a/coxeter/shapes/convex_spheropolygon.py b/coxeter/shapes/convex_spheropolygon.py index aacd9838..fce0112e 100644 --- a/coxeter/shapes/convex_spheropolygon.py +++ b/coxeter/shapes/convex_spheropolygon.py @@ -9,18 +9,14 @@ import numpy as np +from ._distance2d import ( + spheropolygon_shortest_displacement_to_surface, +) from .base_classes import Shape2D from .convex_polygon import ConvexPolygon, _is_convex from .polygon import _align_points_by_normal from .utils import _hoomd_dict_mapping, _map_dict_keys -from ._distance2d import ( - get_vert_zones, - get_edge_zones, - get_face_zones, - spheropolygon_shortest_displacement_to_surface -) - class ConvexSpheropolygon(Shape2D): """A convex spheropolygon. @@ -63,7 +59,7 @@ def __init__(self, vertices, radius, normal=None): self._polygon = ConvexPolygon(vertices, normal) if not _is_convex(self.vertices, self._polygon.normal): raise ValueError("The vertices do not define a convex polygon.") - + self._vertex_zones = None self._edge_zones = None self._face_zones = None @@ -317,10 +313,11 @@ def to_hoomd(self): self._polygon.centroid = old_centroid return hoomd_dict - - def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + def shortest_distance_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ - Solves for the shortest distance (magnitude) between points and + Solves for the shortest distance (magnitude) between points and the surface of a polygon. This function calculates the shortest distance by partitioning @@ -347,9 +344,16 @@ def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0, the shortest distance of each point to the surface [shape = (n_points,)] """ - return np.linalg.norm(spheropolygon_shortest_displacement_to_surface(self._polygon, self.radius, points, translation_vector), axis=1) - - def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + return np.linalg.norm( + spheropolygon_shortest_displacement_to_surface( + self._polygon, self.radius, points, translation_vector + ), + axis=1, + ) + + def shortest_displacement_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ Solves for the shortest displacement (vector) between points and the surface of a polygon. @@ -378,4 +382,6 @@ def shortest_displacement_to_surface(self, points, translation_vector=np.array([ the shortest displacement of each point to the surface [shape = (n_points, 3)] """ - return spheropolygon_shortest_displacement_to_surface(self._polygon, self.radius, points, translation_vector) \ No newline at end of file + return spheropolygon_shortest_displacement_to_surface( + self._polygon, self.radius, points, translation_vector + ) diff --git a/coxeter/shapes/convex_spheropolyhedron.py b/coxeter/shapes/convex_spheropolyhedron.py index 776cfe85..fa6337cd 100644 --- a/coxeter/shapes/convex_spheropolyhedron.py +++ b/coxeter/shapes/convex_spheropolyhedron.py @@ -9,23 +9,13 @@ import numpy as np -from .base_classes import Shape3D -from .convex_polyhedron import ConvexPolyhedron -from .utils import _hoomd_dict_mapping, _map_dict_keys - from ._distance3d import ( - get_edge_face_neighbors, - get_vert_zones, - get_edge_zones, - get_face_zones, - get_vert_normals, - get_edge_normals, - get_weighted_vert_normals, - get_weighted_edge_normals, - shortest_displacement_to_surface, shortest_distance_to_surface, - spheropolyhedron_shortest_displacement_to_surface + spheropolyhedron_shortest_displacement_to_surface, ) +from .base_classes import Shape3D +from .convex_polyhedron import ConvexPolyhedron +from .utils import _hoomd_dict_mapping, _map_dict_keys class ConvexSpheropolyhedron(Shape3D): @@ -389,12 +379,12 @@ def to_hoomd(self): self._polyhedron.centroid = old_centroid return hoomd_dict - - - def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + def shortest_distance_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ - Solves for the shortest distance (magnitude) between points and - the surface of a spheropolyhedron. If the point lies inside the + Solves for the shortest distance (magnitude) between points and + the surface of a spheropolyhedron. If the point lies inside the spheropolyhedron, the distance is negative. This function calculates the shortest distance by partitioning @@ -421,9 +411,14 @@ def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0, the shortest distance of each point to the surface [shape = (n_points,)] """ - return shortest_distance_to_surface(self._polyhedron, points, translation_vector) - self.radius - - def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + return ( + shortest_distance_to_surface(self._polyhedron, points, translation_vector) + - self.radius + ) + + def shortest_displacement_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ Solves for the shortest displacement (vector) between points and the surface of a spheropolyhedron. @@ -452,6 +447,6 @@ def shortest_displacement_to_surface(self, points, translation_vector=np.array([ the shortest displacement of each point to the surface [shape = (n_points, 3)] """ - return spheropolyhedron_shortest_displacement_to_surface(self._polyhedron, self.radius, points, translation_vector) - - + return spheropolyhedron_shortest_displacement_to_surface( + self._polyhedron, self.radius, points, translation_vector + ) diff --git a/coxeter/shapes/polygon.py b/coxeter/shapes/polygon.py index 6635698e..9a1b834a 100644 --- a/coxeter/shapes/polygon.py +++ b/coxeter/shapes/polygon.py @@ -10,6 +10,13 @@ from ..extern.bentley_ottmann import poly_point_isect from ..extern.polytri import polytri +from ._distance2d import ( + get_edge_zones, + get_face_zones, + get_vert_zones, + shortest_displacement_to_surface, + shortest_distance_to_surface, +) from .base_classes import Shape2D from .circle import Circle from .utils import ( @@ -20,14 +27,6 @@ translate_inertia_tensor, ) -from ._distance2d import ( - get_vert_zones, - get_edge_zones, - get_face_zones, - shortest_displacement_to_surface, - shortest_distance_to_surface -) - try: import miniball @@ -434,11 +433,17 @@ def centroid(self, value): self._vertices += np.asarray(value) - self.centroid if self._vertex_zones is not None: - self._vertex_zones["bounds"] += self._vertex_zones["constraint"] @ (np.asarray(value) - self.centroid) + self._vertex_zones["bounds"] += self._vertex_zones["constraint"] @ ( + np.asarray(value) - self.centroid + ) if self._edge_zones is not None: - self._edge_zones["bounds"] += self._edge_zones["constraint"] @ (np.asarray(value) - self.centroid) + self._edge_zones["bounds"] += self._edge_zones["constraint"] @ ( + np.asarray(value) - self.centroid + ) if self._face_zones is not None: - self._face_zones["bounds"] += self._face_zones["constraint"] @ (np.asarray(value) - self.centroid) + self._face_zones["bounds"] += self._face_zones["constraint"] @ ( + np.asarray(value) - self.centroid + ) @property def edges(self): @@ -861,7 +866,6 @@ def to_hoomd(self): self.centroid = old_centroid return hoomd_dict - @property def vertex_zones(self): """dict: Get the constraints and bounds needed to partition the @@ -872,7 +876,7 @@ def vertex_zones(self): if self._vertex_zones is None: self._vertex_zones = get_vert_zones(self) return self._vertex_zones - + @property def edge_zones(self): """dict: Get the constraints and bounds needed to partition @@ -883,7 +887,7 @@ def edge_zones(self): if self._edge_zones is None: self._edge_zones = get_edge_zones(self) return self._edge_zones - + @property def face_zones(self): """dict: Get the constraints and bounds needed to partition @@ -894,10 +898,12 @@ def face_zones(self): if self._face_zones is None: self._face_zones = get_face_zones(self) return self._face_zones - - def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + + def shortest_distance_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ - Solves for the shortest distance (magnitude) between points and + Solves for the shortest distance (magnitude) between points and the surface of a polygon. This function calculates the shortest distance by partitioning @@ -925,8 +931,10 @@ def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0, [shape = (n_points,)] """ return shortest_distance_to_surface(self, points, translation_vector) - - def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + + def shortest_displacement_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ Solves for the shortest displacement (vector) between points and the surface of a polygon. @@ -955,4 +963,4 @@ def shortest_displacement_to_surface(self, points, translation_vector=np.array([ the shortest displacement of each point to the surface [shape = (n_points, 3)] """ - return shortest_displacement_to_surface(self, points, translation_vector) \ No newline at end of file + return shortest_displacement_to_surface(self, points, translation_vector) diff --git a/coxeter/shapes/polyhedron.py b/coxeter/shapes/polyhedron.py index 57a77e0d..e09ec4f9 100644 --- a/coxeter/shapes/polyhedron.py +++ b/coxeter/shapes/polyhedron.py @@ -12,6 +12,18 @@ from .. import io from ..extern.polytri import polytri +from ._distance3d import ( + get_edge_face_neighbors, + get_edge_normals, + get_edge_zones, + get_face_zones, + get_vert_normals, + get_vert_zones, + get_weighted_edge_normals, + get_weighted_vert_normals, + shortest_displacement_to_surface, + shortest_distance_to_surface, +) from .base_classes import Shape3D from .convex_polygon import ConvexPolygon, _is_convex from .polygon import Polygon, _is_simple @@ -24,19 +36,6 @@ translate_inertia_tensor, ) -from ._distance3d import ( - get_edge_face_neighbors, - get_vert_zones, - get_edge_zones, - get_face_zones, - get_vert_normals, - get_edge_normals, - get_weighted_edge_normals, - get_weighted_vert_normals, - shortest_displacement_to_surface, - shortest_distance_to_surface -) - try: import miniball @@ -614,11 +613,17 @@ def centroid(self, value): self._find_equations() if self._vertex_zones is not None: - self._vertex_zones["bounds"] += self._vertex_zones["constraint"] @ (np.asarray(value) - self.centroid) + self._vertex_zones["bounds"] += self._vertex_zones["constraint"] @ ( + np.asarray(value) - self.centroid + ) if self._edge_zones is not None: - self._edge_zones["bounds"] += self._edge_zones["constraint"] @ (np.asarray(value) - self.centroid) + self._edge_zones["bounds"] += self._edge_zones["constraint"] @ ( + np.asarray(value) - self.centroid + ) if self._face_zones is not None: - self._face_zones["bounds"] += self._face_zones["constraint"] @ (np.asarray(value) - self.centroid) + self._face_zones["bounds"] += self._face_zones["constraint"] @ ( + np.asarray(value) - self.centroid + ) @property def bounding_sphere(self): @@ -1087,34 +1092,32 @@ def save(self, filetype, filename): "STL, PLY, VTK, X3D, HTML" ) - - @property def edge_face_neighbors(self): """:class:`numpy.ndarray`: Get the indices of the faces that are adjacent to each edge. - + For a given edge vector oriented pointing upwards and from an outside perspective of the polyhedron, the index of the face to the left of the edge is given by the first column, - and the index of the face to the right of the edge is + and the index of the face to the right of the edge is given by the second column. """ if self._edge_face_neighbors is None: self._edge_face_neighbors = get_edge_face_neighbors(self) return self._edge_face_neighbors - + @property def vertex_zones(self): - """dict: Get the constraints and bounds needed to partition the - volume surrounding a polyhedron into zones where the shortest - distance from any point that is within a vertex zone is the + """dict: Get the constraints and bounds needed to partition the + volume surrounding a polyhedron into zones where the shortest + distance from any point that is within a vertex zone is the distance between the point and the corresponding vertex. """ if self._vertex_zones is None: self._vertex_zones = get_vert_zones(self) return self._vertex_zones - + @property def edge_zones(self): """dict: Get the constraints and bounds needed to partition @@ -1125,7 +1128,7 @@ def edge_zones(self): if self._edge_zones is None: self._edge_zones = get_edge_zones(self) return self._edge_zones - + @property def face_zones(self): """dict: Get the constraints and bounds needed to partition @@ -1137,31 +1140,31 @@ def face_zones(self): if self._face_zones is None: self._face_zones = get_face_zones(self) return self._face_zones - + @property def vertex_normals(self): """:class:`numpy.ndarray`: Get the unit vector normals of vertices. - + The normals point outwards from the polyhedron. """ if self._vertex_normals is None: self._vertex_normals = get_vert_normals(self) return self._vertex_normals - + @property def edge_normals(self): """:class:`numpy.ndarray`: Get the unit vector normals of edges. - + The normals point outwards from the polyhedron. """ if self._edge_normals is None: self._edge_normals = get_edge_normals(self) return self._edge_normals - + @property def weighted_vertex_normals(self): """:class:`numpy.ndarray`: Get the weighted normals of vertices. - + The normals point outwards from the polyhedron. """ return get_weighted_vert_normals(self) @@ -1169,16 +1172,17 @@ def weighted_vertex_normals(self): @property def weighted_edge_normals(self): """:class:`numpy.ndarray`: Get the weighted normals of edges. - + The normals point outwards from the polyhedron. """ return get_weighted_edge_normals(self) - - def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0,0])): + def shortest_distance_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ - Solves for the shortest distance (magnitude) between points and - the surface of a polyhedron. If the point lies inside the + Solves for the shortest distance (magnitude) between points and + the surface of a polyhedron. If the point lies inside the polyhedron, the distance is negative. This function calculates the shortest distance by partitioning @@ -1206,8 +1210,10 @@ def shortest_distance_to_surface(self, points, translation_vector=np.array([0,0, [shape = (n_points,)] """ return shortest_distance_to_surface(self, points, translation_vector) - - def shortest_displacement_to_surface(self, points, translation_vector=np.array([0,0,0])): + + def shortest_displacement_to_surface( + self, points, translation_vector=np.array([0, 0, 0]) + ): """ Solves for the shortest displacement (vector) between points and the surface of a polyhedron. @@ -1237,4 +1243,3 @@ def shortest_displacement_to_surface(self, points, translation_vector=np.array([ [shape = (n_points, 3)] """ return shortest_displacement_to_surface(self, points, translation_vector) - diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 603ef6a1..b1b61379 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -643,52 +643,101 @@ def test_edge_lengths(poly): np.testing.assert_allclose(poly.edge_lengths, lengths) - def test_shortest_distance_convex(): - tri_verts = np.array([[0, 0.5], [-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25]]) + tri_verts = np.array( + [[0, 0.5], [-0.25 * np.sqrt(3), -0.25], [0.25 * np.sqrt(3), -0.25]] + ) triangle = ConvexPolygon(vertices=tri_verts) - test_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1]]) + test_points = np.array( + [[3.5, 3.25, 0], [3, 3.75, 0], [3, 3.25, 0], [3, 3, 1], [3.25, 3.5, -1]] + ) - distances = triangle.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,0])) - displacements = triangle.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,0])) + distances = triangle.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 0]) + ) + displacements = triangle.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 0]) + ) true_distances = np.array([0.3080127018, 0.25, 0, 1, 1.0231690965]) - true_displacements = np.array([[-0.2667468246, -0.1540063509, 0], [0,-0.25,0], [0,0,0],[0,0,-1],[-0.1875, -0.1082531755, 1]]) + true_displacements = np.array( + [ + [-0.2667468246, -0.1540063509, 0], + [0, -0.25, 0], + [0, 0, 0], + [0, 0, -1], + [-0.1875, -0.1082531755, 1], + ] + ) np.testing.assert_allclose(distances, true_distances) np.testing.assert_allclose(displacements, true_displacements) + def test_shortest_distance_concave(): - verts = np.array([[0,0.5],[-0.125,0.75],[-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25],[0.25*np.sqrt(3),0.75]]) + verts = np.array( + [ + [0, 0.5], + [-0.125, 0.75], + [-0.25 * np.sqrt(3), -0.25], + [0.25 * np.sqrt(3), -0.25], + [0.25 * np.sqrt(3), 0.75], + ] + ) concave_poly = Polygon(vertices=verts) - test_points = np.array([[3.5, 3.25,0],[3,3.75,0],[3,3.25,0],[3,3,-1],[3.25,3.5,-1],[3+0.25*np.sqrt(3),4,0]]) + test_points = np.array( + [ + [3.5, 3.25, 0], + [3, 3.75, 0], + [3, 3.25, 0], + [3, 3, -1], + [3.25, 3.5, -1], + [3 + 0.25 * np.sqrt(3), 4, 0], + ] + ) - distances = concave_poly.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,0])) - displacements = concave_poly.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,0])) + distances = concave_poly.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 0]) + ) + displacements = concave_poly.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 0]) + ) - true_distances = np.array([abs(0.25*np.sqrt(3)-0.5), np.sqrt(0.0125), 0, 1, 1, 0.25]) - true_displacements = np.array([[0.25*np.sqrt(3)-0.5,0,0],[-0.1,-0.05,0],[0,0,0],[0,0,1],[0,0,1],[0,-0.25,0]]) + true_distances = np.array( + [abs(0.25 * np.sqrt(3) - 0.5), np.sqrt(0.0125), 0, 1, 1, 0.25] + ) + true_displacements = np.array( + [ + [0.25 * np.sqrt(3) - 0.5, 0, 0], + [-0.1, -0.05, 0], + [0, 0, 0], + [0, 0, 1], + [0, 0, 1], + [0, -0.25, 0], + ] + ) np.testing.assert_allclose(distances, true_distances) np.testing.assert_allclose(displacements, true_displacements) + def test_shortest_distance_general(): - #Creating a random polygon (nonconvex usually) + # Creating a random polygon (nonconvex usually) # np.random.seed(3) - random_angles = np.random.rand(15)*2*np.pi #angles + random_angles = np.random.rand(15) * 2 * np.pi # angles sorted_angles = np.sort(random_angles) - random_dist = np.random.rand(15)*10 #from origin + random_dist = np.random.rand(15) * 10 # from origin - vertices = np.zeros((15,2)) - vertices[:,0] = random_dist * np.cos(sorted_angles) #x - vertices[:,1] = random_dist * np.sin(sorted_angles) #y + vertices = np.zeros((15, 2)) + vertices[:, 0] = random_dist * np.cos(sorted_angles) # x + vertices[:, 1] = random_dist * np.sin(sorted_angles) # y - poly = Polygon(vertices=vertices, normal=[0,0,1]) + poly = Polygon(vertices=vertices, normal=[0, 0, 1]) - points2d = np.random.rand(100,2)*20-10 - points3d = np.random.rand(150, 3)*20 -10 + points2d = np.random.rand(100, 2) * 20 - 10 + points3d = np.random.rand(150, 3) * 20 - 10 distances2d = poly.shortest_distance_to_surface(points2d) distances3d = poly.shortest_distance_to_surface(points3d) @@ -698,20 +747,25 @@ def test_shortest_distance_general(): np.testing.assert_allclose(distances2d, np.linalg.norm(displacements2d, axis=1)) np.testing.assert_allclose(distances3d, np.linalg.norm(displacements3d, axis=1)) - triangle_verts =[] + triangle_verts = [] for tri in poly._triangulation(): triangle_verts.append(list(tri)) - - triangle_verts = np.asarray(triangle_verts) + triangle_verts = np.asarray(triangle_verts) - tri_edges = np.append(triangle_verts[:,1:], np.expand_dims(triangle_verts[:,0], axis=1), axis=1) - triangle_verts #edges point counterclockwise + tri_edges = ( + np.append( + triangle_verts[:, 1:], np.expand_dims(triangle_verts[:, 0], axis=1), axis=1 + ) + - triangle_verts + ) # edges point counterclockwise - edges_90 = np.cross(tri_edges, poly.normal) #point outwards (n_triangles, 3, 3) - upper_bounds = np.sum(edges_90*triangle_verts, axis=2) #(n_triangles, 3) + edges_90 = np.cross(tri_edges, poly.normal) # point outwards (n_triangles, 3, 3) + upper_bounds = np.sum(edges_90 * triangle_verts, axis=2) # (n_triangles, 3) def scipy_closest_point(point, edges_90, upper_bounds): from scipy.optimize import LinearConstraint, minimize + all_tri_distances = [] all_tri_displacements = [] tmps = [] @@ -719,29 +773,42 @@ def scipy_closest_point(point, edges_90, upper_bounds): tri_min_point = minimize( fun=lambda pt: np.linalg.norm(pt - point), # Function to optimize x0=np.zeros(3), # Initial guess - constraints=[LinearConstraint(np.append(triangle[0].squeeze(), [[0,0,1], [0,0,-1]], axis=0), -np.inf, np.append(triangle[1].squeeze(), [0,0]))], - tol=1e-11 - ) + constraints=[ + LinearConstraint( + np.append( + triangle[0].squeeze(), [[0, 0, 1], [0, 0, -1]], axis=0 + ), + -np.inf, + np.append(triangle[1].squeeze(), [0, 0]), + ) + ], + tol=1e-11, + ) triangle_distance = np.linalg.norm(tri_min_point.x - point) all_tri_distances.append(triangle_distance) all_tri_displacements.append(tri_min_point.x - point) - return np.min(all_tri_distances), all_tri_displacements[np.argmin(all_tri_distances)] - + return np.min(all_tri_distances), all_tri_displacements[ + np.argmin(all_tri_distances) + ] + scipy_distances2d = [] scipy_displacements2d = [] for point in points2d: point = np.append(point, [0]) - - scipy_dist2d, scipy_displace2d = scipy_closest_point(point, edges_90, upper_bounds) + + scipy_dist2d, scipy_displace2d = scipy_closest_point( + point, edges_90, upper_bounds + ) scipy_distances2d.append(scipy_dist2d) scipy_displacements2d.append(scipy_displace2d) - + scipy_distances3d = [] scipy_displacements3d = [] for point in points3d: - - scipy_dist3d, scipy_displace3d = scipy_closest_point(point, edges_90, upper_bounds) + scipy_dist3d, scipy_displace3d = scipy_closest_point( + point, edges_90, upper_bounds + ) scipy_distances3d.append(scipy_dist3d) scipy_displacements3d.append(scipy_displace3d) diff --git a/tests/test_polyhedron.py b/tests/test_polyhedron.py index c8d1383a..5188a427 100644 --- a/tests/test_polyhedron.py +++ b/tests/test_polyhedron.py @@ -956,116 +956,306 @@ def test_to_hoomd(poly): assert np.allclose(face, hoomd_dict["faces"][i]) - def test_shortest_distance_convex(): - cube_verts = np.array([[1,1,1], [-1,1,1], [1,-1,1], [1,1,-1], [-1,-1,1], [-1,1,-1],[1,-1,-1],[-1,-1,-1]]) - cube = ConvexPolyhedron(vertices=cube_verts)#, faces=[[1,5,7,4],[0,2,6,3],[2,4,7,6],[3,6,7,5],[0,1,4,2],[1,0,3,5]]) + cube_verts = np.array( + [ + [1, 1, 1], + [-1, 1, 1], + [1, -1, 1], + [1, 1, -1], + [-1, -1, 1], + [-1, 1, -1], + [1, -1, -1], + [-1, -1, -1], + ] + ) + cube = ConvexPolyhedron( + vertices=cube_verts + ) # , faces=[[1,5,7,4],[0,2,6,3],[2,4,7,6],[3,6,7,5],[0,1,4,2],[1,0,3,5]]) - test_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6]]) + test_points = np.array( + [[3, 3, 3], [3, 3, 5], [5, 5, 1], [5, 4, 2], [3, 5, 5], [3, 4, 5], [3, 3, 6]] + ) - distances = cube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) - displacements = cube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) + distances = cube.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + displacements = cube.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - cube_surface_distance = cube.shortest_distance_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) - cube_surface_displacement = cube.shortest_displacement_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(cube_surface_distance, np.zeros((len(test_points)))) - np.testing.assert_allclose(cube_surface_displacement, np.zeros((len(test_points),3))) + cube_surface_distance = cube.shortest_distance_to_surface( + test_points + displacements, translation_vector=np.array([3, 3, 3]) + ) + cube_surface_displacement = cube.shortest_displacement_to_surface( + test_points + displacements, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose(cube_surface_distance, np.zeros(len(test_points))) + np.testing.assert_allclose( + cube_surface_displacement, np.zeros((len(test_points), 3)) + ) true_distances = np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2]) - true_displacements = np.array([[0,0,-1], [-1,-1,1], [-1,0,0], [0,-1,-1], [0,0,-1], [0,0,-2]]) + true_displacements = np.array( + [[0, 0, -1], [-1, -1, 1], [-1, 0, 0], [0, -1, -1], [0, 0, -1], [0, 0, -2]] + ) np.testing.assert_allclose(distances, true_distances) np.testing.assert_allclose(displacements[1:], true_displacements) def test_shortest_distance_concave(): - - test_points = np.array([[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[3.5,2.5,3],[4.5,3,5],[3,3,3],[3,3.5,3.5],[3,3,2.25]]) + test_points = np.array( + [ + [5, 5, 1], + [5, 4, 2], + [3, 5, 5], + [3, 4, 5], + [3, 3, 6], + [3.5, 2.5, 3], + [4.5, 3, 5], + [3, 3, 3], + [3, 3.5, 3.5], + [3, 3, 2.25], + ] + ) - #--- PYRAMID Point on Cube Case --- - pyramidcube_verts = np.array([[1,1,1],[-1,1,1],[1,-1,1],[1,1,-1],[-1,-1,1],[-1,1,-1],[1,-1,-1],[-1,-1,-1],[0,0,3],[0,3,0]]) - pyramid_faces = [[0,1,8],[1,4,8],[4,2,8],[2,0,8],[1,0,9],[5,1,9],[3,5,9],[0,3,9],[1,5,7,4],[0,2,6,3],[2,4,7,6],[3,6,7,5]] + # --- PYRAMID Point on Cube Case --- + pyramidcube_verts = np.array( + [ + [1, 1, 1], + [-1, 1, 1], + [1, -1, 1], + [1, 1, -1], + [-1, -1, 1], + [-1, 1, -1], + [1, -1, -1], + [-1, -1, -1], + [0, 0, 3], + [0, 3, 0], + ] + ) + pyramid_faces = [ + [0, 1, 8], + [1, 4, 8], + [4, 2, 8], + [2, 0, 8], + [1, 0, 9], + [5, 1, 9], + [3, 5, 9], + [0, 3, 9], + [1, 5, 7, 4], + [0, 2, 6, 3], + [2, 4, 7, 6], + [3, 6, 7, 5], + ] pyramidcube = Polyhedron(vertices=pyramidcube_verts, faces=pyramid_faces) - pyramid_distances = pyramidcube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) - pyramid_displacements = pyramidcube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(np.abs(pyramid_distances), np.linalg.norm(pyramid_displacements, axis=1)) + pyramid_distances = pyramidcube.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + pyramid_displacements = pyramidcube.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose( + np.abs(pyramid_distances), np.linalg.norm(pyramid_displacements, axis=1) + ) - pyramid_surface_distance = pyramidcube.shortest_distance_to_surface(test_points + pyramid_displacements, translation_vector=np.array([3,3,3])) - pyramid_surface_displacement = pyramidcube.shortest_displacement_to_surface(test_points + pyramid_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(pyramid_surface_distance, np.zeros((len(test_points)))) - np.testing.assert_allclose(pyramid_surface_displacement, np.zeros((len(test_points),3))) + pyramid_surface_distance = pyramidcube.shortest_distance_to_surface( + test_points + pyramid_displacements, translation_vector=np.array([3, 3, 3]) + ) + pyramid_surface_displacement = pyramidcube.shortest_displacement_to_surface( + test_points + pyramid_displacements, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose(pyramid_surface_distance, np.zeros(len(test_points))) + np.testing.assert_allclose( + pyramid_surface_displacement, np.zeros((len(test_points), 3)) + ) - pyramid_true_distances = np.array([np.sqrt(3), 1, 3/np.sqrt(5), 1/np.sqrt(5), 0, -0.5, 2/np.sqrt(5), -1, -1*np.sqrt(0.5), -0.25]) + pyramid_true_distances = np.array( + [ + np.sqrt(3), + 1, + 3 / np.sqrt(5), + 1 / np.sqrt(5), + 0, + -0.5, + 2 / np.sqrt(5), + -1, + -1 * np.sqrt(0.5), + -0.25, + ] + ) np.testing.assert_allclose(pyramid_distances, pyramid_true_distances) - #--- PRISM Point on Cube Case --- - prismcube_verts = np.array([[1,1,1],[-1,1,1],[1,-1,1],[1,1,-1],[-1,-1,1],[-1,1,-1],[1,-1,-1],[-1,-1,-1],[1,0,3],[-1,0,3],[1,3,0],[-1,3,0]]) - prism_faces = [[0,1,9,8],[2,8,9,4],[2,4,7,6],[3,6,7,5],[3,5,11,10],[1,0,10,11],[0,8,2,6,3,10],[1,11,5,7,4,9]] + # --- PRISM Point on Cube Case --- + prismcube_verts = np.array( + [ + [1, 1, 1], + [-1, 1, 1], + [1, -1, 1], + [1, 1, -1], + [-1, -1, 1], + [-1, 1, -1], + [1, -1, -1], + [-1, -1, -1], + [1, 0, 3], + [-1, 0, 3], + [1, 3, 0], + [-1, 3, 0], + ] + ) + prism_faces = [ + [0, 1, 9, 8], + [2, 8, 9, 4], + [2, 4, 7, 6], + [3, 6, 7, 5], + [3, 5, 11, 10], + [1, 0, 10, 11], + [0, 8, 2, 6, 3, 10], + [1, 11, 5, 7, 4, 9], + ] prismcube = Polyhedron(vertices=prismcube_verts, faces=prism_faces) - prism_distances = prismcube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) - prism_displacements = prismcube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(np.abs(prism_distances), np.linalg.norm(prism_displacements, axis=1)) + prism_distances = prismcube.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + prism_displacements = prismcube.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose( + np.abs(prism_distances), np.linalg.norm(prism_displacements, axis=1) + ) - prism_surface_distance = prismcube.shortest_distance_to_surface(test_points + prism_displacements, translation_vector=np.array([3,3,3])) - prism_surface_displacement = prismcube.shortest_displacement_to_surface(test_points + prism_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(prism_surface_distance, np.zeros((len(test_points)))) - np.testing.assert_allclose(prism_surface_displacement, np.zeros((len(test_points),3))) + prism_surface_distance = prismcube.shortest_distance_to_surface( + test_points + prism_displacements, translation_vector=np.array([3, 3, 3]) + ) + prism_surface_displacement = prismcube.shortest_displacement_to_surface( + test_points + prism_displacements, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose(prism_surface_distance, np.zeros(len(test_points))) + np.testing.assert_allclose( + prism_surface_displacement, np.zeros((len(test_points), 3)) + ) - prism_true_distances = np.array([np.sqrt(70)/5, 1, 3/np.sqrt(5), 1/np.sqrt(5), 0, -0.5, 0.5, -1, -1*np.sqrt(0.5), -0.25]) + prism_true_distances = np.array( + [ + np.sqrt(70) / 5, + 1, + 3 / np.sqrt(5), + 1 / np.sqrt(5), + 0, + -0.5, + 0.5, + -1, + -1 * np.sqrt(0.5), + -0.25, + ] + ) np.testing.assert_allclose(prism_distances, prism_true_distances) - #--- INDENTED Cube Case --- - indented_cube_verts = np.array([[1,1,1],[-1,1,1],[1,-1,1],[1,1,-1],[-1,-1,1],[-1,1,-1],[1,-1,-1],[-1,-1,-1],[0,0,-0.5]]) - indented_faces = [[0,3,5,1],[0,2,6,3],[1,5,7,4],[2,4,7,6],[3,6,7,5],[0,1,8],[0,8,2],[2,8,4],[1,4,8]] + # --- INDENTED Cube Case --- + indented_cube_verts = np.array( + [ + [1, 1, 1], + [-1, 1, 1], + [1, -1, 1], + [1, 1, -1], + [-1, -1, 1], + [-1, 1, -1], + [1, -1, -1], + [-1, -1, -1], + [0, 0, -0.5], + ] + ) + indented_faces = [ + [0, 3, 5, 1], + [0, 2, 6, 3], + [1, 5, 7, 4], + [2, 4, 7, 6], + [3, 6, 7, 5], + [0, 1, 8], + [0, 8, 2], + [2, 8, 4], + [1, 4, 8], + ] indented_cube = Polyhedron(vertices=indented_cube_verts, faces=indented_faces) - indented_distances = indented_cube.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) - indented_displacements = indented_cube.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(np.abs(indented_distances), np.linalg.norm(indented_displacements, axis=1)) - - indented_surface_distance = indented_cube.shortest_distance_to_surface(test_points + indented_displacements, translation_vector=np.array([3,3,3])) - indented_surface_displacement = indented_cube.shortest_displacement_to_surface(test_points + indented_displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(indented_surface_distance, np.zeros((len(test_points))), atol=2e-10) - np.testing.assert_allclose(indented_surface_displacement, np.zeros((len(test_points),3)), atol=2e-10) + indented_distances = indented_cube.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + indented_displacements = indented_cube.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose( + np.abs(indented_distances), np.linalg.norm(indented_displacements, axis=1) + ) + + indented_surface_distance = indented_cube.shortest_distance_to_surface( + test_points + indented_displacements, translation_vector=np.array([3, 3, 3]) + ) + indented_surface_displacement = indented_cube.shortest_displacement_to_surface( + test_points + indented_displacements, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose( + indented_surface_distance, np.zeros(len(test_points)), atol=2e-10 + ) + np.testing.assert_allclose( + indented_surface_displacement, np.zeros((len(test_points), 3)), atol=2e-10 + ) - indented_true_distances = np.array([np.sqrt(3), 1, np.sqrt(2), 1, np.sqrt(5), -0.1714985851, np.sqrt(1.25), 1/np.sqrt(13), 0.5/np.sqrt(13), -0.25]) + indented_true_distances = np.array( + [ + np.sqrt(3), + 1, + np.sqrt(2), + 1, + np.sqrt(5), + -0.1714985851, + np.sqrt(1.25), + 1 / np.sqrt(13), + 0.5 / np.sqrt(13), + -0.25, + ] + ) np.testing.assert_allclose(indented_distances, indented_true_distances) def test_shortest_distance_convex_general(): - #Creating a random convex polyhedron + # Creating a random convex polyhedron # np.random.seed(6) - random_theta = np.random.rand(20)*np.pi - random_phi = np.random.rand(20)*2*np.pi - radius = np.random.rand(1)*5 + random_theta = np.random.rand(20) * np.pi + random_phi = np.random.rand(20) * 2 * np.pi + radius = np.random.rand(1) * 5 - vertices = np.zeros((20,3)) - vertices[:,0] = radius * np.sin(random_theta) * np.cos(random_phi) #x - vertices[:,1] = radius * np.sin(random_theta) * np.sin(random_phi) #y - vertices[:,2] = radius * np.cos(random_theta) + vertices = np.zeros((20, 3)) + vertices[:, 0] = radius * np.sin(random_theta) * np.cos(random_phi) # x + vertices[:, 1] = radius * np.sin(random_theta) * np.sin(random_phi) # y + vertices[:, 2] = radius * np.cos(random_theta) poly = ConvexPolyhedron(vertices=vertices) - points = np.random.rand(1500, 3)*20 -10 + points = np.random.rand(1500, 3) * 20 - 10 distances = poly.shortest_distance_to_surface(points) displacements = poly.shortest_displacement_to_surface(points) - + np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - - #Verifying that the displacements will move the points onto the surface - poly_surface_distance = poly.shortest_distance_to_surface(points+displacements) - poly_surface_displacement = poly.shortest_displacement_to_surface(points+displacements) - np.testing.assert_allclose(poly_surface_distance, np.zeros((len(points))), atol=2e-8) - np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(points), 3)), atol=2e-8) + # Verifying that the displacements will move the points onto the surface + poly_surface_distance = poly.shortest_distance_to_surface(points + displacements) + poly_surface_displacement = poly.shortest_displacement_to_surface( + points + displacements + ) + np.testing.assert_allclose(poly_surface_distance, np.zeros(len(points)), atol=2e-8) + np.testing.assert_allclose( + poly_surface_displacement, np.zeros((len(points), 3)), atol=2e-8 + ) def scipy_closest_point(point, surface_constraint, surface_bounds): from scipy.optimize import LinearConstraint, minimize @@ -1074,14 +1264,13 @@ def scipy_closest_point(point, surface_constraint, surface_bounds): fun=lambda pt: np.linalg.norm(pt - point), # Function to optimize x0=np.zeros(3), # Initial guess constraints=[LinearConstraint(surface_constraint, -np.inf, surface_bounds)], - tol=1e-12 - ) + tol=1e-12, + ) distance = np.linalg.norm(tri_min_point.x - point) displacement = tri_min_point.x - point return distance, displacement - poly_constraint = poly.normals poly_bounds = np.sum(poly_constraint * poly.face_centroids, axis=1) @@ -1089,23 +1278,26 @@ def scipy_closest_point(point, surface_constraint, surface_bounds): scipy_distances = [] scipy_displacements = [] for point in points: - outside_distance, outside_displacement = scipy_closest_point(point, poly_constraint, poly_bounds) + outside_distance, outside_displacement = scipy_closest_point( + point, poly_constraint, poly_bounds + ) - scipy_distances.append( outside_distance) - scipy_displacements.append(outside_displacement ) + scipy_distances.append(outside_distance) + scipy_displacements.append(outside_displacement) scipy_distances = np.asarray(scipy_distances) scipy_displacements = np.asarray(scipy_displacements) - #Setting points inside the polyhedron to have 0 distance + # Setting points inside the polyhedron to have 0 distance is_zero_bool = distances <= 0 zero_inside_distances = distances zero_inside_distances[is_zero_bool] = 0 zero_inside_displacements = displacements - zero_inside_displacements[is_zero_bool] = np.array([0,0,0]) - + zero_inside_displacements[is_zero_bool] = np.array([0, 0, 0]) np.testing.assert_allclose(zero_inside_distances, scipy_distances, atol=2e-8) - np.testing.assert_allclose(zero_inside_displacements, scipy_displacements, atol=2e-5) + np.testing.assert_allclose( + zero_inside_displacements, scipy_displacements, atol=2e-5 + ) diff --git a/tests/test_spheropolygon.py b/tests/test_spheropolygon.py index 88bba452..7462be44 100644 --- a/tests/test_spheropolygon.py +++ b/tests/test_spheropolygon.py @@ -236,42 +236,66 @@ def test_to_hoomd(unit_rounded_square): assert np.allclose(hoomd_dict[key], val), f"{key}" - - - def test_shortest_distance_convex(): - tri_verts = np.array([[0, 0.5], [-0.25*np.sqrt(3), -0.25], [0.25*np.sqrt(3), -0.25]]) - triangle = ConvexSpheropolygon(vertices=tri_verts, radius = 0.25) - - test_points = np.array([[3.5,3.25,0], [3,3.75,0], [3,3.25,0], [3,3,1], [3.25,3.5, -1], [3.5,3.75,1], [3-0.25*np.sqrt(3), 2.65,0], [3,4,-1]]) + tri_verts = np.array( + [[0, 0.5], [-0.25 * np.sqrt(3), -0.25], [0.25 * np.sqrt(3), -0.25]] + ) + triangle = ConvexSpheropolygon(vertices=tri_verts, radius=0.25) + + test_points = np.array( + [ + [3.5, 3.25, 0], + [3, 3.75, 0], + [3, 3.25, 0], + [3, 3, 1], + [3.25, 3.5, -1], + [3.5, 3.75, 1], + [3 - 0.25 * np.sqrt(3), 2.65, 0], + [3, 4, -1], + ] + ) - distances = triangle.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,0])) - displacements = triangle.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,0])) + distances = triangle.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 0]) + ) + displacements = triangle.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 0]) + ) true_distances = np.array([0.0580127018, 0, 0, 1, 1, 1.0463612304, 0, 1.0307764064]) - true_displacements = np.array([[-0.0502404735, -0.0290063509, 0], [0,0,0], [0,0,0], [0,0,-1], [0,0,1], [-0.2667468244, -0.1540063509, -1], [0,0,0], [0,-0.25,1]]) + true_displacements = np.array( + [ + [-0.0502404735, -0.0290063509, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, -1], + [0, 0, 1], + [-0.2667468244, -0.1540063509, -1], + [0, 0, 0], + [0, -0.25, 1], + ] + ) np.testing.assert_allclose(distances, true_distances) np.testing.assert_allclose(displacements, true_displacements) - def test_shortest_distance_general(): - #Creating a random convex spheropolygon + # Creating a random convex spheropolygon # np.random.seed(3) - random_angles = np.random.rand(10)*2*np.pi #angles + random_angles = np.random.rand(10) * 2 * np.pi # angles sorted_angles = np.sort(random_angles) - random_dist = np.random.rand(1)*10 #from origin - radius = np.random.rand(1)*5 + random_dist = np.random.rand(1) * 10 # from origin + radius = np.random.rand(1) * 5 - vertices = np.zeros((10,2)) - vertices[:,0] = random_dist * np.cos(sorted_angles) #x - vertices[:,1] = random_dist * np.sin(sorted_angles) #y + vertices = np.zeros((10, 2)) + vertices[:, 0] = random_dist * np.cos(sorted_angles) # x + vertices[:, 1] = random_dist * np.sin(sorted_angles) # y - poly = ConvexSpheropolygon(vertices=vertices, radius=radius, normal=[0,0,1]) + poly = ConvexSpheropolygon(vertices=vertices, radius=radius, normal=[0, 0, 1]) - points2d = np.random.rand(100,2)*20-10 - points3d = np.random.rand(150, 3)*20 -10 + points2d = np.random.rand(100, 2) * 20 - 10 + points3d = np.random.rand(150, 3) * 20 - 10 distances2d = poly.shortest_distance_to_surface(points2d) distances3d = poly.shortest_distance_to_surface(points3d) @@ -280,9 +304,11 @@ def test_shortest_distance_general(): np.testing.assert_allclose(distances2d, np.linalg.norm(displacements2d, axis=1)) np.testing.assert_allclose(distances3d, np.linalg.norm(displacements3d, axis=1)) - - edges_90 = np.cross(poly._polygon.edge_vectors, poly.normal) #point outwards (10, 3) - upper_bounds = np.sum(edges_90*poly.vertices, axis=1) #(10,) + + edges_90 = np.cross( + poly._polygon.edge_vectors, poly.normal + ) # point outwards (10, 3) + upper_bounds = np.sum(edges_90 * poly.vertices, axis=1) # (10,) def scipy_closest_point(point, edges_90, upper_bounds): from scipy.optimize import LinearConstraint, minimize @@ -290,38 +316,47 @@ def scipy_closest_point(point, edges_90, upper_bounds): tri_min_point = minimize( fun=lambda pt: np.linalg.norm(pt - point), # Function to optimize x0=np.zeros(3), # Initial guess - constraints=[LinearConstraint(np.append(edges_90, [[0,0,1], [0,0,-1]], axis=0), -np.inf, np.append(upper_bounds, [0,0]))], - tol=1e-10 - ) + constraints=[ + LinearConstraint( + np.append(edges_90, [[0, 0, 1], [0, 0, -1]], axis=0), + -np.inf, + np.append(upper_bounds, [0, 0]), + ) + ], + tol=1e-10, + ) distance = np.linalg.norm(tri_min_point.x - point) displacement = tri_min_point.x - point return distance, displacement - - #--- 2D --- + + # --- 2D --- scipy_distances2d = [] scipy_displacements2d = [] for point in points2d: point = np.append(point, [0]) - - scipy_dist2d, scipy_displace2d = scipy_closest_point(point, edges_90, upper_bounds) + + scipy_dist2d, scipy_displace2d = scipy_closest_point( + point, edges_90, upper_bounds + ) scipy_distances2d.append(scipy_dist2d) scipy_displacements2d.append(scipy_displace2d) - - #--- 3D --- + + # --- 3D --- scipy_distances3d = [] scipy_displacements3d = [] for point in points3d: + scipy_dist3d, scipy_displace3d = scipy_closest_point( + point, edges_90, upper_bounds + ) - scipy_dist3d, scipy_displace3d = scipy_closest_point(point, edges_90, upper_bounds) - - inplane_disp = scipy_displace3d - (scipy_displace3d @ poly.normal)*poly.normal + inplane_disp = scipy_displace3d - (scipy_displace3d @ poly.normal) * poly.normal inplane_dist = np.linalg.norm(inplane_disp) if inplane_dist < radius: subtract_vector = inplane_disp else: - subtract_vector = (radius * inplane_disp/ np.linalg.norm(inplane_disp)) + subtract_vector = radius * inplane_disp / np.linalg.norm(inplane_disp) scipy_displace3d = scipy_displace3d - subtract_vector scipy_displacements3d.append(scipy_displace3d) @@ -331,9 +366,13 @@ def scipy_closest_point(point, edges_90, upper_bounds): is_zero2d = scipy_distances2d < 0 scipy_distances2d[is_zero2d] = 0 - scipy_displacements2d = np.asarray(scipy_displacements2d) - scipy_displacements2d = scipy_displacements2d - (radius * scipy_displacements2d / np.expand_dims(np.linalg.norm(scipy_displacements2d, axis=1),axis=1)) - scipy_displacements2d[is_zero2d] = np.array([0,0,0]) + scipy_displacements2d = np.asarray(scipy_displacements2d) + scipy_displacements2d = scipy_displacements2d - ( + radius + * scipy_displacements2d + / np.expand_dims(np.linalg.norm(scipy_displacements2d, axis=1), axis=1) + ) + scipy_displacements2d[is_zero2d] = np.array([0, 0, 0]) np.testing.assert_allclose(distances2d, scipy_distances2d, atol=2e-8) np.testing.assert_allclose(displacements2d, scipy_displacements2d, atol=2e-5) diff --git a/tests/test_spheropolyhedron.py b/tests/test_spheropolyhedron.py index 290db43a..85d9fb14 100644 --- a/tests/test_spheropolyhedron.py +++ b/tests/test_spheropolyhedron.py @@ -161,63 +161,108 @@ def test_to_hoomd(poly, r): def test_shortest_distance_convex(): - - radius = 0.5#np.random.rand(1) - verts = np.array([[1,1,1], [-1,1,1], [1,-1,1], [1,1,-1], [-1,-1,1], [-1,1,-1],[1,-1,-1],[-1,-1,-1]]) - poly = ConvexSpheropolyhedron(vertices=verts, radius = radius) - - test_points = np.array([[3,3,3],[3,3,5],[5,5,1],[5,4,2],[3,5,5],[3,4,5],[3,3,6],[4,4,4],[4,4,3],[4,3,3]]) - - distances = poly.shortest_distance_to_surface(test_points, translation_vector=np.array([3,3,3])) - displacements = poly.shortest_displacement_to_surface(test_points, translation_vector=np.array([3,3,3])) + radius = 0.5 # np.random.rand(1) + verts = np.array( + [ + [1, 1, 1], + [-1, 1, 1], + [1, -1, 1], + [1, 1, -1], + [-1, -1, 1], + [-1, 1, -1], + [1, -1, -1], + [-1, -1, -1], + ] + ) + poly = ConvexSpheropolyhedron(vertices=verts, radius=radius) + + test_points = np.array( + [ + [3, 3, 3], + [3, 3, 5], + [5, 5, 1], + [5, 4, 2], + [3, 5, 5], + [3, 4, 5], + [3, 3, 6], + [4, 4, 4], + [4, 4, 3], + [4, 3, 3], + ] + ) + + distances = poly.shortest_distance_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) + displacements = poly.shortest_displacement_to_surface( + test_points, translation_vector=np.array([3, 3, 3]) + ) print(distances) print(displacements) np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - poly_surface_distance = poly.shortest_distance_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) - poly_surface_displacement = poly.shortest_displacement_to_surface(test_points + displacements, translation_vector=np.array([3,3,3])) - np.testing.assert_allclose(poly_surface_distance, np.zeros((len(test_points))), atol=1e-10) - np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(test_points),3)), atol=1e-10) - - true_distances = np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2, 0, 0, 0]) - radius - true_displacements = np.array([[0,0,-1], [-1,-1,1], [-1,0,0], [0,-1,-1], [0,0,-1], [0,0,-2]]) - true_displacements = true_displacements - radius*(true_displacements/np.expand_dims(np.linalg.norm(true_displacements, axis=1),axis=1)) + poly_surface_distance = poly.shortest_distance_to_surface( + test_points + displacements, translation_vector=np.array([3, 3, 3]) + ) + poly_surface_displacement = poly.shortest_displacement_to_surface( + test_points + displacements, translation_vector=np.array([3, 3, 3]) + ) + np.testing.assert_allclose( + poly_surface_distance, np.zeros(len(test_points)), atol=1e-10 + ) + np.testing.assert_allclose( + poly_surface_displacement, np.zeros((len(test_points), 3)), atol=1e-10 + ) + + true_distances = ( + np.array([-1, 1, np.sqrt(3), 1, np.sqrt(2), 1, 2, 0, 0, 0]) - radius + ) + true_displacements = np.array( + [[0, 0, -1], [-1, -1, 1], [-1, 0, 0], [0, -1, -1], [0, 0, -1], [0, 0, -2]] + ) + true_displacements = true_displacements - radius * ( + true_displacements + / np.expand_dims(np.linalg.norm(true_displacements, axis=1), axis=1) + ) np.testing.assert_allclose(distances, true_distances) np.testing.assert_allclose(displacements[1:7], true_displacements) def test_shortest_distance_convex_general(): - #Creating a random convex spheropolyhedron + # Creating a random convex spheropolyhedron # np.random.seed(6) - random_theta = np.random.rand(20)*np.pi - random_phi = np.random.rand(20)*2*np.pi - radius = np.random.rand(1)*5 #radius for the convex polyhedron - sph_poly_radius = np.random.rand(1)*2 #radius for the spheropolyhedron + random_theta = np.random.rand(20) * np.pi + random_phi = np.random.rand(20) * 2 * np.pi + radius = np.random.rand(1) * 5 # radius for the convex polyhedron + sph_poly_radius = np.random.rand(1) * 2 # radius for the spheropolyhedron - vertices = np.zeros((20,3)) - vertices[:,0] = radius * np.sin(random_theta) * np.cos(random_phi) #x - vertices[:,1] = radius * np.sin(random_theta) * np.sin(random_phi) #y - vertices[:,2] = radius * np.cos(random_theta) + vertices = np.zeros((20, 3)) + vertices[:, 0] = radius * np.sin(random_theta) * np.cos(random_phi) # x + vertices[:, 1] = radius * np.sin(random_theta) * np.sin(random_phi) # y + vertices[:, 2] = radius * np.cos(random_theta) poly = ConvexSpheropolyhedron(vertices=vertices, radius=sph_poly_radius) - points = np.random.rand(1500, 3)*20 -10 + points = np.random.rand(1500, 3) * 20 - 10 distances = poly.shortest_distance_to_surface(points) displacements = poly.shortest_displacement_to_surface(points) - + np.testing.assert_allclose(np.abs(distances), np.linalg.norm(displacements, axis=1)) - - #Verifying that the displacements will move the points onto the surface - poly_surface_distance = poly.shortest_distance_to_surface(points+displacements) - poly_surface_displacement = poly.shortest_displacement_to_surface(points+displacements) - np.testing.assert_allclose(poly_surface_distance, np.zeros((len(points))), atol=2e-8) - np.testing.assert_allclose(poly_surface_displacement, np.zeros((len(points), 3)), atol=2e-8) + # Verifying that the displacements will move the points onto the surface + poly_surface_distance = poly.shortest_distance_to_surface(points + displacements) + poly_surface_displacement = poly.shortest_displacement_to_surface( + points + displacements + ) + np.testing.assert_allclose(poly_surface_distance, np.zeros(len(points)), atol=2e-8) + np.testing.assert_allclose( + poly_surface_displacement, np.zeros((len(points), 3)), atol=2e-8 + ) def scipy_closest_point(point, surface_constraint, surface_bounds): from scipy.optimize import LinearConstraint, minimize @@ -226,13 +271,12 @@ def scipy_closest_point(point, surface_constraint, surface_bounds): fun=lambda pt: np.linalg.norm(pt - point), # Function to optimize x0=np.zeros(3), # Initial guess constraints=[LinearConstraint(surface_constraint, -np.inf, surface_bounds)], - tol=1e-12 - ) + tol=1e-12, + ) distance = np.linalg.norm(tri_min_point.x - point) return distance - poly_constraint = poly._polyhedron.normals poly_bounds = np.sum(poly_constraint * poly._polyhedron.face_centroids, axis=1) @@ -241,7 +285,7 @@ def scipy_closest_point(point, surface_constraint, surface_bounds): for point in points: outside_distance = scipy_closest_point(point, poly_constraint, poly_bounds) - scipy_distances.append( outside_distance) + scipy_distances.append(outside_distance) scipy_distances = np.asarray(scipy_distances) - sph_poly_radius scipy_zero = scipy_distances < 0