Source code for bornsim.grid

"""Reusable centred cubic voxel grids with unit-bearing coordinates."""

from dataclasses import dataclass
import numpy as np
from .units import Quantity, _integer, validate_units


[docs] @dataclass(frozen=True, kw_only=True) class Grid: """Define the spatial discretization once for generation and scattering. Parameters ---------- shape : tuple of int Three explicitly chosen voxel counts, each from 2 to 32. spacing : Quantity Positive, finite cubic voxel width, with its supplied length units. A spacing must be supplied. Use :meth:`from_extent` when the full physical voxel-box extent is known instead of the voxel spacing. Notes ----- Voxel centres are ``(i - (N - 1)/2) * spacing`` on each axis. The box extends half a voxel beyond its outer centres. It describes the sampled domain, not an additional dielectric boundary. Background refractive index belongs to the medium or volume, rather than the spatial grid. """ shape: tuple[int, int, int] spacing: Quantity
[docs] @classmethod def from_extent(cls, *, shape: tuple[int, int, int], extent: Quantity) -> "Grid": """Create an isotropic voxel grid from its full physical extent. ``extent`` gives the full voxel-box width along x, y, and z. Each axis extent must imply the same cubic voxel spacing. """ if not isinstance(shape, (tuple, list)) or len(shape) != 3: raise ValueError("shape must have three axes.") normalized_shape = ( _integer(value=shape[0], name="axis size", low=2, high=32), _integer(value=shape[1], name="axis size", low=2, high=32), _integer(value=shape[2], name="axis size", low=2, high=32), ) validate_units( extent, unit="meter", name="extent", scalar=False, ) if np.shape(extent) != (3,): raise ValueError("extent must contain three axis lengths.") if np.any(~np.isfinite(extent)) or np.any(extent <= 0): raise ValueError("extent must be finite and positive.") spacing = extent / np.asarray(normalized_shape) spacing_magnitudes = spacing.to("meter").magnitude if not np.allclose(spacing_magnitudes, spacing_magnitudes[0], rtol=1e-12, atol=0): raise ValueError("extent and shape must imply the same cubic voxel spacing on every axis.") return cls( shape=normalized_shape, spacing=spacing[0], )
def __post_init__(self) -> None: if not isinstance(self.shape, (tuple, list)) or len(self.shape) != 3: raise ValueError("shape must have three axes.") shape = tuple(_integer(value=n, name="axis size", low=2, high=32) for n in self.shape) validate_units( self.spacing, unit="meter", name="spacing", scalar=True, ) if not np.isfinite(self.spacing) or self.spacing <= 0: raise ValueError("spacing must be finite and positive.") object.__setattr__(self, "shape", shape) def __repr__(self) -> str: return f"Grid(shape={self.shape}, spacing={self.spacing:~g})" @property def positions(self) -> Quantity: """Unit-bearing voxel-centre coordinates, shape ``(*shape, 3)``.""" axes = [np.arange(n) - (n - 1) / 2 for n in self.shape] return np.stack(np.meshgrid(*axes, indexing="ij"), axis=-1) * self.spacing @property def volume(self) -> Quantity: """Physical voxel-box volume in cubic metres.""" return np.prod(self.shape) * self.spacing**3 @property def metadata(self) -> dict[str, object]: """Fresh JSON-compatible grid settings in SI units.""" return {"shape": list(self.shape), "spacing_m": float(self.spacing.to("meter").magnitude)} @classmethod def _resolve(cls, *, grid: "Grid") -> "Grid": if not isinstance(grid, cls): raise TypeError("grid must be a Grid.") return grid