Skip to content

crystal-geometry

Generate 3D crystal geometry from CDL descriptions using half-space intersection and crystallographic symmetry operations. Version 2.0.0.

pip install gemmology-crystal-geometry

Core Functions

# cdl_to_geometry(description: CrystalDescription) → CrystalGeometry

Convert a parsed CDL description into 3D geometry. Uses desc.flat_forms() internally to handle FormGroup nodes.

from cdl_parser import parse_cdl
from crystal_geometry import cdl_to_geometry

desc = parse_cdl("cubic[m3m]:{111}@1.0 + {100}@1.3")
geom = cdl_to_geometry(desc)

print(f"Vertices: {len(geom.vertices)}")  # 24
print(f"Faces: {len(geom.faces)}")        # 14

Parameters:

  • description - Parsed CrystalDescription from cdl-parser

Returns: CrystalGeometry object with vertices, faces, and normals

# cdl_string_to_geometry(cdl_string: str) → CrystalGeometry

Convenience function that parses a CDL string and generates geometry in one call.

from crystal_geometry import cdl_string_to_geometry

geom = cdl_string_to_geometry("cubic[m3m]:{111}")
print(len(geom.vertices))  # 6
print(len(geom.faces))     # 8
# halfspace_intersection_3d(normals, distances, interior_point=None) → ndarray | None

Compute the vertices of the convex polyhedron defined by half-space intersection (each half-space normal · x ≤ distance). Returns an Nx3 vertex array, or None if the intersection is empty/unbounded. Use compute_face_vertices() to derive the ordered face index list for each plane from the resulting vertices.

# compute_face_vertices(vertices, normal, distance, tolerance=1e-6) → list[int]

Find the vertex indices lying on a single face plane, ordered counter-clockwise when viewed from outside. Called once per face normal/distance pair to build up a polyhedron's faces list from the vertices returned by halfspace_intersection_3d.

Convenience Constructors

from crystal_geometry import (
    create_octahedron,
    create_cube,
    create_dodecahedron,
    create_truncated_octahedron,
)

# Create common crystal forms directly
geom = create_octahedron()       # {111} in m3m
geom = create_cube()             # {100} in m3m
geom = create_dodecahedron()     # {110} in m3m
geom = create_truncated_octahedron()  # {111} + {100}

Symmetry Operations

# get_point_group_operations(point_group: str) → list[np.ndarray]

Get the 3x3 rotation/reflection matrices for a crystallographic point group.

from crystal_geometry import get_point_group_operations

matrices = get_point_group_operations("m3m")
print(len(matrices))  # 48 operations

matrices = get_point_group_operations("-3m")
print(len(matrices))  # 12 operations
# generate_equivalent_faces(miller, point_group: str) → list[ndarray]

Generate all symmetry-equivalent face normals from a single Miller index.

# miller_to_normal(h, k, l, lattice) → ndarray

Convert Miller indices to a Cartesian normal vector using lattice parameters.

# get_lattice_for_system(system: str) → LatticeParams

Get default lattice parameters for a crystal system.

Modification Functions

# apply_modifications(vertices, modifications) → ndarray

Apply morphological modifications (elongate, flatten, taper, twist) to vertices. Each modifier takes a params dict (matching the parsed CDL Modification.params), not individual keyword arguments.

from crystal_geometry import apply_elongation, apply_flatten, apply_taper, apply_twist

# Elongate along c-axis
vertices = apply_elongation(vertices, {'axis': 'c', 'ratio': 1.5})

# Flatten along c-axis
vertices = apply_flatten(vertices, {'axis': 'c', 'ratio': 0.5})

# Taper toward one end
vertices = apply_taper(vertices, {'direction': '+c', 'factor': 0.3})

# Twist around an axis
vertices = apply_twist(vertices, {'axis': 'c', 'angle': 30})

Amorphous & Aggregate Geometry

# generate_amorphous_shape(shape: str, radius: float = 1.0, seed: int = 42) → CrystalGeometry

