Source code for pyvcad_metamaterials.tpms

"""Map-first standard and custom TPMS builders."""

# OpenVCAD
import pyvcad as pv

# Local
from .fields import _coerce_attribute
from .maps import _as_maps


TPMS_NAMES = (
    "fischer_koch_s",
    "frd",
    "gyroid",
    "iwp",
    "lidinoid",
    "neovius",
    "schwarz_d",
    "schwarz_p",
    "split_p",
)


def _combine(cell_map, build):
    maps = _as_maps(cell_map)
    nodes = [build(item) for item in maps]
    return nodes[0] if len(nodes) == 1 else pv.MappedStructureCollection(maps, nodes)


_BUILDER_DOC = """Build a mapped {title} TPMS lattice.

    {description}

    The surface repeats once per map cell. Equivalent to
    ``tpms(cell_map, "{name}", mode=mode, wall_thickness=wall_thickness,
    level=level)``; see :func:`tpms` for details.

    Args:
        cell_map (pv.CellMap | pv.CellMapCollection): Map that places the
            cells, e.g. from :func:`rectangular_cell_map`.
        wall_thickness (float | pv.FloatAttribute | callable): Total sheet
            wall thickness in mm, default ``1.0``. Must stay positive. Used
            only when ``mode="sheet"``.
        mode (str): ``"sheet"`` (default) or ``"solid"``.
        level (float | pv.FloatAttribute | callable): Solid level offset in mm,
            default ``0.0``; a positive value grows the solid. Used only when
            ``mode="solid"``.

    Returns:
        pv.Node: a ``pv.MappedImplicitLattice``, or a
        ``pv.MappedStructureCollection`` for a multi-map
        ``pv.CellMapCollection``.

    Example:
        >>> node = mm.{name}(cell_map, wall_thickness=1.2)
    """


def _builder(title, description):
    def decorate(function):
        function.__doc__ = _BUILDER_DOC.format(
            name=function.__name__, title=title, description=description
        )
        return function
    return decorate


[docs] def tpms(cell_map, kind, mode="sheet", wall_thickness=1.0, level=0.0): """Evaluate a standard or custom periodic implicit unit cell in a CellMap. The unit cell repeats once per map cell, so the cell size comes from ``cell_map``. The cell's scalar field is converted to an approximate signed distance in world millimetres through the map Jacobian, so wall thickness and level stay physical lengths on stretched or curved maps. Args: cell_map (pv.CellMap | pv.CellMapCollection): Map that places the cells, e.g. from :func:`rectangular_cell_map`. kind (str | pv.ImplicitUnitCell): A name from ``TPMS_NAMES`` (``"gyroid"``, ``"schwarz_p"``, ...) or a custom ``pv.ImplicitUnitCell``. mode (str): ``"sheet"`` (default) builds a wall of ``wall_thickness`` centred on the mid-surface. ``"solid"`` fills one side of the mid-surface, offset by ``level``. wall_thickness (float | pv.FloatAttribute | callable): Total sheet wall thickness in mm, default ``1.0``. Must stay positive. Used only in ``"sheet"`` mode. level (float | pv.FloatAttribute | callable): Solid level offset in mm, default ``0.0``. The solid is where the signed distance to the mid-surface is below ``level``, so a positive value grows it. Used only in ``"solid"`` mode. Controls accept a number, a ``pv.FloatAttribute`` field sampled at each point (plain ``x``, ``y``, ``z`` are in mm; ``cell_map.logical_position`` and friends give cell coordinates), or a Python function ``f(x, y, z, d)`` that is compiled with ``numba.cfunc``. Returns: pv.Node: a ``pv.MappedImplicitLattice`` for one map, or a ``pv.MappedStructureCollection`` when ``cell_map`` is a ``pv.CellMapCollection`` holding more than one map. Raises: TypeError: If ``kind`` is neither a name nor a ``pv.ImplicitUnitCell``. Example: >>> node = mm.tpms(cell_map, "schwarz_p", wall_thickness=1.0) """ if isinstance(kind, str): unit_cell = pv.ImplicitUnitCell.standard(kind) elif isinstance(kind, pv.ImplicitUnitCell): unit_cell = kind else: raise TypeError("kind must be a TPMS name or pv.ImplicitUnitCell") thickness_field = _coerce_attribute(wall_thickness) level_field = _coerce_attribute(level) return _combine( cell_map, lambda item: pv.MappedImplicitLattice( item, unit_cell, mode=mode, wall_thickness=thickness_field, level=level_field, ), )
[docs] @_builder("gyroid", "Schoen gyroid surface (nodal approximation).") def gyroid(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "gyroid", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("Schwarz P", "Schwarz Primitive surface (nodal approximation).") def schwarz_p(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "schwarz_p", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("Schwarz D", "Schwarz Diamond surface (nodal approximation).") def schwarz_d(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "schwarz_d", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("Neovius", "Neovius surface (nodal approximation).") def neovius(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "neovius", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("I-WP", "Schoen I-WP surface (nodal approximation).") def iwp(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "iwp", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("lidinoid", "Lidinoid surface (nodal approximation).") def lidinoid(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "lidinoid", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("split P", "Split P surface (nodal approximation).") def split_p(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "split_p", mode=mode, wall_thickness=wall_thickness, level=level)
[docs] @_builder("Fischer-Koch S", "Fischer-Koch S surface (nodal approximation).") def fischer_koch_s(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms( cell_map, "fischer_koch_s", mode=mode, wall_thickness=wall_thickness, level=level, )
[docs] @_builder("F-RD", "Schoen F-RD surface (nodal approximation).") def frd(cell_map, wall_thickness=1.0, mode="sheet", level=0.0): return tpms(cell_map, "frd", mode=mode, wall_thickness=wall_thickness, level=level)