Core geometry model¶
Two layers¶
UnitCell is the small declarative layer. It contains sites, directed bonds, and
oriented plaquettes. Lattice is the compiled finite layer for a given extent and
boundary condition.
No model parameter belongs in either layer. A bond type is a geometry classification;
the downstream model decides whether that type means a hopping, exchange coupling, or
something else.
Module boundaries¶
The source tree is split by responsibility rather than by individual class:
definitions.py immutable user declarations: Site, Bond, Plaquette, UnitCell
builders.py temporary UnitCellBuilder and exact cell composition/transforms
tables.py read-only compiled BondTable and PlaquetteTable columns
_compiler.py private validation and finite-geometry compilation pipeline
lattice.py public Lattice object and derived/query operations
predefined.py constructors for common lattices
reciprocal.py reciprocal coordinates, phases, meshes, and optional BZ geometry
plotting.py optional Matplotlib projection of compiled geometry
tools/neighbors.py explicit authoring-time neighbor discovery and source generation
_compiler.py is deliberately private and cohesive. Splitting each compilation stage
into another file would add imports and intermediate public-looking modules without
creating an independent abstraction. It should be split again only if one stage gains a
separate backend (for example, a Rust compiler) or can be reused independently.
plotting.py consumes only public compiled arrays and imports Matplotlib lazily. It is
therefore a diagnostic/view layer, not part of the canonical geometry representation;
no colors, artists, or rendering state are stored on Lattice.
builders.py is mutable only during declaration. Its output is a regular immutable
UnitCell; the finite compiler therefore has one canonical input representation.
make_unit_cell_supercell accepts an orientation-preserving integer matrix and remaps
all integer relations rather than rediscovering them from Cartesian distances.
reciprocal.py distinguishes primitive and simulation-supercell cells in every method.
Coordinate conversions, regular meshes, allowed finite momenta, and translation phases
use NumPy alone. SciPy is imported lazily only for a rank-two Wigner--Seitz
BrillouinZone. Hamiltonian coefficients are never accepted or stored.
tools/neighbors.py is intentionally outside the top-level package API. It searches
periodic images only after an explicit author request, returns an inspectable draft, and
generates fixed Bond(...) source. Neither UnitCell nor Lattice imports it. Predefined
lattices must keep explicit declarations rather than calling discovery at runtime.
Neighbor authoring boundary¶
Automatic neighbor discovery groups relations by Cartesian distance, which is useful for bootstrapping an unfamiliar crystal but cannot infer physical equivalence. In particular, equal-length directions may have different hopping amplitudes, and the deterministic canonical orientation does not imply a hopping direction. Therefore the author must assign types with a classifier or edit the generated declarations.
The search enumerates integer translation shells. After radius r, the smallest singular
value of the direct basis and the largest intracell site separation give a lower bound on
every unseen bond length. Enumeration stops only when that bound lies beyond the requested
distance shell or cutoff. This avoids a hidden fixed image range for skew or embedded
lattices while keeping the implementation NumPy-only.
Bond identity¶
A unit-cell bond is
(source site, target site, integer cell_shift, type)
It is directed. Reversal swaps the endpoints and negates cell_shift. Two declarations
are never deduplicated merely because they map to the same finite pair of site IDs.
For each finite bond the compiler stores:
source, target, type_id, unit_bond, cell_shift, super_idx
unit_bond distinguishes different declarations that share a type. type_id groups
relations that the user considers geometrically equivalent.
Exact lookup returns row indices rather than a single edge because multiedges are
intentional. A plaquette perimeter is represented by PlaquetteBoundary, whose
relative primitive and simulation-supercell shifts are derived from compiled integer
coordinates without a floating-point position lookup.
Periodic boundary representation¶
Let E be the extent, c a finite source cell, and d the declared cell_shift.
For periodic axes:
target_cell = (c + d) mod E
boundary_shift = floor((c + d) / E)
The unique boundary shifts, together with their negatives, form
Lattice.supercell_shifts. The finite bond stores only its integer super_idx into this
small table.
The Cartesian endpoint is therefore
positions[target] + supercell_translations[super_idx]
This remains exact for negative shifts, bonds longer than one finite supercell, and one-cell periodic systems.
Plaquette storage¶
A plaquette is an ordered list of (site, cell_shift) vertices. Finite plaquettes use a
CSR-like representation:
offsets, sites, super_idx, type_id, unit_plaquette, anchor_cell
This supports different polygon sizes in one lattice while keeping vertex data in flat NumPy arrays. Orientation and type are never inferred from a sorted set of site IDs.
Performance boundary¶
Compilation loops over the usually small number of unit-cell definitions, while all finite cells are handled with NumPy arrays. Graph algorithms and third-party conversion can be optional adapters later; they should not become the canonical storage model.
If profiling eventually shows compilation to be important at very large extents, the compiler internals can be moved behind the same API to Numba, Cython, or Rust. No such compiled dependency is justified for the first version.
Release boundary¶
The public compatibility surface consists of the exports in latticegeom.__all__, the
documented latticegeom.tools authoring API, and the public read-only columns on
Lattice, BondTable, and PlaquetteTable. _compiler.py and underscore-prefixed
helpers remain private.
A release should run the full test suite, execute all examples, build the distribution, install the wheel into a clean environment, and build this documentation in strict mode. Optional features must continue to import lazily so the NumPy-only core stays usable.