def triangle_invert(self): """ pre-invert triangle support. this is useful for computing projections on triangles adjoint is fine; we normalize coords anyway """ tri_coords = util.gather(self.topology.FV, self.primal) self.inverted_triangle = util.adjoint(tri_coords)
def metric(self): """ calc metric properties and hodges; nicely vectorized """ topology = self.topology #metrics MP0 = np.ones (topology.P0) MP1 = np.zeros(topology.P1) MP2 = np.zeros(topology.P2) MD0 = np.ones (topology.D0) MD1 = np.zeros(topology.D1) MD2 = np.zeros(topology.D2) #precomputations EVP = util.gather(topology.EVi, self.primal) FEVP = util.gather(topology.FEi, EVP) #[faces, e3, v2, c3] FEM = util.normalize(FEVP.sum(axis=2)) FEV = util.gather(topology.FEi, topology.EVi) #calculate areas; devectorization over e makes things a little more elegant, by avoiding superfluous stacking for e in xrange(3): areas = triangle_area_from_corners(FEVP[:,e,0,:], FEVP[:,e,1,:], self.dual) MP2 += areas #add contribution to primal face util.scatter( #add contributions divided over left and right dual face FEV[:,e,:], #get both verts of each edge np.repeat(areas/2, 2), #half of domain area for both verts MD2) #calc edge lengths MP1 += edge_length(EVP[:,0,:], EVP[:,1,:]) for e in xrange(3): util.scatter( topology.FEi[:,e], edge_length(FEM[:,e,:], self.dual), MD1) #hodge operators self.D2P0 = MD2 / MP0 self.P0D2 = MP0 / MD2 self.D1P1 = MD1 / MP1 self.P1D1 = MP1 / MD1 self.D0P2 = MD0 / MP2 self.P2D0 = MP2 / MD0
def dual_position(self): """calc dual coords from primal; interestingly, this is idential to computing a triangle normal""" #calc direction orthogonal to normal of intesecting plane diff = self.topology.T10 * self.primal #collect these on per-tri basis, including weights, so ordering is correct tri_edge = util.gather(self.topology.FEi, diff) * self.topology.FEs[:, :, None] #for above, could also do cyclical diff on grab(FV, primal) #dual vert les where three mid edge planes intesect self.dual = util.normalize(-util.null(tri_edge))
def triangulate(points, curve): """ return a triangulation of the pointset points, while being constrained by the boundary dicated by curve """ #test curve for self-intersection print 'testing curve for self-intersection' curve.self_intersect() #trim the pointset, to eliminate points co-linear with the cutting curve print 'trimming dataset' diff = np.diff(curve.vertices[curve.faces], axis=1)[:,0,:] length = np.linalg.norm(diff, axis=1) points = curve.trim(points, length.mean()/4) #refine curve iteratively. new points may both obsolete or require novel insertions themselves #so only do the most pressing ones first, then iterate to convergence while True: newcurve = curve.refine(points) if len(newcurve.vertices)==len(curve.vertices): break print 'curve refined' curve = newcurve """ we use the nifty property, that a convex hull of a sphere equals a delauney triangulation of its surface if we have cleverly refined our boundary curve, this trinagulation should also be 'constrained', in the sense of respecting that original boundary curve this is the most computationally expensive part of this function, but we should be done in a minute or so qhull performance; need 51 sec and 2.7gb for 4M points that corresponds to an icosahedron with level 8 subdivision; not too bad editor is very unresponsive at this level anyway """ print 'triangulating' allpoints = np.concatenate((curve.vertices, points)) #include origin; facilitates clipping hull = scipy.spatial.ConvexHull(util.normalize(allpoints)) triangles = hull.simplices #order faces coming from the convex hull print 'ordering faces' FP = util.gather(triangles, allpoints) mid = FP.sum(axis=1) normal = util.normals(FP) sign = util.dot(normal, mid) > 0 triangles = np.where(sign[:,None], triangles[:,::+1], triangles[:,::-1]) mesh = Mesh(allpoints, triangles) assert mesh.is_orientated() return mesh, curve
def subdivide_position(self, position): """calc primal coords from parent""" #one child for each parent vertex_vertex = position #each new vert lies at midpoint edge_vertex = util.normalize(self.edge_mid*vertex_vertex) #ordering convention is vertex-vertex + edge-vertex position = np.vstack((vertex_vertex, edge_vertex)) #calc subdivision planes central = util.gather(self.FEi, edge_vertex) planes = util.adjoint(central) return position, planes
def refine(self, points): """ refine the contour such as to maintain it as a constrained boundary under triangulation using a convex hull this is really the crux of the method pursued in this module we need to 'shield off' any points that lie so close to the edge such as to threaten our constrained boundary by adding a split at the projection of the point on the line, for all vertices within the swept circle of the edge, we may guarantee that a subsequent convex hull of the sphere respects our original boundary """ allpoints = np.vstack((self.vertices, points)) tree = KDTree(allpoints) cp = util.gather(self.faces, self.vertices) normal = util.normalize(np.cross(cp[:,0], cp[:,1])) mid = util.normalize(cp.sum(axis=1)) diff = np.diff(cp, axis=1)[:,0,:] radius = np.linalg.norm(diff, axis=1) / 2 def insertion_point(e, c): """calculate insertion point""" coeff = np.dot( np.linalg.pinv(cp[e].T), allpoints[c]) coeff = coeff / coeff.sum() return coeff[0], np.dot(cp[e].T, coeff) #build new curves _curve_p = [c for c in self.vertices] _curve_idx = [] for e,(m,r,cidx) in enumerate(izip( mid, radius, self.faces)): try: d,ip = min( #codepath for use in iterative scheme; only insert the most balanced split; probably makes more awkward ones obsolete anyway [insertion_point(e,v) for v in tree.query_ball_point(m, r) if not v in cidx], key=lambda x:(x[0]-0.5)**2) #sort on distance from midpoint nidx = len(_curve_p) _curve_idx.append((cidx[0], nidx)) #attach on both ends _curve_idx.append((nidx, cidx[1])) _curve_p.append(ip) #append insertion point except: _curve_idx.append(cidx) #if edge is not split, just copy it return Curve(_curve_p, _curve_idx)
def save_STL_complete(complex, radius, filename): """ split this as mesh_from_datamodel save a mesh to binary STL format the number of triangles grows quickly shapeway and solidworks tap out at a mere 1M and 20k triangles respectively... or 100k for sw surface """ data = np.empty((complex.group.index, complex.group.order, complex.topology.P2, 3, 3), np.float) #essence here is in exact transformations given by the basis trnasforms. this gives a guaranteed leak-free mesh PP = complex.geometry.decomposed FV = complex.topology.FV for i,B in enumerate(complex.group.basis): for t, b in enumerate(B.reshape(-1,3,3)): b = util.normalize(b.T).T #now every row is a normalized vertex P = np.dot(b, PP.T).T * radius[:,i][:, None] #go from decomposed coords to local coordinate system fv = FV[:,::np.sign(np.linalg.det(b))] data[i,t] = util.gather(fv, P) save_STL(filename, data.reshape(-1,3,3))
def self_intersect(self): """ test curve of arc-segments for intersection raises exception in case of intersection alternatively, we might resolve intersections by point insertion but this is unlikely to have any practical utility, and more likely to be annoying """ vertices = self.vertices faces = self.faces tree = KDTree(vertices) # curve points per edge, [n, 2, 3] cp = util.gather(faces, vertices) # normal rotating end unto start normal = util.normalize(np.cross(cp[:,0], cp[:,1])) # midpoints of edges; [n, 3] mid = util.normalize(cp.sum(axis=1)) # vector from end to start, [n, 3] diff = np.diff(cp, axis=1)[:,0,:] # radius of sphere needed to contain edge, [n] radius = np.linalg.norm(diff, axis=1) / 2 * 1.01 # FIXME: this can be vectorized by adapting pinv projector = [np.linalg.pinv(q) for q in np.swapaxes(cp, 1, 2)] # incident[vertex_index] gives a list of all indicent edge indices incident = npi.group_by(faces.flatten(), np.arange(faces.size)) def intersect(i,j): """test if spherical line segments intersect. bretty elegant""" intersection = np.cross(normal[i], normal[j]) #intersection direction of two great circles; sign may go either way though! return all(np.prod(np.dot(projector[e], intersection)) > 0 for e in (i,j)) #this direction must lie within the cone spanned by both sets of endpoints for ei,(p,r,cidx) in enumerate(izip(mid, radius, faces)): V = [v for v in tree.query_ball_point(p, r) if v not in cidx] edges = np.unique([ej for v in V for ej in incident[v]]) for ej in edges: if len(np.intersect1d(faces[ei], faces[ej])) == 0: #does not count if edges touch if intersect(ei, ej): raise Exception('The boundary curves intersect. Check your geometry and try again')
def transfer_operators(self): """ construct metric transfer operators, as required for dual-transfer on pseudo-regular grid we need to calculate overlap between fine and coarse dual domains the crux here is in the treatment of the central triangle holy shitballs this is a dense function. there is some cleanup i could do, but this is also simply some insanely hardcore shit algebraicly optimal multigrid transfer operators on a pseudo-regular grid, here we come """ coarse = self fine = self.child all_tris = np.arange(fine.topology.P2).reshape(coarse.topology.P2, 4) central_tris = all_tris[:,0] corner_tris = all_tris[:,1:] #first, compute contribution to transfer matrices from the central refined triangle coarse_dual = coarse.dual fine_dual = fine.dual[central_tris] face_edge_mid = util.gather(fine.topology.FV[0::4], fine.primal) fine_edge_normal = [np.cross(face_edge_mid[:,i-2,:], face_edge_mid[:,i-1,:]) for i in xrange(3)] fine_edge_mid = [(face_edge_mid[:,i-2,:] + face_edge_mid[:,i-1,:])/2 for i in xrange(3)] fine_edge_dual = [np.cross(fine_edge_mid[i], fine_edge_normal[i]) for i in xrange(3)] fine_edge_normal = np.array(fine_edge_normal) fine_edge_mid = np.array(fine_edge_mid) fine_edge_dual = np.array(fine_edge_dual) coarse_areas = [triangle_area_from_corners(coarse_dual, face_edge_mid[:,i-2,:], face_edge_mid[:,i-1,:]) for i in xrange(3)] fine_areas = [triangle_area_from_corners(fine_dual , face_edge_mid[:,i-2,:], face_edge_mid[:,i-1,:]) for i in xrange(3)] fine_areas = [(fine_areas[i-2]+fine_areas[i-1])/2 for i in xrange(3)] coarse_areas = np.array(coarse_areas) fine_areas = np.array(fine_areas) #normal of edge midpoints to coarse dual interior_normal = np.array([np.cross(face_edge_mid[:,i,:], coarse_dual) for i in xrange(3)]) #the 0-3 index of the overlapping domains #biggest of the subtris formed with the coarse dual vertex seems to work; but cant prove why it is so... touching = np.argmax(coarse_areas, axis=0) ## print touching ## print fine_areas ## print coarse_areas #indexing arrays I = np.arange(len(touching)) m = touching #middle pair l = touching-1 #left-rotated pair r = touching-2 #right-rotated pair #compute sliver triangles sliver_r = triangle_area_from_normals( +fine_edge_normal[l, I], +fine_edge_dual [l, I], +interior_normal [r, I]) sliver_l = triangle_area_from_normals( +fine_edge_normal[r, I], -fine_edge_dual [r, I], -interior_normal [l, I]) ## print 'slivers' ## print sliver_l ## print sliver_r assert(np.all(sliver_l>-1e-10)) assert(np.all(sliver_r>-1e-10)) #assemble area contributions of the middle triangle areas = np.empty((len(touching),3,3)) #coarsetris x coarsevert x finevert #the non-overlapping parts areas[I,l,l] = 0 areas[I,r,r] = 0 #triangular slivers disjoint from the m,m intersection areas[I,r,l] = sliver_l areas[I,l,r] = sliver_r #subset of coarse tri bounding sliver areas[I,r,m] = coarse_areas[r,I] - sliver_l areas[I,l,m] = coarse_areas[l,I] - sliver_r #subset of fine tri bounding sliver areas[I,m,l] = fine_areas[l,I] - sliver_l areas[I,m,r] = fine_areas[r,I] - sliver_r #square middle region; may compute as fine or caorse minus its flanking parts areas[I,m,m] = coarse_areas[m,I] - areas[I,m,l] - areas[I,m,r] #we may get numerical negativity for 2x2x2 symmetry, with equilateral fundemantal domain, #or high subdivision levels. or is error at high subdivision due to failing of touching logic? assert(np.all(areas > -1e-10)) #areas maps between coarse vertices and fine edge vertices. #add mapping for coarse to fine vertices too #need to grab coarsetri x 3coarsevert x 3finevert arrays of coarse and fine vertices fine_vertex = np.repeat( fine .topology.FV[0::4, None, :], 3, axis=1) coarse_vertex = np.repeat( coarse.topology.FV[: , : , None], 3, axis=2) def coo_matrix(data, row, col): """construct a coo_matrix from data and index arrays""" return util.coo_matrix( (data.ravel(),(row.ravel(), col.ravel())), shape=(coarse.topology.D2, fine.topology.D2)) center_transfer = coo_matrix(areas, coarse_vertex, fine_vertex) #add corner triangle contributions; this is relatively easy #coarsetri x 3coarsevert x 3finevert corner_vertex = util.gather(corner_tris, fine.topology.FV) corner_dual = util.gather(corner_tris, fine.dual) corner_primal = util.gather(corner_vertex, fine.primal) #coarsetri x 3coarsevert x 3finevert corner_areas = triangle_areas_around_center(corner_dual, corner_primal) #construct matrix corner_transfer = coo_matrix(corner_areas, coarse_vertex, corner_vertex) self.transfer = util.csr_matrix(center_transfer + corner_transfer) #calc normalizations self.coarse_area = self.transfer * np.ones(fine .topology.D2) self.fine_area = self.transfer.T * np.ones(coarse.topology.D2) self.f = np.sqrt( self.fine_area)[:,None] self.c = np.sqrt( self.coarse_area)[:,None] #test for consistency with metric calculations assert(np.allclose(self.coarse_area, coarse.D2P0, 1e-10)) assert(np.allclose(self.fine_area , fine .D2P0, 1e-10))