def _repel_bounce(free_nodes, vert, smp, rho, fix_nodes, itr, n, delta, max_bounces, bound_force): """ nodes are repelled by eachother and bounce off boundaries """ free_nodes = np.array(free_nodes, dtype=float, copy=True) # if bound_force then use the domain boundary as the force # boundary if bound_force: bound_vert = vert bound_smp = smp else: bound_vert = np.zeros((0, vert.shape[1]), dtype=float) bound_smp = np.zeros((0, vert.shape[1]), dtype=int) # this is used for the lengthscale of the domain scale = vert.ptp() # ensure that the number of nodes used to determine repulsion force # is less than or equal to the total number of nodes n = min(n, free_nodes.shape[0] + fix_nodes.shape[0]) for k in range(itr): # node positions after repulsion free_nodes_new = _repel_step(free_nodes, rho, fix_nodes, n, delta, bound_vert, bound_smp) # boolean array of nodes which are now outside the domain crossed = ~gm.contains(free_nodes_new, vert, smp) bounces = 0 while np.any(crossed): # point where nodes intersected the boundary inter = gm.intersection_point(free_nodes[crossed], free_nodes_new[crossed], vert, smp) # normal vector to intersection point norms = gm.intersection_normal(free_nodes[crossed], free_nodes_new[crossed], vert, smp) # distance that the node wanted to travel beyond the boundary res = free_nodes_new[crossed] - inter # move the previous node position to just within the boundary free_nodes[crossed] = inter - 1e-10 * scale * norms # 3 is the number of bounces allowed if bounces > max_bounces: free_nodes_new[crossed] = inter - 1e-10 * scale * norms break else: # bouce node off the boundary free_nodes_new[crossed] -= 2 * norms * np.sum(res * norms, 1)[:, None] # check to see if the bounced node is now within the domain, # if not then iterations continue crossed = ~gm.contains(free_nodes_new, vert, smp) bounces += 1 free_nodes = free_nodes_new return free_nodes
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_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 _repel_bounce(free_nodes,vert,smp,rho, fix_nodes,itr,n,delta, max_bounces,bound_force): ''' nodes are repelled by eachother and bounce off boundaries ''' free_nodes = np.array(free_nodes,dtype=float,copy=True) # if bound_force then use the domain boundary as the force # boundary if bound_force: bound_vert = vert bound_smp = smp else: bound_vert = np.zeros((0,vert.shape[1]),dtype=float) bound_smp = np.zeros((0,vert.shape[1]),dtype=int) # this is used for the lengthscale of the domain scale = vert.ptp() # ensure that the number of nodes used to determine repulsion force # is less than or equal to the total number of nodes n = min(n,free_nodes.shape[0]+fix_nodes.shape[0]) for k in range(itr): # node positions after repulsion free_nodes_new = _repel_step(free_nodes,rho,fix_nodes, n,delta,bound_vert,bound_smp) # boolean array of nodes which are now outside the domain crossed = ~gm.contains(free_nodes_new,vert,smp) bounces = 0 while np.any(crossed): # point where nodes intersected the boundary inter = gm.intersection_point( free_nodes[crossed], free_nodes_new[crossed], vert,smp) # normal vector to intersection point norms = gm.intersection_normal( free_nodes[crossed], free_nodes_new[crossed], vert,smp) # distance that the node wanted to travel beyond the boundary res = free_nodes_new[crossed] - inter # move the previous node position to just within the boundary free_nodes[crossed] = inter - 1e-10*scale*norms # 3 is the number of bounces allowed if bounces > max_bounces: free_nodes_new[crossed] = inter - 1e-10*scale*norms break else: # bouce node off the boundary free_nodes_new[crossed] -= 2*norms*np.sum(res*norms,1)[:,None] # check to see if the bounced node is now within the domain, # if not then iterations continue crossed = ~gm.contains(free_nodes_new,vert,smp) bounces += 1 free_nodes = free_nodes_new return free_nodes
def _repel_stick(free_nodes,vert,smp,rho, fix_nodes,itr,n,delta, bound_force): ''' nodes are repelled by eachother and then become fixed when they hit a boundary ''' free_nodes = np.array(free_nodes,dtype=float,copy=True) # if bound_force then use the domain boundary as the force # boundary if bound_force: bound_vert = vert bound_smp = smp else: bound_vert = np.zeros((0,vert.shape[1]),dtype=float) bound_smp = np.zeros((0,vert.shape[1]),dtype=int) # Keeps track of whether nodes in the interior or boundary. -1 # indicates interior and >= 0 indicates boundary. If its on the # boundary then the number is the index of the simplex that the node # is on smpid = np.repeat(-1,free_nodes.shape[0]) # length scale of the domain scale = vert.ptp() # ensure that the number of nodes used to compute repulsion force is # less than or equal to the total number of nodes n = min(n,free_nodes.shape[0]+fix_nodes.shape[0]) for k in range(itr): # indices of all interior nodes interior, = (smpid==-1).nonzero() # indices of nodes associated with a simplex (i.e. nodes which # intersected a boundary boundary, = (smpid>=0).nonzero() # nodes which are stationary all_fix_nodes = np.vstack((fix_nodes,free_nodes[boundary])) # new position of free nodes free_nodes_new = np.array(free_nodes,copy=True) # shift positions of interior nodes free_nodes_new[interior] = _repel_step(free_nodes[interior], rho,all_fix_nodes,n,delta, bound_vert,bound_smp) # indices of free nodes which crossed a boundary crossed = ~gm.contains(free_nodes_new,vert,smp) # if a node intersected a boundary then associate it with a simplex smpid[crossed] = gm.intersection_index( free_nodes[crossed], free_nodes_new[crossed], vert,smp) # outward normal vector at intesection points norms = gm.intersection_normal( free_nodes[crossed], free_nodes_new[crossed], vert,smp) # intersection point for nodes which crossed a boundary inter = gm.intersection_point( free_nodes[crossed], free_nodes_new[crossed], vert,smp) # new position of nodes which crossed the boundary is just within # the intersection point free_nodes_new[crossed] = inter - 1e-10*scale*norms free_nodes = free_nodes_new return free_nodes,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
def _repel_stick(free_nodes, vert, smp, rho, fix_nodes, itr, n, delta, bound_force): """ nodes are repelled by eachother and then become fixed when they hit a boundary """ free_nodes = np.array(free_nodes, dtype=float, copy=True) # if bound_force then use the domain boundary as the force # boundary if bound_force: bound_vert = vert bound_smp = smp else: bound_vert = np.zeros((0, vert.shape[1]), dtype=float) bound_smp = np.zeros((0, vert.shape[1]), dtype=int) # Keeps track of whether nodes in the interior or boundary. -1 # indicates interior and >= 0 indicates boundary. If its on the # boundary then the number is the index of the simplex that the node # is on smpid = np.repeat(-1, free_nodes.shape[0]) # length scale of the domain scale = vert.ptp() # ensure that the number of nodes used to compute repulsion force is # less than or equal to the total number of nodes n = min(n, free_nodes.shape[0] + fix_nodes.shape[0]) for k in range(itr): # indices of all interior nodes interior, = (smpid == -1).nonzero() # indices of nodes associated with a simplex (i.e. nodes which # intersected a boundary boundary, = (smpid >= 0).nonzero() # nodes which are stationary all_fix_nodes = np.vstack((fix_nodes, free_nodes[boundary])) # new position of free nodes free_nodes_new = np.array(free_nodes, copy=True) # shift positions of interior nodes free_nodes_new[interior] = _repel_step( free_nodes[interior], rho, all_fix_nodes, n, delta, bound_vert, bound_smp ) # indices of free nodes which crossed a boundary crossed = ~gm.contains(free_nodes_new, vert, smp) # if a node intersected a boundary then associate it with a simplex smpid[crossed] = gm.intersection_index(free_nodes[crossed], free_nodes_new[crossed], vert, smp) # outward normal vector at intesection points norms = gm.intersection_normal(free_nodes[crossed], free_nodes_new[crossed], vert, smp) # intersection point for nodes which crossed a boundary inter = gm.intersection_point(free_nodes[crossed], free_nodes_new[crossed], vert, smp) # new position of nodes which crossed the boundary is just within # the intersection point free_nodes_new[crossed] = inter - 1e-10 * scale * norms free_nodes = free_nodes_new return free_nodes, smpid