Generate non-crystalline geometry (e.g. massive, botryoidal, reniform, stalactitic, mammillary, nodular, conchoidal) for CDL amorphous[...] descriptions. The returned CrystalGeometry has is_amorphous=True. seed makes the procedural mesh deterministic.

# generate_aggregate(base_geometry: CrystalGeometry, arrangement: str, count: int, spacing: float | None = None, orientation: str | None = None, seed: int = 42) → CrystalGeometry

Instance a base geometry into a crystal aggregate (parallel, random, radial, epitaxial, druse, cluster arrangements), matching CDL's ~ aggregate operator. The result's aggregate_metadata records the arrangement, instance count, spacing, and orientation; component_ids attributes each face back to its instance. count is capped by AGGREGATE_MAX_INSTANCES.

from crystal_geometry import generate_amorphous_shape, generate_aggregate, create_octahedron

# Botryoidal amorphous mass (e.g. precious opal)
geom = generate_amorphous_shape("botryoidal", radius=1.0)
print(geom.is_amorphous)  # True

# Cluster of octahedra
base = create_octahedron()
cluster = generate_aggregate(base, arrangement="cluster", count=6)
print(cluster.aggregate_metadata.arrangement)  # 'cluster'
print(cluster.aggregate_metadata.n_instances)  # 6

Twin System

from crystal_geometry import (
    get_twin_law,
    list_twin_laws,
    get_gemstone_twins,
    TwinLaw,
)

# List all available twin laws (14 total)
laws = list_twin_laws()
print(laws)  # ['albite', 'baveno', 'brazil', 'carlsbad', 'dauphine', ...]

# Get a specific twin law (note: 'spinel_law', not 'spinel')
law = get_twin_law("spinel_law")
print(law.name)      # 'Spinel Law (Macle)'
print(law.axis)      # array([...]) - twin rotation/reflection axis
print(law.angle)     # 180.0

# Get all twin laws for a specific gemstone
twins = get_gemstone_twins("quartz")
print(twins)  # ['brazil', 'dauphine', 'japan']

Habit System

from crystal_geometry import (
    get_habit,
    list_habits,
    get_gemstone_habits,
    CrystalHabit,
)

# List all registered habits (15 total)
habits = list_habits()
print(habits)  # ['barrel', 'cube', 'dodecahedron', 'feldspar_tabular', ...]

# Get a specific habit - CrystalHabit exposes vertices/faces directly
# (there is no .generate() method)
habit = get_habit("hexagonal_prism", scale=1.0)
print(habit.name)            # 'Hexagonal Prism'
print(habit.vertices.shape)  # (12, 3)
print(len(habit.faces))      # 8

# Get habits for a specific gemstone
gem_habits = get_gemstone_habits("diamond")
print(gem_habits)  # ['octahedron', 'dodecahedron', 'cube']

Classes

CrystalGeometry

Container for 3D crystal geometry data.

AttributeTypeDescription
vertices np.ndarray Nx3 array of vertex coordinates
faces list[list[int]] List of faces (each face is a list of vertex indices)
face_normals list[np.ndarray] Normal vectors for each face
face_forms list[int] Form index for each face (for colour-by-form rendering)
face_millers list[tuple[int, int, int]] Miller indices for each face
forms list[CrystalForm] Original form definitions from CDL
component_ids list[int] | None Component ID for each face, for twins/aggregates
twin_metadata TwinMetadata | None Twin metadata if geometry is twinned
is_amorphous bool Whether this is an amorphous (non-crystalline) geometry (default False)
aggregate_metadata AggregateMetadata | None Aggregate metadata if geometry was generated via generate_aggregate()

LatticeParams

Unit cell lattice parameters.

AttributeTypeDescription
a float Unit cell parameter a (default 1.0)
b float Unit cell parameter b (default 1.0)
c float Unit cell parameter c (default 1.0)
alpha float Angle alpha, in radians (default π/2)
beta float Angle beta, in radians (default π/2)
gamma float Angle gamma, in radians (default π/2)

Backend Acceleration

from crystal_geometry import get_backend, get_backend_info

# Check which backend is active
print(get_backend())      # 'native' or 'python'
print(get_backend_info()) # Detailed backend information