Source code for pyvcad_metamaterials.faces

"""Map-first native face, crystallographic plate, and honeycomb builders."""

# STD
import math

# OpenVCAD
import pyvcad as pv

# Local
from .fields import _coerce_attribute
from .maps import _as_maps
from .tessellations import tessellation
from .unit_cells import _HONEYCOMB_EDGE_INDICES, _honeycomb_points


FACE_LATTICE_NAMES = (
    "plate",
    "simple_cubic_plate",
    "body_centered_cubic_plate",
    "face_centered_cubic_plate",
    "cubic_octet_plate",
    "hexagonal_honeycomb",
    "reentrant_honeycomb",
    "square_honeycomb",
    "rotated_square_honeycomb",
    "triangular_honeycomb",
    "rotated_triangular_honeycomb",
)

_CUBE_VERTICES = (
    (0.0, 0.0, 0.0),
    (1.0, 0.0, 0.0),
    (0.0, 1.0, 0.0),
    (1.0, 1.0, 0.0),
    (0.0, 0.0, 1.0),
    (1.0, 0.0, 1.0),
    (0.0, 1.0, 1.0),
    (1.0, 1.0, 1.0),
)
_CUBE_EDGES = (
    (0, 1), (1, 3), (3, 2), (2, 0),
    (4, 5), (5, 7), (7, 6), (6, 4),
    (0, 4), (1, 5), (2, 6), (3, 7),
)
_PLATE_FAMILIES = {
    "simple_cubic_plate": (
        ((1, 0, 0), (0, 1, 0), (0, 0, 1)),
        (0.5, 0.5, 0.5),
        100,
    ),
    "body_centered_cubic_plate": (
        (
            (1, 1, 0), (1, -1, 0),
            (1, 0, 1), (1, 0, -1),
            (0, 1, 1), (0, 1, -1),
        ),
        (0.125, 0.25, 0.375),
        200,
    ),
    "face_centered_cubic_plate": (
        ((1, 1, 1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)),
        (0.5, 0.5, 0.5),
        300,
    ),
}


def _dot(first, second):
    return sum(a * b for a, b in zip(first, second))


def _subtract(first, second):
    return tuple(a - b for a, b in zip(first, second))


def _cross(first, second):
    return (
        first[1] * second[2] - first[2] * second[1],
        first[2] * second[0] - first[0] * second[2],
        first[0] * second[1] - first[1] * second[0],
    )


def _normalize(vector):
    length = math.sqrt(_dot(vector, vector))
    return tuple(value / length for value in vector)


def _append_unique(points, point, tolerance=1e-9):
    for existing in points:
        if all(abs(a - b) <= tolerance for a, b in zip(existing, point)):
            return
    points.append(point)


def _plane_polygon(normal, offset):
    points = []
    tolerance = 1e-10
    for first_index, second_index in _CUBE_EDGES:
        first = _CUBE_VERTICES[first_index]
        second = _CUBE_VERTICES[second_index]
        first_distance = _dot(normal, first) - offset
        second_distance = _dot(normal, second) - offset
        if abs(first_distance) <= tolerance:
            _append_unique(points, first)
        if abs(second_distance) <= tolerance:
            _append_unique(points, second)
        if first_distance * second_distance < -(tolerance ** 2):
            fraction = first_distance / (first_distance - second_distance)
            point = tuple(
                first[axis] + fraction * (second[axis] - first[axis])
                for axis in range(3)
            )
            _append_unique(points, point)
    if len(points) < 3:
        return []

    center = tuple(
        sum(point[axis] for point in points) / len(points)
        for axis in range(3)
    )
    unit_normal = _normalize(normal)
    reference = min(
        ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
        key=lambda axis: abs(_dot(axis, unit_normal)),
    )
    tangent = _normalize(_cross(reference, unit_normal))
    bitangent = _cross(unit_normal, tangent)
    return sorted(
        points,
        key=lambda point: math.atan2(
            _dot(_subtract(point, center), bitangent),
            _dot(_subtract(point, center), tangent),
        ),
    )


def _plate_family_data(name):
    normals, translation, tag_base = _PLATE_FAMILIES[name]
    vertices = []
    faces = []
    face_tags = []
    orientation_tags = []
    for orientation, normal in enumerate(normals):
        minimum = min(_dot(normal, point) for point in _CUBE_VERTICES)
        maximum = max(_dot(normal, point) for point in _CUBE_VERTICES)
        phase = _dot(normal, translation) % 1.0
        first_level = math.floor(minimum - phase) - 1
        last_level = math.ceil(maximum - phase) + 1
        for level in range(first_level, last_level + 1):
            polygon = _plane_polygon(normal, level + phase)
            if len(polygon) < 3:
                continue
            start = len(vertices)
            vertices.extend(polygon)
            for index in range(1, len(polygon) - 1):
                faces.append((start, start + index, start + index + 1))
                face_tags.append(tag_base + orientation)
                orientation_tags.append(orientation)
    return vertices, faces, face_tags, orientation_tags


