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