Example #1
0
 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)
Example #2
0
    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
Example #3
0
 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))
Example #4
0
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
Example #5
0
    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
Example #6
0
    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)
Example #7
0
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))
Example #8
0
    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')
Example #9
0
    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))