Source code for pyvcad_metamaterials.unit_cells

"""Unit-cell edge generators for strut-based lattices.

Each generator returns a list of edges for a single unit cell as
``[((x0, y0, z0), (x1, y1, z1)), ...]`` in **normalized, origin-centered**
coordinates that span ``[-0.5, 0.5]`` on every axis. ``lattices.py`` shifts them
into the native ``GraphUnitCell`` interval ``[0, 1]``; the mapped C++ node then
tiles and deforms them through the supplied ``CellMap``.

The four classic cells (``cubic``, ``bcc``, ``fcc``, ``kelvin``) mirror the edge
definitions used by the C++ ``GraphLattice`` presets in
``core-lib/src/tree/leaf/lattice/graph_lattice.cpp`` so results are consistent
with the built-in node. Additional crystallographic and engineering presets are
defined with explicit junctions so graph metadata agrees with the geometry. Most
of the newer cells are a rule applied to a tessellation (``tessellations.py``):
the docstring of each says which.
"""

# STD
import math

# Local
from .tessellations import _periodic_graph, periodic_bonds, periodic_voronoi, tessellation


_H = 0.5   # half cell (normalized)
_Q = 0.25  # quarter cell (normalized)

# Eight cube corners, indexed to match the C++ GraphLattice vertex numbering.
_CORNERS = [
    (-_H, -_H, -_H),  # v0
    (_H, -_H, -_H),   # v1
    (-_H, _H, -_H),   # v2
    (_H, _H, -_H),    # v3
    (-_H, -_H, _H),   # v4
    (_H, -_H, _H),    # v5
    (-_H, _H, _H),    # v6
    (_H, _H, _H),     # v7
]


def cubic():
    """Simple cubic cell: the twelve edges of the cube."""
    v = _CORNERS
    idx = [
        (0, 1), (1, 3), (3, 2), (2, 0),   # bottom face
        (4, 5), (5, 7), (7, 6), (6, 4),   # top face
        (0, 4), (1, 5), (2, 6), (3, 7),   # vertical connectors
    ]
    return [(v[a], v[b]) for a, b in idx]


def bcc():
    """Body-centered cubic cell: the four body diagonals (corner to opposite
    corner through the center)."""
    v = _CORNERS
    idx = [(0, 7), (1, 6), (2, 5), (3, 4)]
    return [(v[a], v[b]) for a, b in idx]


def fcc():
    """Face-centered cubic cell: the twelve face diagonals."""
    v = _CORNERS
    idx = [
        (0, 6), (0, 5), (0, 3),
        (1, 7), (1, 4), (1, 2),
        (2, 7), (2, 4),
        (3, 5), (3, 6),
        (5, 6), (7, 4),
    ]
    return [(v[a], v[b]) for a, b in idx]


def kelvin():
    """Kelvin cell (truncated octahedron) edges."""
    # Vertices grouped by face, in origin-centered normalized coordinates.
    v = [
        (_Q, 0.0, _H), (0.0, _Q, _H), (-_Q, 0.0, _H), (0.0, -_Q, _H),        # top
        (_Q, 0.0, -_H), (0.0, _Q, -_H), (-_Q, 0.0, -_H), (0.0, -_Q, -_H),    # bottom
        (_H, _Q, 0.0), (_H, 0.0, _Q), (_H, -_Q, 0.0), (_H, 0.0, -_Q),        # right
        (-_H, _Q, 0.0), (-_H, 0.0, _Q), (-_H, -_Q, 0.0), (-_H, 0.0, -_Q),    # left
        (_Q, _H, 0.0), (0.0, _H, _Q), (-_Q, _H, 0.0), (0.0, _H, -_Q),        # front
        (_Q, -_H, 0.0), (0.0, -_H, _Q), (-_Q, -_H, 0.0), (0.0, -_H, -_Q),    # back
    ]
    idx = [
        (0, 1), (1, 2), (2, 3), (3, 0),        # top square
        (4, 5), (5, 6), (6, 7), (7, 4),        # bottom square
        (8, 9), (9, 10), (10, 11), (11, 8),    # right square
        (12, 13), (13, 14), (14, 15), (15, 12),  # left square
        (16, 17), (17, 18), (18, 19), (19, 16),  # front square
        (20, 21), (21, 22), (22, 23), (23, 20),  # back square
        (0, 9), (3, 21), (2, 13), (1, 17),     # top connectors
        (4, 11), (5, 19), (6, 15), (7, 23),    # bottom connectors
        (14, 22), (12, 18), (8, 16), (10, 20),  # side connectors
    ]
    return [(v[a], v[b]) for a, b in idx]


