Exact integer geometry on the Eisenstein A₂ hexagonal lattice — the densest packing of circles in a plane. Every point has exactly six equidistant neighbors. No privileged axis. No floating-point drift. This package provides Eisenstein integer arithmetic, build placement with collision detection, and A* pathfinding on the hexagonal neighbor graph.
pip install slackwater-latticeEisenstein integers are ℤ[ω] where ω = e^(2πi/3) = −1/2 + i√3/2. They form a triangular/hexagonal lattice in the complex plane.
Coordinate system: A point (a, b) in Eisenstein coordinates maps to Cartesian:
x = (a − b/2) · scale
y = b · (√3/2) · scale
Norm (squared distance from origin):
N(a + bω) = a² − ab + b²
This is always a non-negative integer. All arithmetic is exact — no floating point, no drift.
Six neighbor directions (unit norm elements):
| Direction | Offset | Cartesian |
|---|---|---|
| +1 | (1, 0) | East |
| −1 | (−1, 0) | West |
| +ω | (0, 1) | Northeast |
| −ω | (0, −1) | Southwest |
| +(1+ω) | (1, 1) | Southeast |
| −(1+ω) | (−1, −1) | Northwest |
from slackwater_lattice import EisensteinInteger
EisensteinInteger(a: int, b: int)An exact point on the A₂ lattice. Represents the value a + bω. Frozen dataclass — hashable, comparable.
Arithmetic operators:
| Operator | Signature | Formula |
|---|---|---|
+ |
(ei) -> ei |
(a+c, b+d) |
− |
(ei) -> ei |
(a−c, b−d) |
* |
(ei) -> ei |
(ac−bd, ad+bc−bd) |
conjugate() |
() -> ei |
(a−b, −b) |
norm() |
() -> int |
a² − ab + b² |
− (unary) |
() -> ei |
(−a, −b) |
Coordinate conversion:
ei.to_cartesian(scale: float = 1.0) -> tuple[float, float]
EisensteinInteger.from_cartesian(x: float, y: float, scale: float = 1.0) -> EisensteinIntegerThe from_cartesian method rounds to the nearest lattice point. This is the snapping algorithm — continuous space → discrete lattice. It is deterministic and exact.
Lattice operations:
ei.neighbors() -> list[EisensteinInteger] # 6 equidistant neighbors
ei.rings(radius: int) -> list[EisensteinInteger] # all points within hex distance
ei.is_unit() -> bool # norm == 1?
ei.is_zero() -> bool # a==0 and b==0?Module-level functions:
snap(x: float, z: float, scale: float = 1.0) -> EisensteinInteger
distance(a: EisensteinInteger, b: EisensteinInteger) -> int # squared Euclidean
hex_distance(a: EisensteinInteger, b: EisensteinInteger) -> int # graph steps
midpoint_region(a: EisensteinInteger, b: EisensteinInteger) -> list[EisensteinInteger]Hex distance formula:
if da·db ≥ 0: d = max(|da|, |db|) # same sector
else: d = |da| + |db| # opposite sectors
from slackwater_lattice import BuildPlacement, PlacementError
BuildPlacement(
scale: float = 4.0, # world units per lattice unit
)Manages occupied lattice points for a build. Collision detection is exact integer comparison — O(1) hash lookup, no tolerance bands.
Snapping:
bp.snap_position(x: float, z: float) -> EisensteinInteger
bp.snap_rotation(rotation_deg: float) -> int # nearest 60° incrementCollision and reservation:
bp.check_collision(lattice_point: EisensteinInteger) -> bool
bp.check_region_clear(center: EisensteinInteger, radius: int = 1) -> bool
bp.reserve(lattice_point: EisensteinInteger, meta: dict | None = None) -> EisensteinInteger
bp.release(lattice_point: EisensteinInteger) -> bool
bp.place(x: float, z: float, meta: dict | None = None) -> EisensteinInteger # snap + reservereserve() raises PlacementError on collision. place() is the combined snap-and-reserve convenience.
Queries:
bp.nearest_free(point: EisensteinInteger, max_radius: int = 20) -> EisensteinInteger | None
bp.occupied_neighbors(point: EisensteinInteger) -> list[EisensteinInteger]
bp.free_neighbors(point: EisensteinInteger) -> list[EisensteinInteger]
bp.boundary_points() -> list[EisensteinInteger] # frontier of the build
bp.footprint_area() -> int # count of occupied points
bp.diameter() -> int # max hex distance between any two pointsSupports in operator, len(), and iteration.
from slackwater_lattice import LatticePathfinder, LatticePath
LatticePathfinder(
placement: BuildPlacement,
allow_through_occupied: bool = False,
)A* pathfinder on the Eisenstein lattice. Uses hex_distance as the heuristic — admissible and consistent on the neighbor graph, guaranteeing optimal paths.
Methods:
pf.find_path(
start: EisensteinInteger,
goal: EisensteinInteger,
max_steps: int = 10_000,
) -> LatticePath | NoneReturns the shortest path through free lattice points, or None if unreachable.
pf.find_all_paths(start, goal, max_paths: int = 5) -> list[LatticePath]
pf.reachable(start, max_distance: int = 20) -> dict[EisensteinInteger, int]find_all_paths uses a simplified Yen's algorithm for k-shortest paths. reachable does BFS within max_distance steps.
LatticePath:
@dataclass
class LatticePath:
points: list[EisensteinInteger]
length() -> int # number of steps (edges)
is_empty() -> bool # single point or lessSupports iteration and indexing.
from slackwater_lattice import EisensteinInteger, distance, hex_distance
z = EisensteinInteger(3, 1)
print(z.norm()) # 7 (9 - 3 + 1)
print(z.to_cartesian(4.0)) # (10.0, 3.464...)
neighbors = z.neighbors() # 6 points at distance 1from slackwater_lattice import BuildPlacement
bp = BuildPlacement(scale=4.0)
point = bp.place(10.5, 3.2)
print(point) # nearest lattice point to (10.5, 3.2) at scale 4
# Collision-free placement
free = bp.nearest_free(point)from slackwater_lattice import EisensteinInteger, BuildPlacement, LatticePathfinder
bp = BuildPlacement()
# Build a wall
for i in range(-2, 3):
bp.reserve(EisensteinInteger(0, i))
pf = LatticePathfinder(placement=bp)
path = pf.find_path(EisensteinInteger(-3, 0), EisensteinInteger(3, 0))
print(path) # LatticePath(E(-3) → E(-2) → ... → E(3))
print(path.length()) # hex-optimal, avoids all wall pointsMIT