def _combine_face_data(first, second):
    first_vertices, first_faces, first_tags, first_orientations = first
    second_vertices, second_faces, second_tags, second_orientations = second
    offset = len(first_vertices)
    return (
        first_vertices + second_vertices,
        first_faces + [
            tuple(index + offset for index in face) for face in second_faces
        ],
        first_tags + second_tags,
        first_orientations + second_orientations,
    )


def _face_cell(data):
    vertices, faces, face_tags, orientation_tags = data
    return pv.FaceUnitCell(
        [pv.Vec3(*point) for point in vertices],
        faces,
        face_tags=face_tags,
        orientation_tags=orientation_tags,
    )


[docs] def simple_cubic_plate_cell(): """Return the three orthogonal {100} plate families.""" return _face_cell(_plate_family_data("simple_cubic_plate"))
[docs] def body_centered_cubic_plate_cell(): """Return the six {110} plate families associated with a BCC crystal.""" return _face_cell(_plate_family_data("body_centered_cubic_plate"))
[docs] def face_centered_cubic_plate_cell(): """Return the four {111} plate families, also called an octet plate cell.""" return _face_cell(_plate_family_data("face_centered_cubic_plate"))
[docs] def cubic_octet_plate_cell(): """Return the combined simple-cubic and face-centered-cubic plate cell.""" return _face_cell( _combine_face_data( _plate_family_data("simple_cubic_plate"), _plate_family_data("face_centered_cubic_plate"), ) )
def _honeycomb_face_cell(points): vertices = [] indices = {} faces = [] def vertex(point): if point not in indices: indices[point] = len(vertices) vertices.append(pv.Vec3(*point)) return indices[point] for start, end in _HONEYCOMB_EDGE_INDICES: first = points[start] second = points[end] faces.append(( vertex((first[0], first[1], 0.0)), vertex((second[0], second[1], 0.0)), vertex((second[0], second[1], 1.0)), vertex((first[0], first[1], 1.0)), )) return pv.FaceUnitCell( vertices, faces, face_tags=list(range(len(faces))), orientation_tags=list(range(len(faces))), )
[docs] def hexagonal_honeycomb_cell(): """Return an extruded regular-hexagonal wall cell.""" return _honeycomb_face_cell(_honeycomb_points())
[docs] def reentrant_honeycomb_cell(angle_degrees=-30.0, rib_ratio=2.0): """Return an extruded re-entrant wall cell. ``rib_ratio`` is h/l. The physical U/V pitch ratio required to preserve the requested angle is returned by ``reentrant_hex_prism_reference_aspect_ratio``. """ return _honeycomb_face_cell( _honeycomb_points( reentrant=True, angle_degrees=angle_degrees, rib_ratio=rib_ratio, ) )
[docs] def square_honeycomb_cell(): """Return square honeycomb walls: the side faces of ``tessellation("cube")``. Walls stand on the U and V cell faces and run along W (the cubic plate cell without its W-normal plates). """ return tessellation("cube").face_cell(face_tags="side")
[docs] def rotated_square_honeycomb_cell(): """Return square honeycomb walls turned 45 degrees about W: walls along both cell diagonals in plan.""" return tessellation("rotated_square_prism").face_cell(face_tags="side")
[docs] def triangular_honeycomb_cell(): """Return triangular honeycomb walls with one wall family along U. Keep the U:V pitch at 1:sqrt(3) (``reference_cell_size``) for equilateral triangles. """ return tessellation("triangular_prism").face_cell(face_tags="side")
[docs] def rotated_triangular_honeycomb_cell(): """Return triangular honeycomb walls with one wall family along V (U:V = sqrt(3):1).""" return tessellation("rotated_triangular_prism").face_cell(face_tags="side")
FACE_UNIT_CELLS = { "simple_cubic_plate": simple_cubic_plate_cell, "body_centered_cubic_plate": body_centered_cubic_plate_cell, "face_centered_cubic_plate": face_centered_cubic_plate_cell, "cubic_octet_plate": cubic_octet_plate_cell, "hexagonal_honeycomb": hexagonal_honeycomb_cell, "reentrant_honeycomb": reentrant_honeycomb_cell, "square_honeycomb": square_honeycomb_cell, "rotated_square_honeycomb": rotated_square_honeycomb_cell, "triangular_honeycomb": triangular_honeycomb_cell, "rotated_triangular_honeycomb": rotated_triangular_honeycomb_cell, }
[docs] def face_unit_cell(name): """Return a standard static face cell by name. Args: name (str): One of :data:`FACE_LATTICE_NAMES`. ``"plate"`` returns a flat ``pv.PlateUnitCell`` (eccentricity 1.0); the other names call the matching ``*_cell`` builder with its defaults. Returns: pv.FaceUnitCell: The unit cell, ready for :func:`face_lattice`. Raises: ValueError: If the name is unknown. """ if name == "plate": return pv.PlateUnitCell(1.0) if name not in FACE_UNIT_CELLS: raise ValueError("Unknown face unit cell '{}'. Available: {}".format( name, ", ".join(FACE_LATTICE_NAMES))) return FACE_UNIT_CELLS[name]()
[docs] def face_lattice( cell_map, unit_cell, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Tile a standard, static, or native parametric face cell through a CellMap. Each face of the cell is a mid-surface that is mapped through the cell map and thickened into a wall. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. unit_cell (str | pv.FaceUnitCell): A name from :data:`FACE_LATTICE_NAMES`, or a ``pv.FaceUnitCell`` such as one from a ``*_cell`` builder or :meth:`PeriodicTessellation.face_cell`. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Raises: ValueError: If ``unit_cell`` is an unknown name. TypeError: If ``unit_cell`` is neither a name nor a ``pv.FaceUnitCell``. Example: >>> node = mm.face_lattice(cell_map, "hexagonal_honeycomb", wall_thickness=0.6) """ if isinstance(unit_cell, str): unit_cell = face_unit_cell(unit_cell) if not isinstance(unit_cell, pv.FaceUnitCell): raise TypeError("unit_cell must be a face-cell name or pv.FaceUnitCell") thickness_field = _coerce_attribute(wall_thickness) maps = _as_maps(cell_map) nodes = [ pv.MappedFaceLattice( item, unit_cell, thickness_field, surface_mode=surface_mode, surface_tolerance=float(surface_tolerance), ) for item in maps ] return nodes[0] if len(nodes) == 1 else pv.MappedStructureCollection(maps, nodes)
[docs] def plate_lattice( cell_map, wall_thickness=1.0, eccentricity=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build the period-two folded plate lattice with legacy eccentricity semantics. The cell is a ``pv.PlateUnitCell``: plates that fold between neighbouring cells, so the pattern repeats every two cells. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. eccentricity (float | pv.FloatAttribute | callable): Fold eccentricity, sampled at the logical cell centres. ``1.0`` (default) is flat; values must be positive. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.plate_lattice(cell_map, wall_thickness=0.6, eccentricity=1.5) """ unit_cell = pv.PlateUnitCell(_coerce_attribute(eccentricity)) return face_lattice( cell_map, unit_cell, wall_thickness=wall_thickness, surface_mode=surface_mode, surface_tolerance=surface_tolerance, )
[docs] def simple_cubic_plate( cell_map, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build a mapped simple cubic (three orthogonal {100} families) plate lattice. Equivalent to ``face_lattice(cell_map, "simple_cubic_plate", wall_thickness, surface_mode, surface_tolerance)``; see :func:`face_lattice` and :func:`simple_cubic_plate_cell`. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.simple_cubic_plate(cell_map, wall_thickness=0.6) """ return face_lattice( cell_map, "simple_cubic_plate", wall_thickness, surface_mode, surface_tolerance, )
[docs] def body_centered_cubic_plate( cell_map, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build a mapped body-centred cubic (six {110} families) plate lattice. Equivalent to ``face_lattice(cell_map, "body_centered_cubic_plate", wall_thickness, surface_mode, surface_tolerance)``; see :func:`face_lattice` and :func:`body_centered_cubic_plate_cell`. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.body_centered_cubic_plate(cell_map, wall_thickness=0.6) """ return face_lattice( cell_map, "body_centered_cubic_plate", wall_thickness, surface_mode, surface_tolerance, )
[docs] def face_centered_cubic_plate( cell_map, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build a mapped face-centred cubic (four {111} families, octet) plate lattice. Equivalent to ``face_lattice(cell_map, "face_centered_cubic_plate", wall_thickness, surface_mode, surface_tolerance)``; see :func:`face_lattice` and :func:`face_centered_cubic_plate_cell`. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.face_centered_cubic_plate(cell_map, wall_thickness=0.6) """ return face_lattice( cell_map, "face_centered_cubic_plate", wall_thickness, surface_mode, surface_tolerance, )
[docs] def octet_plate( cell_map, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Alias for :func:`face_centered_cubic_plate`.""" return face_centered_cubic_plate( cell_map, wall_thickness, surface_mode, surface_tolerance )
[docs] def cubic_octet_plate( cell_map, cubic_wall_thickness=1.0, octet_wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build the SC+FCC plate hybrid with independent family thicknesses. The union of :func:`simple_cubic_plate` and :func:`face_centered_cubic_plate` on the same cell map. Use :func:`face_lattice` with ``"cubic_octet_plate"`` for one thickness field. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. cubic_wall_thickness (float | pv.FloatAttribute | callable): Thickness of the {100} plates in mm, default ``1.0``. octet_wall_thickness (float | pv.FloatAttribute | callable): Thickness of the {111} plates in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Union: The union of the two plate lattices. Example: >>> node = mm.cubic_octet_plate(cell_map, cubic_wall_thickness=0.5, octet_wall_thickness=0.4) """ cubic = simple_cubic_plate( cell_map, cubic_wall_thickness, surface_mode, surface_tolerance, ) octet = face_centered_cubic_plate( cell_map, octet_wall_thickness, surface_mode, surface_tolerance, ) return pv.Union(0.0, [cubic, octet])
[docs] def hexagonal_honeycomb( cell_map, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build a mapped regular hexagonal honeycomb wall lattice. Equivalent to ``face_lattice(cell_map, "hexagonal_honeycomb", wall_thickness, surface_mode, surface_tolerance)``; see :func:`face_lattice` and :func:`hexagonal_honeycomb_cell`. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.hexagonal_honeycomb(cell_map, wall_thickness=0.6) """ return face_lattice( cell_map, "hexagonal_honeycomb", wall_thickness, surface_mode, surface_tolerance, )
[docs] def reentrant_honeycomb( cell_map, wall_thickness=1.0, angle_degrees=-30.0, rib_ratio=2.0, surface_mode="bilinear", surface_tolerance=0.05, ): """Build a mapped re-entrant (auxetic) honeycomb wall lattice. Equivalent to ``face_lattice(cell_map, reentrant_honeycomb_cell(angle_degrees, rib_ratio), ...)``; see :func:`face_lattice`. Use :func:`reentrant_hex_prism_reference_aspect_ratio` for the U:V pitch that keeps the requested angle. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. angle_degrees (float): Angle of the inclined ribs, strictly between -90 and 0, default ``-30.0``. rib_ratio (float): Positive ratio h/l of the vertical rib length to the inclined rib length, default ``2.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Raises: ValueError: If ``angle_degrees`` or ``rib_ratio`` is out of range. Example: >>> node = mm.reentrant_honeycomb(cell_map, wall_thickness=0.6, angle_degrees=-20.0) """ return face_lattice( cell_map, reentrant_honeycomb_cell(angle_degrees, rib_ratio), wall_thickness, surface_mode, surface_tolerance, )
def _honeycomb_builder(name, description): def build(cell_map, wall_thickness=1.0, surface_mode="bilinear", surface_tolerance=0.05): return face_lattice(cell_map, name, wall_thickness, surface_mode, surface_tolerance) build.__name__ = name build.__qualname__ = name build.__doc__ = """Build a mapped {description} wall lattice. Equivalent to ``face_lattice(cell_map, "{name}", wall_thickness, surface_mode, surface_tolerance)``; see :func:`face_lattice`. Use the U:V:W proportions from :func:`reference_cell_size` for regular cells. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. wall_thickness (float | pv.FloatAttribute | callable): Wall thickness in mm, default ``1.0``. surface_mode (str): ``"bilinear"`` (default) or ``"triangulated"`` wall surfaces. surface_tolerance (float): Largest surface error in mm on curved maps, default ``0.05``. Returns: pv.Node: a ``pv.MappedFaceLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.{name}(cell_map, wall_thickness=0.6) """.format(name=name, description=description) return build square_honeycomb = _honeycomb_builder("square_honeycomb", "square honeycomb") rotated_square_honeycomb = _honeycomb_builder( "rotated_square_honeycomb", "square honeycomb turned 45 degrees about W") triangular_honeycomb = _honeycomb_builder("triangular_honeycomb", "triangular honeycomb") rotated_triangular_honeycomb = _honeycomb_builder( "rotated_triangular_honeycomb", "triangular honeycomb turned 90 degrees")