def octet():
    """Octet truss cell: face-center to corner struts (tetrahedra) plus the
    octahedron formed by the six face centers."""
    corners = _CORNERS
    faces = {
        "x+": (_H, 0.0, 0.0),
        "x-": (-_H, 0.0, 0.0),
        "y+": (0.0, _H, 0.0),
        "y-": (0.0, -_H, 0.0),
        "z+": (0.0, 0.0, _H),
        "z-": (0.0, 0.0, -_H),
    }
    edges = []

    # Each face center to the four corners lying on that face.
    axis_of = {"x+": (0, _H), "x-": (0, -_H), "y+": (1, _H),
               "y-": (1, -_H), "z+": (2, _H), "z-": (2, -_H)}
    for key, center in faces.items():
        axis, value = axis_of[key]
        for corner in corners:
            if abs(corner[axis] - value) < 1e-9:
                edges.append((center, corner))

    # Octahedron: every face center connects to every non-opposite face center.
    opposite = {"x+": "x-", "x-": "x+", "y+": "y-",
                "y-": "y+", "z+": "z-", "z-": "z+"}
    keys = list(faces)
    for i in range(len(keys)):
        for j in range(i + 1, len(keys)):
            if opposite[keys[i]] != keys[j]:
                edges.append((faces[keys[i]], faces[keys[j]]))

    return edges


def diamond():
    """Diamond cubic cell: tetrahedral bonds between the two interpenetrating
    FCC sub-lattices."""
    # Defined in the [0, 1] cube, then shifted to be origin-centered.
    tetra_bonds = [
        ((0.25, 0.25, 0.25), [(0, 0, 0), (0.5, 0.5, 0), (0.5, 0, 0.5), (0, 0.5, 0.5)]),
        ((0.75, 0.75, 0.25), [(0.5, 0.5, 0), (1, 1, 0), (1, 0.5, 0.5), (0.5, 1, 0.5)]),
        ((0.75, 0.25, 0.75), [(0.5, 0, 0.5), (1, 0.5, 0.5), (1, 0, 1), (0.5, 0.5, 1)]),
        ((0.25, 0.75, 0.75), [(0, 0.5, 0.5), (0.5, 1, 0.5), (0.5, 0.5, 1), (0, 1, 1)]),
    ]

    def shift(p):
        return (p[0] - 0.5, p[1] - 0.5, p[2] - 0.5)

    edges = []
    for node, neighbors in tetra_bonds:
        for neighbor in neighbors:
            edges.append((shift(node), shift(neighbor)))
    return edges


def isotruss():
    """SC+BCC+SCC Isotruss topology with every center junction explicit.

    The topology combines the twelve cube edges, eight center-to-corner struts,
    and six center-to-face-center struts. The name identifies the graph
    topology; isotropic mechanical behavior still depends on member radii.
    """
    center = (0.0, 0.0, 0.0)
    face_centers = [
        (-_H, 0.0, 0.0), (_H, 0.0, 0.0),
        (0.0, -_H, 0.0), (0.0, _H, 0.0),
        (0.0, 0.0, -_H), (0.0, 0.0, _H),
    ]
    return cubic() + [
        (center, corner) for corner in _CORNERS
    ] + [
        (center, face_center) for face_center in face_centers
    ]


def fluorite():
    """Fluorite crystal net: FCC calcium sites joined to tetrahedral sites."""
    calcium = list(_CORNERS) + [
        (-_H, 0.0, 0.0), (_H, 0.0, 0.0),
        (0.0, -_H, 0.0), (0.0, _H, 0.0),
        (0.0, 0.0, -_H), (0.0, 0.0, _H),
    ]
    tetrahedral = [
        (x, y, z)
        for x in (-_Q, _Q)
        for y in (-_Q, _Q)
        for z in (-_Q, _Q)
    ]
    edges = []
    target_squared_length = 3.0 / 16.0
    for site in tetrahedral:
        for neighbor in calcium:
            squared_length = sum(
                (site[axis] - neighbor[axis]) ** 2 for axis in range(3)
            )
            if abs(squared_length - target_squared_length) < 1e-12:
                edges.append((site, neighbor))
    return edges


