def _disperse_within_boundary(nodes, vert, smp, rho=None, pinned_nodes=None, m=None, delta=0.1, bound_force=False): ''' Returns `nodes` after beingly slightly dispersed within the boundaries defined by `vert` and `smp`. The disperson is analogous to electrostatic repulsion, where neighboring node exert a repulsive force on eachother. If a node is repelled into a boundary then it bounces back in. ''' if bound_force: bound_vert, bound_smp = vert, smp else: bound_vert, bound_smp = None, None # node positions after repulsion out = _disperse(nodes, rho=rho, pinned_nodes=pinned_nodes, m=m, delta=delta, vert=bound_vert, smp=bound_smp) # boolean array of nodes which are now outside the domain crossed = intersection_count(nodes, out, vert, smp) > 0 # point where nodes intersected the boundary inter = intersection_point(nodes[crossed], out[crossed], vert, smp) # normal vector to intersection point norms = intersection_normal(nodes[crossed], out[crossed], vert, smp) # distance that the node wanted to travel beyond the boundary res = out[crossed] - inter # bouce node off the boundary out[crossed] -= 2 * norms * np.sum(res * norms, 1)[:, None] # check to see if the bounced nodes still intersect the boundary. If # not then set the bounced nodes back to their original position crossed = intersection_count(nodes, out, vert, smp) > 0 out[crossed] = nodes[crossed] return out
def _has_intersections(c, x, vert, smp): ''' Check if any of the edges (`c`, `x[i]`) intersect the boundary defined by `vert` and `smp`. ''' N = len(x) cext = np.repeat(c[None, :], N, axis=0) # number of times each edge intersects the boundary count = intersection_count(x, cext, vert, smp) # return True if there are intersections out = np.any(count > 0) return out
def _has_edge_intersections(c, x, vert, smp, check_all_edges): """ Check if any of the edges (*c*,*x[i]*) intersect the boundary defined by *vert* and *smp*. If *check_all_edges* is True then the edges (*x[i]*,*x[j]*) are also tested. """ N = len(x) cext = np.repeat(c[None, :], N, axis=0) # number of times each edge intersects the boundary count = intersection_count(x, cext, vert, smp) # return True if there are no intersections intersects = np.any(count > 0) if ~intersects & check_all_edges: a, b = [], [] for i, j in combinations(range(N), 2): a += [i] b += [j] # number of times each edge intersects the boundary count = intersection_count(x[a], x[b], vert, smp) # return True if there are no intersections intersects = np.any(count > 0) return intersects
def _snap_to_boundary(nodes, vert, smp, delta=1.0): ''' Snaps nodes to the boundary defined by `vert` and `smp`. This is done by slightly shifting each node along the basis directions and checking to see if that caused a boundary intersection. If so, then the intersecting node is reset at the point of intersection. Returns ------- (N, D) float array New nodes positions. (N, D) int array Index of the simplex that each node is on. If a node is not on a simplex (i.e. it is an interior node) then the simplex index is -1. ''' n, dim = nodes.shape # find the distance to the nearest node dx = _neighbors(nodes, 2)[1][:, 1] # allocate output arrays out_smpid = np.full(n, -1, dtype=int) out_nodes = np.array(nodes, copy=True) min_dist = np.full(n, np.inf, dtype=float) for i in range(dim): for sign in [-1.0, 1.0]: # `pert_nodes` is `nodes` shifted slightly along dimension `i` pert_nodes = np.array(nodes, copy=True) pert_nodes[:, i] += delta * sign * dx # find which segments intersect the boundary idx, = (intersection_count(nodes, pert_nodes, vert, smp) > 0).nonzero() # find the intersection points pnt = intersection_point(nodes[idx], pert_nodes[idx], vert, smp) # find the distance between `nodes` and the intersection point dist = np.linalg.norm(nodes[idx] - pnt, axis=1) # only snap nodes which have an intersection point that is # closer than any of their previously found intersection points snap = dist < min_dist[idx] snap_idx = idx[snap] out_smpid[snap_idx] = intersection_index(nodes[snap_idx], pert_nodes[snap_idx], vert, smp) out_nodes[snap_idx] = pnt[snap] min_dist[snap_idx] = dist[snap] return out_nodes, out_smpid
def disperse(nodes, vert=None, smp=None, rho=None, fix_nodes=None, m=None, delta=0.1, bound_force=False): ''' Returns *nodes* after beingly slightly dispersed. The disperson is analogous to electrostatic repulsion, where neighboring node exert a repulsive force on eachother. The repulsive force for each node is constant, by default, but it can vary spatially by specifying *rho*. If a node intersects the boundary defined by *vert* and *smp* then it will bounce off the boundary elastically. This ensures that no nodes will leave the domain, assuming the domain is closed and all nodes are initially inside. Using the electrostatic analogy, this function returns the nodes after a single *time step*, and greater amounts of dispersion can be attained by calling this function iteratively. Parameters ---------- nodes : (N,D) float array Node positions. vert : (P,D) array, optional Boundary vertices. smp : (Q,D) array, optional Describes how the vertices are connected to form the boundary. rho : function, optional Node density function. Takes a (N,D) array of coordinates in D dimensional space and returns an (N,) array of densities which have been normalized so that the maximum density in the domain is 1.0. This function will still work if the maximum value is normalized to something less than 1.0; however it will be less efficient. fix_nodes : (F,D) array, optional Nodes which do not move and only provide a repulsion force. m : int, optional Number of neighboring nodes to use when calculating the repulsion force. When *m* is small, the equilibrium state tends to be a uniform node distribution (regardless of *rho*), when *m* is large, nodes tend to get pushed up against the boundaries. delta : float, optional Scaling factor for the node step size. The step size is equal to *delta* times the distance to the nearest neighbor. bound_force : bool, optional If True, then nodes cannot repel other nodes through the domain boundary. Set to True if the domain has edges that nearly touch eachother. Setting this to True may significantly increase computation time. Returns ------- out : (N,D) float array Nodes after being dispersed. ''' nodes = np.asarray(nodes, dtype=float) if vert is None: vert = np.zeros((0, nodes.shape[1]), dtype=float) else: vert = np.asarray(vert, dtype=float) if smp is None: smp = np.zeros((0, nodes.shape[1]), dtype=int) else: smp = np.asarray(smp, dtype=int) if bound_force: bound_vert, bound_smp = vert, smp else: bound_vert, bound_smp = None, None if rho is None: def rho(p): return np.ones(p.shape[0]) if fix_nodes is None: fix_nodes = np.zeros((0, nodes.shape[1]), dtype=float) else: fix_nodes = np.asarray(fix_nodes, dtype=float) if m is None: # number of neighbors defaults to 3 raised to the number of # spatial dimensions m = 3**nodes.shape[1] # ensure that the number of nodes used to determine repulsion force # is less than or equal to the total number of nodes m = min(m, nodes.shape[0] + fix_nodes.shape[0]) # node positions after repulsion out = _disperse(nodes, fix_nodes, rho, m, delta, bound_vert, bound_smp) # boolean array of nodes which are now outside the domain crossed = intersection_count(nodes, out, vert, smp) > 0 # point where nodes intersected the boundary inter = intersection_point(nodes[crossed], out[crossed], vert, smp) # normal vector to intersection point norms = intersection_normal(nodes[crossed], out[crossed], vert, smp) # distance that the node wanted to travel beyond the boundary res = out[crossed] - inter # bouce node off the boundary out[crossed] -= 2 * norms * np.sum(res * norms, 1)[:, None] # check to see if the bounced nodes still intersect the boundary. If # not then set the bounced nodes back to their original position crossed = intersection_count(nodes, out, vert, smp) > 0 out[crossed] = nodes[crossed] return out
def snap_to_boundary(nodes, vert, smp, delta=1.0): ''' Snaps nodes to the boundary defined by *vert* and *smp*. This is done by slightly shifting each node along the basis directions and checking to see if that caused a boundary intersection. If so, then the intersecting node is reset at the point of intersection. Parameters ---------- nodes : (N,D) float array Node positions. vert : (M,D) float array Vertices making up the boundary. smp : (P,D) int array Connectivity of the vertices to form the boundary. Each row contains the indices of the vertices which form one simplex of the boundary. delta : float, optional Controls the maximum snapping distance. The maximum snapping distance for each node is *delta* times the distance to the nearest neighbor. This defaults to 1.0. Returns ------- out_nodes : (N,D) float array New nodes positions. out_smp : (N,D) int array Index of the simplex that each node is on. If a node is not on a simplex (i.e. it is an interior node) then the simplex index is -1. ''' nodes = np.asarray(nodes, dtype=float) vert = np.asarray(vert, dtype=float) smp = np.asarray(smp, dtype=int) n, dim = nodes.shape # find the distance to the nearest node dx = neighbors(nodes, 2)[1][:, 1] # allocate output arrays out_smpid = np.full(n, -1, dtype=int) out_nodes = np.array(nodes, copy=True) min_dist = np.full(n, np.inf, dtype=float) for i in range(dim): for sign in [-1.0, 1.0]: # *pert_nodes* is *nodes* shifted slightly along dimension *i* pert_nodes = np.array(nodes, copy=True) pert_nodes[:, i] += delta * sign * dx # find which segments intersect the boundary idx, = (intersection_count(nodes, pert_nodes, vert, smp) > 0).nonzero() # find the intersection points pnt = intersection_point(nodes[idx], pert_nodes[idx], vert, smp) # find the distance between *nodes* and the intersection point dist = np.linalg.norm(nodes[idx] - pnt, axis=1) # only snap nodes which have an intersection point that is # closer than any of their previously found intersection points snap = dist < min_dist[idx] snap_idx = idx[snap] out_smpid[snap_idx] = intersection_index(nodes[snap_idx], pert_nodes[snap_idx], vert, smp) out_nodes[snap_idx] = pnt[snap] min_dist[snap_idx] = dist[snap] return out_nodes, out_smpid