Source code for pyvcad_metamaterials.lattices

"""Map-first native graph-lattice builders."""

# OpenVCAD
import pyvcad as pv

# Local
from .fields import _coerce_attribute
from .maps import _as_maps
from .unit_cells import (
    UNIT_CELLS,
    unit_cell_edges,
    reentrant_hex_prism as reentrant_hex_prism_edges,
    reentrant_3d as reentrant_3d_edges,
)


LATTICE_NAMES = tuple(sorted(UNIT_CELLS))


def _unit_cell(value):
    if isinstance(value, pv.GraphUnitCell):
        return value
    edges = unit_cell_edges(value) if isinstance(value, str) else list(value)
    vertices = []
    indices = {}
    indexed_edges = []
    for start, end in edges:
        pair = []
        for point in (start, end):
            normalized = tuple(float(coordinate) + 0.5 for coordinate in point)
            if normalized not in indices:
                indices[normalized] = len(vertices)
                vertices.append(pv.Vec3(*normalized))
            pair.append(indices[normalized])
        indexed_edges.append(tuple(pair))
    return pv.GraphUnitCell(vertices, indexed_edges)


[docs] def graph_lattice( cell_map, unit_cell, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0, ): """Tile a standard or custom graph (strut) cell through a CellMap. One copy of the cell fills each map cell. Struts are curved to follow the map, and every radius is a physical length in millimetres. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells, e.g. from :func:`rectangular_cell_map`. unit_cell (str | pv.GraphUnitCell | list): A name from ``LATTICE_NAMES`` (``"bcc"``, ``"octet"``, ...), a ``pv.GraphUnitCell`` with vertices in ``[0, 1]``, or a list of edges ``((x0, y0, z0), (x1, y1, z1))`` in normalized cell coordinates spanning ``[-0.5, 0.5]``. beam_radius (float | pv.FloatAttribute | callable): Strut radius in mm, default ``0.8``. Must stay positive. node_radius (float | pv.FloatAttribute | callable | None): Radius in mm of a sphere added at every unit-cell vertex. ``None`` (default) adds no joint spheres. curve_tolerance (float): Largest chord error in mm allowed when a strut is subdivided to follow a curved map, default ``0.05``. member_blend_radius (float | pv.FloatAttribute | pv.ScalarParameter | callable): Radius in mm of the smooth blend that fillets the nodes where distinct struts and joints meet, default ``0.0`` (sharp). A field needs ``pv.ScalarParameter`` bounds. Struts shared by neighbouring cells count once, so they never bulge. Controls accept a number, a ``pv.FloatAttribute`` field sampled at each point (plain ``x``, ``y``, ``z`` are in mm), or a Python function ``f(x, y, z, d)`` that is compiled with ``numba.cfunc``. Returns: pv.Node: a ``pv.MappedGraphLattice`` for one map, or a ``pv.MappedStructureCollection`` when ``cell_map`` is a ``pv.CellMapCollection`` holding more than one map. Example: >>> node = mm.graph_lattice(cell_map, "bcc", beam_radius=1.0) """ graph = _unit_cell(unit_cell) beam_field = _coerce_attribute(beam_radius) node_field = None if node_radius is None else _coerce_attribute(node_radius) nodes = [ pv.MappedGraphLattice( item, graph, beam_field, node_radius=node_field, curve_tolerance=float(curve_tolerance), member_blend_radius=member_blend_radius, ) for item in _as_maps(cell_map) ] return nodes[0] if len(nodes) == 1 else pv.MappedStructureCollection(_as_maps(cell_map), nodes)
_BUILDER_DOC = """Build a mapped {title} strut lattice. {description} Equivalent to ``graph_lattice(cell_map, "{name}", beam_radius, node_radius, curve_tolerance, member_blend_radius)``; see :func:`graph_lattice`. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells, e.g. from :func:`rectangular_cell_map`. beam_radius (float | pv.FloatAttribute | callable): Strut radius in mm, default ``0.8``. Must stay positive. node_radius (float | pv.FloatAttribute | callable | None): Radius in mm of a sphere at every unit-cell vertex; ``None`` (default) adds none. curve_tolerance (float): Largest chord error in mm when struts follow a curved map, default ``0.05``. member_blend_radius (float | pv.FloatAttribute | pv.ScalarParameter | callable): Smooth-blend radius in mm that fillets the nodes, default ``0.0``. Returns: pv.Node: a ``pv.MappedGraphLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Example: >>> node = mm.{name}(cell_map, beam_radius=0.7) """ def _builder(title, description): def decorate(function): function.__doc__ = _BUILDER_DOC.format( name=function.__name__, title=title, description=description ) return function return decorate
[docs] @_builder("simple cubic", "The twelve edges of the cube.") def cubic(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "cubic", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder("body-centred cubic (BCC)", "The four body diagonals through the cell centre.") def bcc(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "bcc", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder("face-centred cubic (FCC)", "The twelve face diagonals.") def fcc(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "fcc", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder("Kelvin", "The edges of a truncated octahedron (Kelvin cell).") def kelvin(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "kelvin", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder( "octet truss", "Face-centre to corner struts plus the octahedron joining the six face centres.", ) def octet(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "octet", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder("diamond", "Tetrahedral bonds of the diamond cubic structure.") def diamond(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "diamond", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder( "Isotruss", "Cube edges, centre-to-corner and centre-to-face struts (SC + BCC + SCC topology).", ) def isotruss(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "isotruss", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder( "fluorite", "Fluorite crystal net: face-centred sites joined to the eight tetrahedral sites.", ) def fluorite(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, "fluorite", beam_radius, node_radius, curve_tolerance, member_blend_radius)
[docs] @_builder( "hexagonal prism", "Edge network of a regular hexagonal prism extruded along W. Use" " :func:`hex_prism_reference_aspect_ratio` as the U/V cell pitch ratio to" " keep the hexagons regular.", ) def hex_prism_edge(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice( cell_map, "hex_prism_edge", beam_radius, node_radius, curve_tolerance, member_blend_radius )
[docs] def reentrant_hex_prism( cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, angle_degrees=-30.0, rib_ratio=2.0, member_blend_radius=0.0, ): """Build a mapped re-entrant (auxetic) hexagonal prism strut lattice. An extruded re-entrant honeycomb graph, regenerated from ``angle_degrees`` and ``rib_ratio`` and tiled through ``cell_map`` with :func:`graph_lattice`. To keep the intended geometry, give the map the U/V pitch ratio from :func:`reentrant_hex_prism_reference_aspect_ratio` with the same two parameters. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells, e.g. from :func:`rectangular_cell_map`. beam_radius (float | pv.FloatAttribute | callable): Strut radius in mm, default ``0.8``. Must stay positive. node_radius (float | pv.FloatAttribute | callable | None): Radius in mm of a sphere at every unit-cell vertex; ``None`` (default) adds none. curve_tolerance (float): Largest chord error in mm when struts follow a curved map, default ``0.05``. angle_degrees (float): Inclined-member angle in degrees, strictly between ``-90`` and ``0``, default ``-30``. rib_ratio (float): Vertical-to-inclined member length ratio, positive, default ``2.0``. member_blend_radius (float | pv.FloatAttribute | pv.ScalarParameter | callable): Smooth-blend radius in mm that fillets the nodes, default ``0.0``. Returns: pv.Node: a ``pv.MappedGraphLattice``, 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_hex_prism(cell_map, beam_radius=0.5, angle_degrees=-20.0) """ return graph_lattice( cell_map, reentrant_hex_prism_edges(angle_degrees, rib_ratio), beam_radius, node_radius, curve_tolerance, member_blend_radius, )
def _named(name, title, description): """Make a named builder for a parameter-free catalog cell.""" def build(cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, member_blend_radius=0.0): return graph_lattice(cell_map, name, beam_radius, node_radius, curve_tolerance, member_blend_radius) build.__name__ = name build.__qualname__ = name build.__doc__ = _BUILDER_DOC.format(name=name, title=title, description=description) return build rhombic_dodecahedron = _named( "rhombic_dodecahedron", "rhombic-dodecahedron foam", "The Voronoi edges of FCC sites: rhombic dodecahedra filling space.") weaire_phelan = _named( "weaire_phelan", "Weaire-Phelan foam", "The Voronoi edges of the A15 sites: two pentagonal dodecahedra and six" " tetrakaidecahedra per cell, four struts at every node.") truncated_cube = _named( "truncated_cube", "truncated-cube", "The edges of the truncated cubic honeycomb (truncated cubes and octahedra); every" " strut lies in a cell face plane.") column = _named( "column", "column", "One W column per cell, tied at its ends to the cell corners by X struts in the faces.") columns = _named( "columns", "two-column", "Two W columns per cell at the centres of the cell's U and V edges, tied by a" " half-pitch square grid in every layer.") triangular_prism_edge = _named( "triangular_prism_edge", "triangular-prism (strut triangular honeycomb)", "Edges of triangular prisms extruded along W, one edge family along U. Keep U:V =" " 1:sqrt(3) for equilateral triangles.") rotated_triangular_prism_edge = _named( "rotated_triangular_prism_edge", "rotated triangular-prism", "Triangular prisms turned 90 degrees, one edge family along V (U:V = sqrt(3):1).") rotated_square_prism_edge = _named( "rotated_square_prism_edge", "rotated square-prism", "Square prisms turned 45 degrees about W: X struts in every layer and W struts at the" " cell's vertical edges and centre.") hex_prism_vertex_centroid = _named( "hex_prism_vertex_centroid", "hexagonal-prism vertex-centroid", "Each hexagonal prism's centre joined to its twelve corners.") hex_prism_central_axis = _named( "hex_prism_central_axis", "hexagonal-prism central-axis", "Hexagonal-prism edges plus a W column through every prism, its ends joined to the" " cap corners by spokes.") hex_prism_diamond = _named( "hex_prism_diamond", "hexagonal diamond", "Tetrahedral bonds of hexagonal diamond (lonsdaleite): puckered hexagons in every" " layer joined by W bonds, on the corners of the hexagons of ``hex_prism_edge``.") hex_prism_laves = _named( "hex_prism_laves", "hexagonal Laves-phase", "Bonds between the smaller atoms of the C14 Laves phase: corner- and face-sharing" " tetrahedra (kagome layers joined through triangles).") tet_oct_vertex_centroid = _named( "tet_oct_vertex_centroid", "tetrahedral-octahedral vertex-centroid", "Each tetrahedron's and octahedron's centre in the tet-oct honeycomb joined to its" " corners.") oct_vertex_centroid = _named( "oct_vertex_centroid", "octahedral face vertex-centroid", "The face centres of the tet-oct honeycomb's octahedra joined to their three corners.")
[docs] def reentrant_3d( cell_map, beam_radius=0.8, node_radius=None, curve_tolerance=0.05, angle_degrees=-30.0, rib_ratio=2.0, member_blend_radius=0.0, ): """Build a mapped three-dimensional re-entrant (auxetic) strut lattice. Two orthogonal families of the planar re-entrant pattern of :func:`reentrant_hex_prism`, turned so the re-entrant direction is W, share their W ribs; compression along W pulls the cells in along both U and V. Give the map the U:V:W proportions from :func:`reference_cell_size` with the same two parameters to keep the requested angle. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells. beam_radius (float | pv.FloatAttribute | callable): Strut radius in mm, default ``0.8``. node_radius (float | pv.FloatAttribute | callable | None): Radius in mm of a sphere at every unit-cell vertex; ``None`` (default) adds none. curve_tolerance (float): Largest chord error in mm when struts follow a curved map, default ``0.05``. angle_degrees (float): Inclined-member angle in degrees, strictly between ``-90`` and ``0``, default ``-30``. rib_ratio (float): W-rib to inclined member length ratio, positive, default ``2.0``. member_blend_radius (float | pv.FloatAttribute | pv.ScalarParameter | callable): Smooth-blend radius in mm that fillets the nodes, default ``0.0``. Returns: pv.Node: a ``pv.MappedGraphLattice``, or a ``pv.MappedStructureCollection`` for a multi-map ``pv.CellMapCollection``. Raises: ValueError: If ``angle_degrees`` or ``rib_ratio`` is out of range. Example: >>> u, v, w = mm.reference_cell_size("reentrant_3d") >>> node = mm.reentrant_3d(cell_map, beam_radius=0.4, angle_degrees=-25.0) """ return graph_lattice( cell_map, reentrant_3d_edges(angle_degrees, rib_ratio), beam_radius, node_radius, curve_tolerance, member_blend_radius, )