_HONEYCOMB_EDGE_INDICES = [
    (3, 2), (3, 5), (3, 9),
    (1, 0), (1, 9), (1, 13),
    (10, 0), (10, 2), (10, 11),
    (6, 7), (14, 15), (4, 5), (8, 9), (13, 12),
    (0, 14), (2, 6),
]


def _honeycomb_points(reentrant=False, angle_degrees=-30.0, rib_ratio=2.0):
    if reentrant:
        angle = math.radians(float(angle_degrees))
        rib_ratio = float(rib_ratio)
        if not -90.0 < float(angle_degrees) < 0.0:
            raise ValueError("angle_degrees must lie strictly between -90 and 0")
        if not math.isfinite(rib_ratio) or rib_ratio <= 0.0:
            raise ValueError("rib_ratio must be positive and finite")
        inset = -math.sin(angle)
        height = rib_ratio + 2.0 * inset
        inner_low = inset / height
        inner_high = (rib_ratio + inset) / height
        boundary_low = 2.0 * inset / height
        boundary_high = rib_ratio / height
    else:
        inner_low = 1.0 / 3.0
        inner_high = 2.0 / 3.0
        boundary_low = 1.0 / 6.0
        boundary_high = 5.0 / 6.0

    return [
        (0.75, inner_high), (0.75, inner_low),
        (0.25, inner_high), (0.25, inner_low),
        (0.0, 0.0), (0.0, boundary_low),
        (0.0, boundary_high), (0.0, 1.0),
        (0.5, 0.0), (0.5, boundary_low),
        (0.5, boundary_high), (0.5, 1.0),
        (1.0, 0.0), (1.0, boundary_low),
        (1.0, boundary_high), (1.0, 1.0),
    ]


def hex_prism_edge():
    """Regular hexagonal-prism edge network: the edges of ``tessellation("hexagonal_prism")``.

    The U/V unit-cell pitch should use the reference aspect ratio returned by
    :func:`hex_prism_reference_aspect_ratio` to preserve regular hexagons. W
    struts stand only at hexagon corners.
    """
    return tessellation("hexagonal_prism").edges()


def reentrant_hex_prism(angle_degrees=-30.0, rib_ratio=2.0):
    """Extruded re-entrant hexagonal graph: the edges of the re-entrant prism tessellation.

    ``rib_ratio`` is the vertical-to-inclined member length ratio. The catalog
    defaults follow the foldable equal-height geometry h/l=2 at -30 degrees. W
    struts stand only at hexagon corners.
    """
    return tessellation(
        "reentrant_hexagonal_prism",
        angle_degrees=angle_degrees,
        rib_ratio=rib_ratio,
    ).edges()


# Struts of the "column" rule on a prism: the axis through the centre and spokes from the
# centre of each cap to its corners.
_COLUMN_RULES = ("central_axes", "facet_vertex_centroid")

_FCC_SITES = ((0.0, 0.0, 0.0), (0.5, 0.5, 0.0), (0.5, 0.0, 0.5), (0.0, 0.5, 0.5))
_A15_SITES = (
    (0.0, 0.0, 0.0), (0.5, 0.5, 0.5),
    (0.25, 0.5, 0.0), (0.75, 0.5, 0.0),
    (0.0, 0.25, 0.5), (0.0, 0.75, 0.5),
    (0.5, 0.0, 0.25), (0.5, 0.0, 0.75),
)

# Hexagonal crystal cells use the rectangular supercell of the hexagonal prism cells: U = 2a,
# V = sqrt(3) a, W = c, with the ideal c / a = sqrt(8 / 3) of close packing.
_C_OVER_A = math.sqrt(8.0 / 3.0)
_HEXAGONAL_CELL_SIZE = (2.0 / math.sqrt(3.0), 1.0, _C_OVER_A / math.sqrt(3.0))


