def splitsphere(domain, particles, config): """Creates geometry and topology of spheres and sphere surface sub-sections. :param domain Domain: spatial domain for mesh. :param particles numpy.ndarray: particle coordinates and radii. :param config Config: configuration for mesh build. :return: list containing SpherePiece objects. :rtype: list. """ logger.info('Splitting input particles') # Create analytical representation of sphere sphere_pieces = [] for p in particles: sphere = Sphere(p[0], p[1:4], p[4], config) sphere.initialise_points() sphere_pieces += sphere.split(domain) # Generate point sets at sphere surfaces for sphere_piece in sphere_pieces: sphere_piece.construct() # Handle overlaps handle_overlaps(sphere_pieces, config) return sphere_pieces
def boundarypslg(domain, sphere_pieces, config): """Handles creation of high quality triangulations of the domain boundaries. :param domain Domain: spatial domain for mesh. :param sphere_pieces list: list of SpherePiece objects. :param config Config: configuration for mesh build. :return: list of BoundaryPLC objects for the triangulated boundaries. :rtype: list. """ logger.info('Triangulating domain boundaries') ds = config.segment_length boundary_PSLGs = build_boundary_PSLGs(domain, sphere_pieces, ds) area_constraints = AreaConstraints(domain, sphere_pieces, ds) return triangulate_PSLGs(boundary_PSLGs, area_constraints)
def build(domain, particles, config): """Handles building tetrahedral mesh topology and geometry based on input particle set and configuration. :param domain Domain: spatial domain for mesh. :param particles numpy.ndarray: particle coordinates and radii. :param config Config: configuration for mesh build. :return mesh: tetrahedral mesh. :rtype: MeshInfo. """ logger.info('Starting mesh build process') sphere_pieces = splitsphere(domain, particles, config) boundaries = boundarypslg(domain, sphere_pieces, config) mesh = build_tetmesh(domain, sphere_pieces, boundaries, config) logger.info('Completed mesh build') return mesh
def load_data(args): """Handles loading input data and configuration options. :param args argparse.Namespace: parsed command line arguments. :return: tuple containing Domain, numpy.ndarray of particle data, and Config. :rtype: tuple. :raises DataLoaderError: Indicates insufficient data provided. """ logger.info('Reading program inputs') config = read_config_file(args.config_file) particle_file = args.particle_file or config.particle_file if particle_file: if isinstance(particle_file, str): particle_file = open(particle_file, 'r') try: L, PBC, particles = read_particle_file(particle_file) finally: if not config.output_prefix: config.output_prefix = os.path.splitext( os.path.basename(particle_file.name))[0] config.output_prefix += ',a_%1.2e,s_%1.2e'\ % (config.tetgen_max_volume, config.segment_length) particle_file.close() else: single_mode_missing_required = [ not args.particle_center, not args.particle_radius, not args.domain_dimensions, not args.pbc, ] if any(single_mode_missing_required): raise DataLoaderError('Insufficient data provided to build mesh.') L = args.domain_dimensions PBC = args.pbc particles = np.array([[0] + args.particle_center + [args.particle_radius]]) if not config.output_prefix: config.output_prefix = './mesh' domain = Domain(L, PBC) particles = duplicate_particles(domain, particles, config) if not config.allow_overlaps: particles[:, 4] -= 0.001 * particles[:, 4].min() domain, particles = extend_domain(domain, particles, config.segment_length) return domain, particles, config
def redirect_tetgen_output(fname='./tet.log'): """Context manager to redirect stdout of TetGen subprocess to file `fname`.""" import ctypes, io, os, sys libc = ctypes.CDLL(None) c_stdout = ctypes.c_void_p.in_dll(libc, 'stdout') def _redirect_stdout(to_fd): libc.fflush(c_stdout) sys.stdout.close() os.dup2(to_fd, original_stdout_fd) sys.stdout = io.TextIOWrapper(os.fdopen(original_stdout_fd, 'wb')) def extract_stats(f): """Returns string containing key metrics of the constructed mesh.""" while True: l = f.readline() if 'Statistics:' in l.decode('ascii'): stats = [f.readline().decode('ascii') for i in range(11)] npoints, ntets, nfaces, nedges = [ int(sl.split()[-1]) for sl in stats[7:] ] return 'Built mesh with {} points, {} tetrahedra, {} faces, and {} edges'\ .format(npoints, ntets, nfaces, nedges) logger.info(' -> calling TetGen (writing log to {})'.format(fname)) original_stdout_fd = sys.stdout.fileno() saved_stdout_fd = os.dup(original_stdout_fd) try: tfile = open(fname, mode='w+b') _redirect_stdout(tfile.fileno()) yield _redirect_stdout(saved_stdout_fd) tfile.seek(0, io.SEEK_SET) logger.info(extract_stats(tfile)) tfile.close() finally: tfile.close() os.close(saved_stdout_fd)
def build_tetmesh(domain, sphere_pieces, boundaries, config): """Handles calling TetGen to construct the tetrahedral mesh. :param domain Domain: spatial domain for mesh. :param sphere_pieces list: list of SpherePiece objects. :param boundaries list: list of boundaryPLC objects. :param config Config: configuration for mesh build. :return mesh: tetrahedral mesh. :rtype: MeshInfo. """ logger.info('Building tetrahedral mesh') boundaries = duplicate_lower_boundaries(domain, boundaries) points = build_point_list(domain, sphere_pieces, boundaries) # Fix boundary points to exactly zero for i in range(3): points[(np.isclose(points[:, i], 0.), i)] = 0. facets, markers = build_facet_list(sphere_pieces, boundaries) holes = build_hole_list(sphere_pieces) rad_edge = config.tetgen_rad_edge_ratio min_angle = config.tetgen_min_angle max_volume = config.tetgen_max_volume options = tet.Options('pq{}/{}nzfennYCV'.format(rad_edge, min_angle)) options.quiet = False mesh = tet.MeshInfo() mesh.set_points(points) mesh.set_facets(facets.tolist(), markers=markers.tolist()) mesh.set_holes(holes) with redirect_tetgen_output(): return tet.build(mesh, options=options, verbose=True, max_volume=max_volume)
def output_mesh(mesh, config): """Outputs mesh in formats specified in config. :param mesh MeshInfo: tetrahedral mesh. :param config Config: configuration for mesh build. """ if not config.output_format: return logger.info('Outputting mesh in formats: {}'.format(', '.join( config.output_format))) if 'msh' in config.output_format: from mesh_sphere_packing.tetmesh import write_msh write_msh('%s.msh' % config.output_prefix, mesh) if 'multiflow' in config.output_format: from mesh_sphere_packing.tetmesh import write_multiflow write_multiflow('%s.h5' % config.output_prefix, mesh) if 'ply' in config.output_format: from mesh_sphere_packing.tetmesh import write_ply write_ply('%s.ply' % config.output_prefix, mesh) if 'poly' in config.output_format: from mesh_sphere_packing.tetmesh import write_poly write_poly('%s.poly' % config.output_prefix, mesh) if 'vtk' in config.output_format: mesh.write_vtk('%s.vtk' % config.output_prefix)
if 'poly' in config.output_format: from mesh_sphere_packing.tetmesh import write_poly write_poly('%s.poly' % config.output_prefix, mesh) if 'vtk' in config.output_format: mesh.write_vtk('%s.vtk' % config.output_prefix) def build(domain, particles, config): """Handles building tetrahedral mesh topology and geometry based on input particle set and configuration. :param domain Domain: spatial domain for mesh. :param particles numpy.ndarray: particle coordinates and radii. :param config Config: configuration for mesh build. :return mesh: tetrahedral mesh. :rtype: MeshInfo. """ logger.info('Starting mesh build process') sphere_pieces = splitsphere(domain, particles, config) boundaries = boundarypslg(domain, sphere_pieces, config) mesh = build_tetmesh(domain, sphere_pieces, boundaries, config) logger.info('Completed mesh build') return mesh if __name__ == '__main__': args = get_parser().parse_args() domain, particles, config = load_data(args) mesh = build(domain, particles, config) output_mesh(mesh, config) logger.info('Finished')
def build_point_list(domain, sphere_pieces, boundaries): """Constructs full list of vertices for all geometry in the domain without duplicates. Reindexes all topology after vertex removal. This step is expensive but necessary as duplicate vertices cause segfaults in TetGen. :param domain Domain: spatial domain for mesh. :param sphere_pieces list: list of SpherePiece objects. :param boundaries list: list of boundaryPLC objects. :return: array of all vertices without duplicates. :rtype: numpy.ndarray. """ logger.info(' -> building vertex list...') vcount = 0 piece_points = [] for points, tris in [(p.points, p.tris) for p in sphere_pieces]: piece_points.append(points) tris += vcount vcount += len(points) if len(piece_points): piece_points = np.vstack(piece_points) else: piece_points = np.empty((0, 3), dtype=np.float64) on_x_lower = np.isclose(piece_points[:, 0], 0.) on_y_lower = np.isclose(piece_points[:, 1], 0.) on_z_lower = np.isclose(piece_points[:, 2], 0.) on_x_upper = np.isclose(piece_points[:, 0], domain.L[0]) on_y_upper = np.isclose(piece_points[:, 1], domain.L[1]) on_z_upper = np.isclose(piece_points[:, 2], domain.L[2]) bpp_idx = np.where(on_x_lower | on_y_lower | on_z_lower | on_x_upper | on_y_upper | on_z_upper)[0] remap = {child: parent for child, parent in enumerate(bpp_idx)} vcount = len(bpp_idx) boundary_points = [] for b in boundaries: boundary_points.append(b.points) b.tris += vcount vcount += len(b.points) boundary_points = np.vstack([piece_points[bpp_idx]] + boundary_points) mask = np.full(len(boundary_points), True) # Find duplicated vertices tree = cKDTree(boundary_points) _dup = sorted(tree.query_pairs(TOL), key=lambda x: x[0]) dup = {} for k, v in _dup: if not v in dup: dup[v] = k del _dup # Remove duplicated vertices mask[list(dup.keys())] = False boundary_points = boundary_points[mask] # Reindex triangles vcount_bpp = len(bpp_idx) vcount_piece = len(piece_points) remap.update({ v + vcount_bpp: v + vcount_piece for v in range(len(boundary_points) - len(bpp_idx)) }) reindex = {old: new for new, old in enumerate(np.where(mask)[0])} reindex.update({k: reindex[v] for k, v in dup.items()}) del dup for b in boundaries: b.tris[:] = np.array([remap[reindex[v]] for v in b.tris.flatten()]).reshape(b.tris.shape) return np.vstack((piece_points, boundary_points[vcount_bpp:]))