"""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,
)