def _hexagonal_sites(fractional):
    """Map sites given in a hexagonal cell (a1, a2 at 120 degrees, c) into the supercell."""
    sites = set()
    for f1, f2, f3 in fractional:
        for n1 in range(-3, 4):
            for n2 in range(-3, 4):
                # Cartesian in units of a, shifted half a cell along U so the atom columns of
                # the hexagonal diamond sit on the corners of hex_prism_edge's hexagons.
                x = (f1 + n1) - 0.5 * (f2 + n2) + 0.5
                y = 0.5 * math.sqrt(3.0) * (f2 + n2)
                u = x / 2.0
                v = y / math.sqrt(3.0)
                if -1e-9 <= u < 1.0 - 1e-9 and -1e-9 <= v < 1.0 - 1e-9:
                    sites.add((round(u, 12) + 0.0, round(v, 12) + 0.0, round(f3 % 1.0, 12) + 0.0))
    return sorted(sites)


def _bond_net(fractional, bond_length):
    """Nearest-neighbour bonds of hexagonal sites, ``bond_length`` in units of a."""
    unit = _HEXAGONAL_CELL_SIZE[1] / math.sqrt(3.0)   # a in the cell_size units (V = 1)
    return periodic_bonds(_hexagonal_sites(fractional), 1.02 * bond_length * unit,
                          cell_size=_HEXAGONAL_CELL_SIZE)


def rhombic_dodecahedron():
    """Rhombic-dodecahedron foam: the Voronoi edges of FCC sites (:func:`periodic_voronoi`)."""
    return periodic_voronoi(_FCC_SITES).edges()


def weaire_phelan():
    """Weaire-Phelan foam: the Voronoi edges of the eight A15 sites.

    Two pentagonal dodecahedra and six tetrakaidecahedra fill each cell; every
    interior node joins four struts.
    """
    return periodic_voronoi(_A15_SITES).edges()


def truncated_cube():
    """Edges of the truncated cubic honeycomb: a truncated cube centred in the cell, with
    octahedra at the cell corners. Every strut lies in a cell face plane."""
    return tessellation("truncated_cube").edges()


def column():
    """One W column per cell, tied at the top and bottom by X struts in the cell faces.

    The column rule on ``tessellation("cube")``: the axis through the centre and
    spokes from each cap's centre to its corners.
    """
    return tessellation("cube").edges(_COLUMN_RULES, face_tags="cap")


def columns():
    """Two W columns per cell, tied by a half-pitch square grid in every layer.

    The column rule of :func:`column` on ``tessellation("rotated_square_prism")``,
    so the columns stand at the centres of the cell's U and V edges.
    """
    return tessellation("rotated_square_prism").edges(_COLUMN_RULES, face_tags="cap")


def reentrant_3d(angle_degrees=-30.0, rib_ratio=2.0):
    """Three-dimensional re-entrant (auxetic) strut cell.

    Two orthogonal families of the planar re-entrant strut pattern of
    :func:`reentrant_hex_prism`, turned so that the re-entrant direction is W:
    U-W walls at V = 1/4 and 3/4 and V-W walls at U = 1/4 and 3/4. The walls
    share their full W ribs, so compression along W pulls the walls in along
    both U and V. Give the map the U:V:W proportions from
    :func:`reference_cell_size` with the same parameters.

    Args:
        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``.

    Returns:
        list: Origin-centred edges.
    """
    import numpy as np

    points = _honeycomb_points(reentrant=True, angle_degrees=angle_degrees, rib_ratio=rib_ratio)
    segments = []
    for offset in (0.25, 0.75):
        for start, end in _HONEYCOMB_EDGE_INDICES:
            first, second = points[start], points[end]
            segments.append((np.array((first[0], offset, first[1])), np.array((second[0], offset, second[1]))))
            segments.append((np.array((offset, first[0], first[1])), np.array((offset, second[0], second[1]))))
    return _periodic_graph(segments)


def triangular_prism_edge():
    """Triangular-prism edge network (a strut triangular honeycomb), one edge family along U.

    Edges of ``tessellation("triangular_prism")``; keep U:V = 1:sqrt(3) for
    equilateral triangles.
    """
    return tessellation("triangular_prism").edges()


def rotated_triangular_prism_edge():
    """Triangular-prism edge network turned 90 degrees: one edge family along V.

    Keep U:V = sqrt(3):1 for equilateral triangles.
    """
    return tessellation("rotated_triangular_prism").edges()


