def trim(self, points, radius): """ remove points too close to the cut curve. they dont add anything, and only lead to awkward faces """ #some precomputations tree = KDTree(points) cp = self.vertices[self.faces] normal = util.normalize(np.cross(cp[:,0], cp[:,1])) mid = util.normalize(cp.sum(axis=1)) diff = np.diff(cp, axis=1)[:,0,:] edge_radius = np.sqrt(util.dot(diff, diff)/4 + radius**2) index = np.ones(len(points), np.bool) #eliminate near edges def near_edge(e, p): return np.abs(np.dot(points[p]-mid[e], normal[e])) < radius for i,(p,r) in enumerate(izip(mid, edge_radius)): coarse = tree.query_ball_point(p, r) index[[c for c in coarse if near_edge(i, c)]] = 0 #eliminate near points for p in self.vertices: coarse = tree.query_ball_point(p, radius) index[coarse] = 0 return points[index]
def compute_angles(self): """compute angles for each triangle-vertex""" edges = self.edges().reshape(-1, 3, 2) vecs = np.diff(self.vertices[edges], axis=2)[:, :, 0] vecs = util.normalize(vecs) angles = np.arccos(-util.dot(vecs[:, [1, 2, 0]], vecs[:, [2, 0, 1]])) assert np.allclose(angles.sum(axis=1), np.pi, rtol=1e-3) return angles
def compute_divergence(self, field): """compute divergence of vector field at faces at vertices""" edges = self.edges().reshape(-1, 3, 2) sorted_edges = np.sort(edges, axis=-1) vecs = np.diff(self.vertices[sorted_edges], axis=2)[:, :, 0, :] inner = util.dot(vecs, field[:, None, :]) cotan = 1 / np.tan(self.compute_angles()) vertex_incidence = self.compute_vertex_incidence() return vertex_incidence.T * self.remap_edges(inner * cotan) / 2
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 edge_length(*edge): """compute spherical edge length. Parameters ---------- edge : 2 x ndarray, [n, 3], float arc segments described by their start and end position Returns ------- lengths: ndarray, [n], float length along the unit sphere of each segment """ return np.arccos(util.dot(*edge))
def triangle_area_from_normals(*edge_planes): """compute spherical area from triplet of great circles Parameters ---------- edge_planes : 3 x ndarray, [n, 3], float edge normal vectors of great circles Returns ------- areas : ndarray, [n], float spherical area enclosed by the input planes """ edge_planes = [util.normalize(ep) for ep in edge_planes ] angles = [util.dot(edge_planes[p-2], edge_planes[p-1]) for p in xrange(3)] #a3 x [faces, c3] areas = sum(np.arccos(-a) for a in angles) - np.pi #faces return areas
def solve_poisson_rec(hierarchy, forcefield, edges, coefficients): """ poisson solution on a single height level """ complex = hierarchy[-1] #decide on recursion if len(hierarchy) > 4: coefficients, heightfield = solve_poisson_rec( hierarchy[:-1], hierarchy[-2].restrict_d2(forcefield), edges, coefficients ) def smooth(x): width = 0.005 return multigrid.diffuse(hierarchy, x, width**2) def height_from_force(force): return multigrid.solve_poisson(hierarchy, force) #external force field offset = 0 ## external_force = complex.D2P0 * (forcefield + offset) external_force = forcefield if len(edges)==0: #? why center is p0? f = complex.P0D2 * external_force f = f - f.mean() return None, height_from_force(complex.D2P0 * f) + 1 else: #take midpoints of edges, for correct boundary handling edges = np.vstack(edges) radius = np.sqrt(util.dot(edges, edges)) #compute target radius at each point print 'outer unknowns' print len(radius) #pick edges against the sphere. give each edge its own mapping mapping = brushes.Mapping(hierarchy, edges) def sample_residual(height): """maps p0 height form to an absolute residual vector""" return radius - mapping.sample(height) if coefficients is None: #init coefs as average of force net_external_force = complex.deboundify(external_force).sum() base_coefficients = np.ones_like(radius) / len(radius) * net_external_force else: base_coefficients = coefficients print 'net force' print base_coefficients.sum() if True: force_curve = smooth(mapping.inject(base_coefficients)) force = force_curve - external_force else: #if no boundary conditions, act only on homogenous part force = external_force.mean() - external_force base_height = height_from_force(force) height_dirs = [] coeff_dirs = [] height = base_height + 0 coefficients = base_coefficients + 0 #this substraction is key; if we search in homogenous subspace, need to translate rhs there too! homogenous_radius = radius - mapping.sample(base_height) def find_homogenous_minimizer(basis): """ find the linear combination of height basis vectors, such that sampled_basis dot coeff ~= radius perform fit modulo ones vector. sampled basis vectors need no sum to zero, but we dont want to fit this component """ B = np.vstack([mapping.sample(b) for b in basis]+[np.ones_like(radius)]) S = np.dot(B, B.T) r = np.dot(B, homogenous_radius) c = np.linalg.lstsq(S, r)[0] ## print c return lambda I: sum(i*q for i,q in izip(I, c)) ## for i in range(18-len(hierarchy)): #iterate until convergence for i in range(10): #iterate until convergence print i #scale curves with coef estimates as preconditioner? coeff_dir = sample_residual(height) #/ scaling coeff_dir = coeff_dir - coeff_dir.mean() #balance forces; move only in space of valid coefficients print 'outer convergence', np.linalg.norm( coeff_dir) force_dir = smooth(mapping.inject(coeff_dir)) height_dir = height_from_force(force_dir) height_dirs.append(height_dir) coeff_dirs .append(coeff_dir) minimizer = find_homogenous_minimizer(height_dirs) height = base_height + minimizer(height_dirs) coefficients = base_coefficients + minimizer(coeff_dirs) res = sample_residual(height) height = height + res.mean() ## print len(hierarchy) ## print coefficients if False: #visualize force field force -= force.min() force /= force.max() return force*0.1+1 return coefficients, height
def solve_poisson(datamodel): """ take a datamodel and return a heightfield that conforms to the immersed boundary conditions inner iteration is the mg solve from force to height outer iteration is a krylov subspace method is has several custom steps, to cope with the left and right nullspace problem """ hierachy = datamodel.hierarchy complex = hierachy[-1] def smooth(x): width = 0.005 return multigrid.diffuse(hierachy, x, width**2) def height_from_force(force): ## return multigrid.solve_poisson(hierachy, force) return complex.P0D2 * multigrid.solve_poisson_full(hierachy, force) #external force field offset = 0 scaling = -1 external_force = complex.D2P0 * (datamodel.forcefield * scaling + offset) ## def map_edge(edge): ## points = edge.instantiate()[0,0] ## points = (points[1:]+points[:-1])/2 ## radius = np.sqrt(util.dot(points, points)) #compute target radius at each point ## mapping = brushes.Mapping(hierachy, points) ## ## edges = [map_edge(edge) for edge in datamodel.edges if edge.driving] #instantiate all curves; pick first occurance edges = [edge.instantiate()[0,0] for edge in datamodel.edges if edge.driving] if len(edges)==0: f = complex.P0D2 * external_force f = f - f.mean() return height_from_force(complex.D2P0 * f) + 1 #take midpoints of edges, for correct boundary handling edges = [(edge[1:]+edge[:-1])/2 for edge in edges] edges = np.vstack(edges) radius = np.sqrt(util.dot(edges, edges)) #compute target radius at each point print 'outer unknowns' print len(radius) #pick edges against the sphere. give each edge its own mapping mapping = brushes.Mapping(hierachy, edges) #preconditioner. less response in denser regions. somehow, this is a complete disaster ## scaling = mapping.sample(smooth(mapping.inject(np.ones_like(radius)))) #### scaling = np.sqrt(scaling) ## print 'scaling' ## print scaling ## print scaling.min(), scaling.max() def sample_residual(height): """maps p0 height form to an absolute residual vector""" return radius - mapping.sample(height) #init coefs as average of force net_external_force = complex.deboundify(external_force).sum() base_coefficients = np.ones_like(radius) / len(radius) * net_external_force if True: force_curve = smooth(mapping.inject(base_coefficients)) force = force_curve - external_force else: #if no boundary conditions, act only on homogenous part force = external_force.mean() - external_force base_height = height_from_force(force) height_dirs = [] coeff_dirs = [] height = base_height + 0 coefficients = base_coefficients + 0 #this substraction is key; if we search in homogenous subspace, need to translate rhs there too! homogenous_radius = radius - mapping.sample(base_height) def find_homogenous_minimizer(basis): """ find the linear combination of height basis vectors, such that sampled_basis dot coeff ~= radius perform fit modulo ones vector. sampled basis vectors need no sum to zero, but we dont want to fit this component """ B = np.vstack([mapping.sample(b) for b in basis]+[np.ones_like(radius)]) S = np.dot(B, B.T) r = np.dot(B, homogenous_radius) ## c = np.linalg.lstsq(S, r)[0] c = np.linalg.solve(S, r) ## print c return lambda I: sum(i*q for i,q in izip(I, c)) for i in range(15): #iterate until convergence print i #scale curves with coef estimates as preconditioner? coeff_dir = sample_residual(height) #/ scaling coeff_dir = coeff_dir - coeff_dir.mean() #balance forces; move only in space of valid coefficients print 'outer convergence' print np.linalg.norm( coeff_dir) force_dir = smooth(mapping.inject(coeff_dir)) height_dir = height_from_force(force_dir) height_dirs.append(height_dir) coeff_dirs .append(coeff_dir) minimizer = find_homogenous_minimizer(height_dirs) height = base_height + minimizer(height_dirs) coefficients = base_coefficients + minimizer(coeff_dirs) res = sample_residual(height) height = height + res.mean() print coefficients if False: #visualize force field force -= force.min() force /= force.max() return force*0.1+1 return height