Example #1
0
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)
Example #3
0
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
Example #4
0
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)
Example #7
0
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)
Example #8
0
    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:]))