def rotated_square_prism_edge():
    """Square-prism edge network turned 45 degrees about W: X struts in every
    layer and W struts at the cell's vertical edges and centre."""
    return tessellation("rotated_square_prism").edges()


def hex_prism_vertex_centroid():
    """Hexagonal prisms with each prism's centre joined to its twelve corners
    (``"cell_vertex_centroid"`` on ``tessellation("hexagonal_prism")``)."""
    return tessellation("hexagonal_prism").edges("cell_vertex_centroid")


def hex_prism_central_axis():
    """Hexagonal-prism edges plus a central W column in every prism, whose ends
    are joined to the cap corners by spokes (the column rule of :func:`column`)."""
    hexagonal = tessellation("hexagonal_prism")
    return _periodic_union(hexagonal.edges(), hexagonal.edges(_COLUMN_RULES, face_tags="cap"))


def hex_prism_diamond():
    """Hexagonal diamond (lonsdaleite): tetrahedral bonds, with puckered
    hexagons in every layer joined by W bonds.

    The atom columns sit on the corners of :func:`hex_prism_edge`'s hexagons.
    Ideal bond angles need the U:V:W proportions from
    :func:`reference_cell_size`.
    """
    sites = ((1.0 / 3.0, 2.0 / 3.0, 0.0), (1.0 / 3.0, 2.0 / 3.0, 3.0 / 8.0),
             (2.0 / 3.0, 1.0 / 3.0, 0.5), (2.0 / 3.0, 1.0 / 3.0, 7.0 / 8.0))
    return _bond_net(sites, 3.0 / 8.0 * _C_OVER_A)


def hex_prism_laves():
    """Hexagonal Laves-phase net: the bonds between the smaller atoms of the C14
    structure, a network of corner- and face-sharing tetrahedra (kagome layers
    joined through triangles). Uses the proportions from :func:`reference_cell_size`.
    """
    x = -1.0 / 6.0
    sites = [(0.0, 0.0, 0.0), (0.0, 0.0, 0.5),
             (x, 2.0 * x, 0.25), (-2.0 * x, -x, 0.25), (x, -x, 0.25),
             (-x, -2.0 * x, 0.75), (2.0 * x, x, 0.75), (-x, x, 0.75)]
    return _bond_net(sites, 0.5)


def tet_oct_vertex_centroid():
    """The tetrahedral-octahedral honeycomb with each tetrahedron's and
    octahedron's centre joined to its corners (``"cell_vertex_centroid"``)."""
    return tessellation("tet_oct").edges("cell_vertex_centroid")


def oct_vertex_centroid():
    """The face centres of the octahedra of the tetrahedral-octahedral
    honeycomb joined to their three corners (``"facet_vertex_centroid"`` on the
    octahedra). The octahedra's own centre-to-corner struts would be
    :func:`cubic` at half the pitch."""
    return tessellation("tet_oct").edges("facet_vertex_centroid", kinds="oct")


def _periodic_union(*edge_lists):
    import numpy as np

    segments = [
        (np.asarray(start) + 0.5, np.asarray(end) + 0.5)
        for edges in edge_lists for start, end in edges
    ]
    return _periodic_graph(segments)


[docs] def hex_prism_reference_aspect_ratio(): """Return the U/V cell pitch ratio for regular hexagons, ``2 / sqrt(3)``. Use it to size the map cells for :func:`hex_prism_edge`, e.g. a U pitch of ``ratio * v_pitch``. Returns: float: U pitch divided by V pitch. """ return 2.0 / math.sqrt(3.0)
[docs] def reentrant_hex_prism_reference_aspect_ratio( angle_degrees=-30.0, rib_ratio=2.0, ): """Return the U/V cell pitch ratio preserving a re-entrant geometry. Pass the same values given to :func:`reentrant_hex_prism`. Args: 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``. Returns: float: U pitch divided by V pitch. Raises: ValueError: If either parameter is out of range. """ angle = math.radians(float(angle_degrees)) rib_ratio = float(rib_ratio) if not -90.0 < float(angle_degrees) < 0.0: raise ValueError("angle_degrees must lie strictly between -90 and 0") if not math.isfinite(rib_ratio) or rib_ratio <= 0.0: raise ValueError("rib_ratio must be positive and finite") return 4.0 * math.cos(angle) / (rib_ratio - 2.0 * math.sin(angle))
#: Registry mapping a friendly cell name to its edge generator. UNIT_CELLS = { "cubic": cubic, "bcc": bcc, "fcc": fcc, "kelvin": kelvin, "octet": octet, "diamond": diamond, "isotruss": isotruss, "fluorite": fluorite, "hex_prism_edge": hex_prism_edge, "reentrant_hex_prism": reentrant_hex_prism, "rhombic_dodecahedron": rhombic_dodecahedron, "weaire_phelan": weaire_phelan, "truncated_cube": truncated_cube, "column": column, "columns": columns, "reentrant_3d": reentrant_3d, "triangular_prism_edge": triangular_prism_edge, "rotated_triangular_prism_edge": rotated_triangular_prism_edge, "rotated_square_prism_edge": rotated_square_prism_edge, "hex_prism_vertex_centroid": hex_prism_vertex_centroid, "hex_prism_central_axis": hex_prism_central_axis, "hex_prism_diamond": hex_prism_diamond, "hex_prism_laves": hex_prism_laves, "tet_oct_vertex_centroid": tet_oct_vertex_centroid, "oct_vertex_centroid": oct_vertex_centroid, } _HEX_PRISM_SIZE = (2.0 / math.sqrt(3.0), 1.0, 1.0) _REFERENCE_CELL_SIZES = { "hex_prism_edge": _HEX_PRISM_SIZE, "hex_prism_vertex_centroid": _HEX_PRISM_SIZE, "hex_prism_central_axis": _HEX_PRISM_SIZE, "hexagonal_honeycomb": _HEX_PRISM_SIZE, "hex_prism_diamond": _HEXAGONAL_CELL_SIZE, "hex_prism_laves": _HEXAGONAL_CELL_SIZE, "triangular_prism_edge": (1.0, math.sqrt(3.0), 1.0), "triangular_honeycomb": (1.0, math.sqrt(3.0), 1.0), "rotated_triangular_prism_edge": (math.sqrt(3.0), 1.0, 1.0), "rotated_triangular_honeycomb": (math.sqrt(3.0), 1.0, 1.0), }
[docs] def reference_cell_size(name, angle_degrees=-30.0, rib_ratio=2.0): """Return the U:V:W cell proportions that keep a catalog cell's intended shape. Give a map cells with these proportions (any common scale) to get regular hexagons and triangles, ideal bond angles, or the requested re-entrant angle. Cells without a preferred shape return ``(1, 1, 1)``; for extruded prism and honeycomb cells the W value is a free choice. Args: name (str): A graph cell name from ``LATTICE_NAMES`` or a face cell name from ``FACE_LATTICE_NAMES``. angle_degrees (float): For ``reentrant_hex_prism``, ``reentrant_3d`` and ``reentrant_honeycomb``: the inclined-member angle, default ``-30``. rib_ratio (float): For the same cells: the rib ratio, default ``2.0``. Returns: tuple[float, float, float]: Relative U, V, W pitches. Example: >>> u, v, w = mm.reference_cell_size("hex_prism_diamond") >>> pitch = 8.0 / v # 8 mm along V >>> cell_map = mm.rectangular_cell_map(bounds, cell_size=(u * pitch, v * pitch, w * pitch)) """ if name in ("reentrant_hex_prism", "reentrant_honeycomb"): return (reentrant_hex_prism_reference_aspect_ratio(angle_degrees, rib_ratio), 1.0, 1.0) if name == "reentrant_3d": ratio = reentrant_hex_prism_reference_aspect_ratio(angle_degrees, rib_ratio) return (ratio, ratio, 1.0) if name in _REFERENCE_CELL_SIZES: return tuple(float(value) for value in _REFERENCE_CELL_SIZES[name]) from .faces import FACE_LATTICE_NAMES if name in UNIT_CELLS or name in FACE_LATTICE_NAMES: return (1.0, 1.0, 1.0) raise ValueError("Unknown unit cell '{}'".format(name))
def unit_cell_edges(name): """Return the normalized edge list for a named unit cell.""" if name not in UNIT_CELLS: raise ValueError("Unknown unit cell '{}'. Available: {}".format( name, ", ".join(sorted(UNIT_CELLS)))) return UNIT_CELLS[name]()