Public Python API#
BornSim computes scattering from finite voxel samples. Choose physical units, spatial and angular configurations explicitly. Grid.from_extent derives an isotropic voxel spacing from explicit axis extents and shape. GaussianMedium, ExponentialMedium, and WhittleMaternMedium generate random fields; RandomSphereMedium places identical nonoverlapping spheres. StructuredMedium voxelizes backgrounds and shapes and can add a seeded fluctuation field over the full structure. Solver accepts either AngularSampling or Directions for fixed volumes, or an ensemble of generated volumes. Integrated coefficients describe finite-sample cross sections divided by volume. Infinite-medium analytical formulas are test references, not public solvers.
Numerical calculation#
from bornsim import AngularSampling, Directions, Grid, Solver, Source
from bornsim.medium.random_medium import GaussianMedium
from bornsim.units import ureg
grid = Grid(
shape=(4, 4, 4),
spacing=40 * ureg.nanometer,
)
medium = GaussianMedium(
background_refractive_index=1.33,
refractive_index_std=0.01,
correlation_length=80 * ureg.nanometer,
)
volume = medium.to_volume(
grid=grid,
seed=42,
)
sampling = AngularSampling(
start=0 * ureg.degree,
end=180 * ureg.degree,
n_points=19,
)
source = Source(wavelength=633 * ureg.nanometer)
solver = Solver(
source=source,
sampling=sampling,
order=2,
)
result = solver.solve(target=volume)
print(f'mu_s = {result.mu_s}, g = {result.g}')
directions = Directions(
vectors=[
[0, 0, 1],
[1, 0, 0],
],
)
directional_result = solver.solve(
target=volume,
sampling=directions,
)
cut = solver.solve_cut(
target=volume,
directions=directions,
)
np.testing.assert_allclose(
directional_result.differential.magnitude,
cut.differential.magnitude,
)
Field construction#
from bornsim import Grid, Layer, StructuredMedium, Volume
from bornsim.medium.random_medium import WhittleMaternMedium
from bornsim.units import ureg
grid = Grid.from_extent(
shape=(16, 16, 16),
extent=(400, 400, 400) * ureg.nanometer,
)
layer = Layer.from_center_and_thickness(
centre=(0, 0, -100) * ureg.nanometer,
thickness=200 * ureg.nanometer,
refractive_index=1.335,
)
statistics = WhittleMaternMedium(
background_refractive_index=1.33,
refractive_index_std=0.002,
correlation_length=50 * ureg.nanometer,
smoothness=1.5,
)
medium = StructuredMedium()
medium.add_background(refractive_index=1.33)
medium.add_structures(layer)
medium.add_fluctuations(medium=statistics)
volume = medium.to_volume(
grid=grid,
seed=42,
)
absolute_refractive_index = volume.refractive_index
imported_volume = Volume.from_refractive_index(
refractive_index=absolute_refractive_index,
background_refractive_index=1.33,
grid=grid,
)
Alternatively, independently generated volumes with matching grids and
backgrounds can be combined with volume_a + volume_b. The result’s
refractive_index property exposes its immutable absolute voxel field.
Volume.from_refractive_index also constructs a volume from an absolute
unitless index array, converting it to the linearized contrast representation.
Configuration#
Incident illumination for BornSim scattering calculations.
- class bornsim.source.Source(*, wavelength)[source]#
Define an unpolarized plane wave propagating along the positive z axis.
- Parameters:
wavelength (Quantity) – Positive, finite vacuum wavelength. Explicit length units are required; the supplied units are preserved. A wavelength must be supplied.
- wavelength#
Scalar vacuum wavelength with its supplied length units.
- Type:
Quantity
- Raises:
ValueError – If the wavelength is nonpositive, nonfinite, nonscalar, or has units incompatible with length.
- Parameters:
wavelength (Quantity)
Notes
The transverse x and y incident polarizations have equal weights and are averaged incoherently. Arbitrary incidence directions and polarized illumination are not currently supported.
Examples
>>> from bornsim import Source >>> from bornsim.units import ureg ... ... >>> source = Source(wavelength=633 * ureg.nanometer) >>> source.wavelength.check("[length]") True
Reusable centred cubic voxel grids with unit-bearing coordinates.
- class bornsim.grid.Grid(*, shape, spacing)[source]#
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
from_extent()when the full physical voxel-box extent is known instead of the voxel spacing.Notes
Voxel centres are
(i - (N - 1)/2) * spacingon 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.- classmethod from_extent(*, shape, extent)[source]#
Create an isotropic voxel grid from its full physical extent.
extentgives the full voxel-box width along x, y, and z. Each axis extent must imply the same cubic voxel spacing.- Parameters:
shape (tuple[int, int, int])
extent (Quantity)
- Return type:
- property metadata: dict[str, object]#
Fresh JSON-compatible grid settings in SI units.
- property positions: Quantity#
Unit-bearing voxel-centre coordinates, shape
(*shape, 3).
- property volume: Quantity#
Physical voxel-box volume in cubic metres.
Validated observation directions for numerical scattering.
- class bornsim.directions.Directions(*, vectors)[source]#
Define an ordered collection of Cartesian unit observation vectors.
- Parameters:
vectors (array_like or Quantity) – Explicit dimensionless vectors of shape (n, 3), with 1 to 16384 rows. Every vector must be finite and have unit length. Vectors are copied into an immutable array; they are never normalized automatically.
Notes
Coordinates use the global x, y, z axes. The incident wave propagates along positive z. Vector order is preserved in scattered amplitudes. Use
vectorsto access the NumPy array andfrom_angles()to construct vectors from explicitly unit-bearing spherical coordinates.- classmethod from_angles(*, polar_angles, azimuth_angles)[source]#
Convert paired or broadcastable spherical angles into unit vectors.
Polar angles are measured from positive z and lie in [0, pi]. Azimuth angles are measured from positive x toward positive y and are periodic. Both inputs require explicit angular units. Broadcast coordinates are flattened in row-major order, preserving polar/azimuth grid ordering.
- Parameters:
polar_angles (Quantity)
azimuth_angles (Quantity)
- Return type:
Full directional output sampling and independent solid-angle quadrature.
- class bornsim.sampling.AngularSampling(*, angles=None, start=None, end=None, n_points=None, polar_samples=32, azimuth_samples=8)[source]#
Configure directional plots and their solid-angle normalization.
- Parameters:
angles (Quantity, optional) – One-dimensional output polar angles theta, from 0 to pi, with 1 to 181 entries. Angular values require explicit units. Cannot be combined with start, end or n_points. Omit to generate an evenly spaced range.
start (Quantity, optional) – Inclusive range endpoints between 0 and pi. Angular values require explicit units. Defaults are 0 and pi; descending ranges are supported.
end (Quantity, optional) – Inclusive range endpoints between 0 and pi. Angular values require explicit units. Defaults are 0 and pi; descending ranges are supported.
n_points (int, optional) – Number of evenly spaced output angles, from 1 to 181. Default is 121. A single point samples start.
polar_samples (int, optional) – Gauss-Legendre integration nodes in cos(theta), from 16 to 128. Default is 32. These are independent of the output angles.
azimuth_samples (int, optional) – Uniform azimuth directions in [0, 2*pi), from 4 to 32. Default is 8. Used for both directional output and solid-angle integration.
Notes
Intensities are retained at every output (theta, phi). One-dimensional curves select sampled meridians; azimuth averages require an explicit azimuth_average() call. 3D phase surfaces use directional data. Integration uses
dOmega = dphi * d(cos(theta)). Each Born order’s phase density is its intensity divided by its integrated coefficient. Refine both quadratures independently of output plot resolution.- property azimuths: Quantity#
Return uniform output and integration azimuths.
- Returns:
Azimuth directions spanning one full turn, in radians.
- Return type:
Quantity
- check_work(*, grid, order, realizations=1)[source]#
Reject synchronous workloads above 100 million voxel-direction-orders.
- Parameters:
grid (Grid) – Spatial grid used to estimate the number of voxel-direction pairs.
order (int) – Highest cumulative Born order in the requested calculation.
realizations (int, optional) – Number of independent volumes; defaults to 1.
- Returns:
Returns normally when the estimated workload is within the limit.
- Return type:
None
- Raises:
ValueError – If the estimate exceeds 100 million voxel-direction-orders.
- property directions: Directions#
Return unit directions for output and integration grids.
- Returns:
Directions with output polar angles first, followed by the Gauss-Legendre integration nodes, for each azimuth.
- Return type:
- property metadata: dict[str, list[int | float] | int]#
Return fresh SI sampling settings for reproducible result archives.
- Returns:
Output angles in radians and the polar and azimuthal sample counts.
- Return type:
dict[str, list[int | float] | int]
- summarize(*, result)[source]#
Reduce numeric Born data while retaining all directional intensities.
- Parameters:
result (BornResult) – Numeric Born amplitudes, cumulative differential data, and isolated-term differential data for one volume.
- Returns:
Numeric arrays with these keys:
meanandtermshave shape (order, output angle);directional_termsanddirectional_differentialhave shape (order, output angle, azimuth);directional_amplitudesadds incident polarization and vector-component axes of sizes two and three;integralshas shape (order, 3), for total, first-moment, and reduced coefficients.- Return type:
dict[str, numpy.ndarray]
Notes
Output amplitudes retain (order, theta, phi, incident polarization, vector component). Integrated coefficients are finite-sample cross sections divided by voxel-box volume, not intrinsic material values.
Reproducible independent-realization sampling configurations.
- class bornsim.ensemble_sampling.EnsembleSampling(*, realizations=None, seed=None, seeds=None)[source]#
Choose consecutive seeds or an explicit ordered set of distinct seeds.
realizations defaults to four and seed to zero when seeds is omitted. Explicit seeds cannot be combined with realizations or seed. Seeds range from zero to 2**32-1; there are 1 to 32 independent realizations. Duplicate seeds are rejected because they do not provide independent uncertainty.
- Parameters:
realizations (int)
seed (int)
seeds (tuple[int, ...])
- property metadata#
Return exact ordered seeds for reproducibility.
Media and geometry#
Numerical random-medium statistics, independent of analytical solutions.
- class bornsim.medium.base.Medium[source]#
Abstract interface for media that can be sampled into a finite volume.
Instantiate
bornsim.GaussianMediumorbornsim.StructuredMedium, not this base class. Concrete media specify a uniformbackground_refractive_indexand implementto_volume()using cubic voxels, SI spacing, and centred coordinates. The resulting field storesdelta_refractive_index = n(r) - n0; propagation retainsepsilon_r = n0**2 + 2*n0*delta_refractive_index.- add_background(*, refractive_index=None, medium=None, material=None)[source]#
Configure a composable medium’s background in place.
StructuredMedium accepts exactly one of a positive uniform
refractive_indexor a RandomFieldMedium supplied asmedium. Statistical media describe homogeneous distributions; use StructuredMedium to compose material.
- add_structures(*structures)[source]#
Append positional material shapes to a composable medium in place.
StructuredMedium supports Layer, Sphere, Ellipsoid, Box, and Cylinder. Later structures replace earlier material wherever their masks overlap.
- property is_random: bool#
Whether independent generation seeds represent random realizations.
- property metadata: dict[str, object]#
Return a fresh JSON-compatible description with numeric SI values.
Concrete media extend this dictionary with their own statistics or geometry. Generation seeds and grids belong to individual volumes.
Seeded collections of identical, nonoverlapping spheres.
- class bornsim.medium.random_spheres.RandomSphereMedium(*, background_refractive_index, sphere_refractive_index, radius, sphere_count, max_placement_attempts=10000)[source]#
Place a fixed number of identical spheres by sequential rejection sampling.
Centres are proposed uniformly inside the grid box, inset by the radius so entire spheres remain inside. Proposals overlapping an accepted sphere are rejected. This defines a sequential placement distribution, rather than an equilibrium hard-sphere distribution. Dense configurations may fail to fit within
max_placement_attempts; failure raises instead of returning fewer spheres. Voxelization samples material at voxel centres, so an unresolved sphere can occupy no voxels.Both refractive indices are required plain numbers. Radius requires scalar length units. Each realization records its medium and generation seed.
- Parameters:
background_refractive_index (float)
sphere_refractive_index (float)
radius (Quantity)
sphere_count (int)
max_placement_attempts (int)
- property is_random: bool#
Whether independent generation seeds represent random realizations.
- property metadata: dict[str, object]#
Return a fresh JSON-compatible description with numeric SI values.
Concrete media extend this dictionary with their own statistics or geometry. Generation seeds and grids belong to individual volumes.
Seeded random fields with explicitly chosen spatial covariance.
- class bornsim.medium.random_medium.ExponentialMedium(*, background_refractive_index, refractive_index_std, correlation_length)[source]#
Exponential spatial covariance: C(r) = refractive_index_std² exp(-r/ell).
- Parameters:
background_refractive_index (float)
refractive_index_std (float)
correlation_length (Quantity)
- class bornsim.medium.random_medium.GaussianMedium(*, background_refractive_index, refractive_index_std, correlation_length)[source]#
Gaussian spatial covariance: C(r) = refractive_index_std² exp(-r²/(2 ell²)).
- Parameters:
background_refractive_index (float)
refractive_index_std (float)
correlation_length (Quantity)
- class bornsim.medium.random_medium.RandomFieldMedium(*, background_refractive_index, refractive_index_std, correlation_length)[source]#
Abstract spectral synthesis shared by covariance-specific random fields.
Fields have Gaussian one-point statistics. Synthesis preserves expected variance and the DC mode, using a doubled box followed by cropping. Individual realizations are never recentered or rescaled.
- Parameters:
background_refractive_index (float)
refractive_index_std (float)
correlation_length (Quantity)
- abstract property correlation: str#
Spatial covariance family recorded in provenance.
- property is_random: bool#
Statistical media support independent seeded realizations.
- property metadata: dict[str, object]#
Return independent SI statistics for result provenance.
Lengths use the
_msuffix. This describes ensemble statistics rather than a particular seed or a sample’s measured mean and variance.
- spectral_weight(*, q_squared)[source]#
Return dimensionless spectrum shape at squared wavenumbers in m^-2.
The zero-wavenumber weight is one. This is not the normalized continuum power spectral density; the synthesis code supplies discrete variance normalization. Inputs must be finite and nonnegative.
- Parameters:
q_squared (Quantity)
- Return type:
ndarray
- to_volume(*, grid, seed=0)[source]#
Generate a seeded random refractive-index Volume on an explicit Grid.
The field has a Gaussian one-point probability distribution for every spatial covariance model. Spectral synthesis uses a doubled periodic box and crops its first half. Expected point variance is normalized; individual samples are never recentered or rescaled. Grid resolution and synthesis-box size can alter sampled covariance. The seed defaults to zero and remains explicit in the generated volume’s provenance.
- class bornsim.medium.random_medium.WhittleMaternMedium(*, background_refractive_index, refractive_index_std, correlation_length, smoothness)[source]#
Whittle–Matérn covariance with explicitly chosen positive smoothness.
The spectrum is proportional to (1 + (q ell)²/(2 smoothness))**(-smoothness-3/2). Smoothness 0.5 reproduces ExponentialMedium with the same correlation length.
- Parameters:
background_refractive_index (float)
refractive_index_std (float)
correlation_length (Quantity)
smoothness (float)
Voxel-centre geometry for finite structured dielectric samples.
Lengths use SI metres or compatible quantities. Shapes set absolute refractive index, not dielectric contrast; Volume retains the solver’s linearized constitutive law. Interfaces are sampled at cell centres without subvoxel averaging.
- class bornsim.geometry.Box(*, centre=<factory>, refractive_index=None, material=None, size, rotation=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))[source]#
Set absolute refractive index where
abs(local) <= size/2in every axis.Size contains three positive full side lengths along the local axes. All lengths use SI or compatible quantities; the boundary is closed.
- Parameters:
- class bornsim.geometry.Cylinder(*, centre=<factory>, refractive_index=None, material=None, radius, height, axis=2, rotation=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))[source]#
A finite circular cylinder with absolute refractive index and closed boundary.
Membership requires
abs(local[axis]) <= height/2and squared transverse distance <= radius**2. Radius and full height are positive SI lengths or quantities; axis is 0, 1, or 2, default z. Centre has three lengths.- Parameters:
centre (Quantity)
refractive_index (float | None)
material (Material | None)
radius (Quantity)
height (Quantity)
axis (int)
rotation (Rotation | tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | ndarray[tuple[Any, ...], dtype[float64]] | Sequence[Sequence[float]] | Quantity)
- class bornsim.geometry.Ellipsoid(*, centre=<factory>, refractive_index=None, material=None, radii, rotation=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))[source]#
Set absolute refractive index where
sum((local/radii)**2) <= 1.Three positive semi-axis radii align with local x, y, z. All lengths use SI or compatible quantities. rotation maps local semi-axes into global coordinates.
- Parameters:
- class bornsim.geometry.Layer(*, centre=<factory>, refractive_index=None, material=None, lower, upper, axis=2, rotation=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))[source]#
A slab with absolute refractive index inside
lower <= local[axis] < upper.Bounds are SI lengths in the slab local frame, offset by centre and oriented by rotation. The default axis is z (2); x and y are 0 and 1. The half-open interval gives adjacent slabs an unambiguous interface. The slab spans the voxel box in transverse axes; it represents a finite sample, not an infinite planar background.
Use
from_center_and_thickness()to specify a slab by its centre and full thickness instead of its local lower and upper bounds.- Parameters:
centre (Quantity)
refractive_index (float | None)
material (Material | None)
lower (Quantity)
upper (Quantity)
axis (int)
rotation (Rotation | tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | ndarray[tuple[Any, ...], dtype[float64]] | Sequence[Sequence[float]] | Quantity)
- classmethod from_center_and_thickness(*, thickness, centre, refractive_index=None, material=None, axis=2, rotation=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))[source]#
Create a slab from its centre and full thickness along
axis.- Parameters:
thickness (Quantity)
centre (Quantity)
refractive_index (float | None)
material (Material | None)
axis (int)
rotation (Rotation | tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | ndarray[tuple[Any, ...], dtype[float64]] | Sequence[Sequence[float]] | Quantity)
- Return type:
- class bornsim.geometry.Sphere(*, centre=<factory>, refractive_index=None, material=None, radius)[source]#
Set absolute refractive index where
sum((r-centre)**2) <= radius**2.Radius is positive; centre is a three-component SI length or quantity. The closed surface is sampled at voxel centres, giving a staircase boundary.
- Parameters:
centre (Quantity)
refractive_index (float | None)
material (Material | None)
radius (Quantity)
- class bornsim.geometry.StructuredMedium(*, regions=(), background_refractive_index=None, overlap='replace', warn_on_clipping=False)[source]#
Compose ordered material regions in a uniform background.
- Parameters:
regions (sequence of Layer, Sphere, Ellipsoid, Box, or Cylinder) – Regions set absolute refractive index. Overlap precedence follows the overlap policy; indices are not added. Default is empty.
background_refractive_index (float) – Explicit positive uniform background refractive index. It fills uncovered voxels and extends outside the finite voxel box.
overlap ({"replace", "preserve", "error"}) – Later regions win, earlier regions win, or reject shared voxel centres.
warn_on_clipping (bool) – Warn when geometry extends beyond the voxel box; default False.
fluctuations (RandomFieldMedium, configured with
add_fluctuations()) – Optional additive field applied over the background and every region.
Notes
Configure this mutable builder with
add_background,add_structures, and optionallyadd_fluctuations. An empty builder has no background; voxelization requires an explicit one. Medium remains abstract; random statistics and geometry shapes remain frozen. Calls update the builder and return None. Previously generated volumes do not change. Constructor regions are still accepted for existing callers. Voxelization returnsdelta_refractive_index(r) = n(r) - background_refractive_index. A configured additive fluctuation field is added after the absolute-index regions are composed. The numerical solver usesepsilon_r = n0**2 + 2*n0*delta_refractive_index, omittingdelta_refractive_index**2even at higher Born orders. Strong refractive index differences can invalidate this constitutive approximation or Born iteration. Regions crossing the box are clipped. Layers therefore have finite lateral extent; this is not a transfer-matrix or layered-background Green solver.- add_background(*, refractive_index=None, medium=None, material=None)[source]#
Set the background in place, retaining all existing structures.
Supply exactly one of
refractive_index(a positive uniform refractive index) ormedium(RandomFieldMedium statistics). A random background is sampled with to_volume(seed=…) and fills only voxels outside structures. The exterior scattering background remains uniform at background_refractive_index. Replacing a background changes the reference n0, not structures’ absolute refractive indices. Validation precedes any state change. Returns None. Random statistics are copied when added.
- add_fluctuations(*, medium)[source]#
Set an additive random field applied over the background and all structures.
The statistical medium must share the configured uniform background. This replaces any previously configured additive fluctuation field. Random backgrounds and additive fluctuations are mutually exclusive. Returns None. Generated volumes record the medium and seed as provenance.
- add_structures(*structures)[source]#
Append material regions in argument order; return None.
Accepts any number of positional Layer, Sphere, Ellipsoid, Box, or Cylinder objects. Later arguments replace earlier material in overlaps and replace a random background inside their masks. Separately configured additive fluctuations remain present over every region. All arguments are validated before editing the builder; an empty call is a no-op. Existing voxel samples are independent snapshots.
- property is_random#
Random backgrounds or additive fluctuations supply independent realizations.
- property metadata#
Return independent SI geometry descriptions in application order.
Region type, absolute refractive index, axis where applicable, and dimensions are recorded. Length keys have the
_msuffix, and vectors are JSON lists. The list order preserves replacement precedence in overlapping regions. Configured additive fluctuation statistics are recorded underfluctuations. Generated volumes with fluctuations retain a snapshot of this metadata and their seed.
- to_volume(*, grid, seed=0)[source]#
Voxelize this medium on an explicit Grid.
An explicit background is required. Ordered shapes set absolute refractive indices; overlap follows replace, preserve, or error. A random background uses the supplied seed and is replaced by structures inside their masks. Additive fluctuations use the same seed and are applied over all regions.
Immutable, reusable homogeneous optical materials.
- class bornsim.material.Material(*, refractive_index)[source]#
A nondispersive real refractive index shared by any number of shapes.
refractive_indexis an absolute positive dimensionless refractive index, not a contrast. Supply a plain number; quantities are rejected. Absorption and wavelength dispersion are not implemented.- Parameters:
refractive_index (float)
- property metadata#
Fresh JSON-compatible material description.
Rotation inputs accept a bornsim.rotation.Rotation, a nested matrix
sequence, a NumPy array, or a dimensionless quantity. Shapes normalize accepted
inputs to immutable bornsim.rotation.RotationMatrix tuples;
bornsim.rotation.RotationInput describes the accepted forms.
Proper three-dimensional rotations for immutable material shapes.
- class bornsim.rotation.Rotation(*, axis, angle)[source]#
Define an active right-handed rotation about a dimensionless axis.
angle accepts angular quantities; angular values require explicit units. The axis is normalized.
matrixmaps local column coordinates into global coordinates. It follows the right-hand rule, and successive world rotations compose asR_new @ R_existing. Shape membership uses local row coordinates(positions - centre) @ R. Shapes accept this object, a proper orthonormal 3-by-3 matrix, or a dimensionless matrix quantity; they store the normalized matrix as an immutable tuple of tuples. No voxel resampling is performed.- Parameters:
axis (tuple[float, float, float])
angle (Quantity)
- property matrix: ndarray[tuple[Any, ...], dtype[float64]]#
Return Rodrigues’ active rotation matrix, shape (3, 3).
Finite refractive-index fields on centred cubic voxel grids.
- class bornsim.volume.Volume(*, delta_refractive_index, background_refractive_index, grid, medium=None, seed=None)[source]#
Store a finite voxel field and its explicit spatial Grid.
The copied, read-only delta_refractive_index array has shape grid.shape.
refractive_indexreturns the read-only absolute fieldbackground_refractive_index + delta_refractive_index. Addition combines contrasts from volumes only when their grids and backgrounds match. Background refractive index is a plain number; grid spacing and positions carry physical units. Relative permittivity is linearized as background_refractive_index**2 + 2*background_refractive_index*delta_refractive_index. The quadratic fluctuation term is omitted at every Born order. Medium.to_volume records generation statistics and seed.medium_metadatareturns the metadata snapshot captured when the volume was created. Manually constructed volumes have only their explicitly supplied provenance.Use
from_refractive_index()when the input array contains absolute refractive indices rather than fluctuations from the background.- Parameters:
- classmethod from_refractive_index(*, refractive_index, background_refractive_index, grid, medium=None, seed=None)[source]#
Construct a volume from an absolute refractive-index field.
- Parameters:
refractive_index (array_like) – Plain numerical absolute refractive indices, with shape
grid.shape. Values must not carry units.background_refractive_index (float) – Positive, unitless reference refractive index.
grid (Grid) – Explicit spatial grid matching the field shape.
medium (Medium, optional) – Medium metadata associated with generation. Must be supplied together with
seed.seed (int, optional) – Generation seed, supplied together with
medium.
- Returns:
Immutable volume storing the field as
refractive_index - background_refractive_index.- Return type:
- Raises:
ValueError – If the field, background, grid shape, or resulting linearized permittivity is invalid.
TypeError – If
mediumis not a Medium instance.
- property medium_metadata: dict[str, object] | None#
Return a copy of the generation metadata captured at construction.
- Returns:
A fresh copy of the source medium’s metadata, or None when the volume has no associated medium.
- Return type:
dict[str, object] or None
- plot_3d(*, backend='plotly', mode=None, field='delta_refractive_index', length_unit='nanometer', surface_count=8, opacity=None, opacity_scale='uniform')[source]#
Build a three-dimensional figure of the finite voxel medium.
- Parameters:
backend ({'matplotlib', 'plotly'}, optional) – Default is Plotly for browser interaction. Select ‘matplotlib’ explicitly for a Matplotlib figure.
mode (str, optional) – Matplotlib supports ‘voxels’ (default). Plotly supports ‘volume’ (default) and ‘isosurface’. Uniform Plotly fields are rendered as volume data; an isosurface cannot be generated from a uniform field.
field ({'delta_refractive_index', 'refractive_index', 'permittivity'}, optional) – Scalar field to display, default refractive index fluctuation. ‘refractive_index’ displays n0 + delta_refractive_index; ‘permittivity’ displays the solver’s linearized relative permittivity n0**2 + 2*n0*delta_refractive_index, not (n0+delta_refractive_index)**2.
length_unit (str, optional) – Spatial display unit, default ‘nanometer’. Stored SI data is unchanged.
surface_count (int, optional) – Number of contours, from 1 to 32; default 8. More surfaces increase browser rendering cost. Used only by Plotly volume and isosurface modes.
opacity (float, optional) – Surface opacity in (0, 1]. Default is 1 for Matplotlib and 0.15 for Plotly.
opacity_scale ({'uniform', 'increasing'}, optional) – Default ‘uniform’ gives all contours the same opacity. ‘increasing’ is available only for Plotly volume mode and scales opacity from zero at the lowest displayed contour to
opacityat the highest. Use field=’refractive_index’ to emphasize high refractive index, rather than the magnitude of positive and negative fluctuations.
- Returns:
figure – Matplotlib: call pyplot.show() to display or figure.savefig() to export. Rotation needs an interactive Matplotlib backend; gallery images are static. Plotly: call figure.show() or figure.write_html(). Constructing a figure opens no window or browser.
- Return type:
matplotlib.figure.Figure or plotly.graph_objects.Figure
- Raises:
ValueError – If a mode, scalar field, display unit, or rendering setting is invalid.
Notes
Matplotlib voxels preserve staircase interfaces. Dense transparent scenes can have depth-ordering limitations in Matplotlib. Plotly contours interpolate between voxel centres and are visualization aids, not exact curved interfaces or a replacement for grid refinement. Aspect ratio follows physical dimensions. All voxels are retained, with at most 32**3 samples under Volume’s grid limit; browser performance also depends on surface count and graphics support. This plots the input material, not an electromagnetic field.
- plot_slice(*, normal='z', index=None, field='refractive_index', length_unit='nanometer', title=None)[source]#
Build a Matplotlib figure of one voxel plane.
- Parameters:
normal ({'x', 'y', 'z'}, optional) – Axis perpendicular to the plane; default ‘z’.
index (int, optional) – Voxel index along the normal. Defaults to n//2, the positive central plane for an even grid. The title gives its actual position.
field ({'refractive_index', 'delta_refractive_index', 'permittivity'}, optional) – Display n0 + delta_refractive_index, fluctuations, or linearized relative permittivity n0**2 + 2*n0*delta_refractive_index. Default ‘refractive_index’.
length_unit (str, optional) – Spatial display unit; default ‘nanometer’. Stored SI data is unchanged.
title (str, optional) – Override the title describing the field and plane position.
- Returns:
figure – Equal-aspect image with voxel-edge extents and a labelled colorbar. Call pyplot.show() to display or figure.savefig() to export. Colors use the full volume’s range to compare different planes; fluctuation colors are symmetric about zero.
- Return type:
matplotlib.figure.Figure
- Raises:
ValueError – If the plane, index, field, or length unit is invalid.
- property positions: Quantity#
Return voxel-centre coordinates relative to the sample centre.
- Returns:
positions – Unit-bearing coordinates, shape (nx, ny, nz, 3), with the final axis ordered as x, y, z.
- Return type:
Quantity
- property refractive_index: ndarray#
Return the read-only absolute refractive-index field.
- Returns:
Absolute refractive indices, equal to the background plus
delta_refractive_index.- Return type:
numpy.ndarray
- property volume: Quantity#
Return the total physical volume of all voxels.
- Returns:
volume – Unit-bearing sample volume, equal to
delta_refractive_index.size*spacing**3.- Return type:
Quantity
Solver and results#
Solver configuration for finite-volume and ensemble Born calculations.
- class bornsim.solver.Solver(*, source, sampling=<factory>, order=3)[source]#
Configure numerical Born scattering from explicit finite samples.
Source chooses the wavelength. AngularSampling supplies output directions and integration quadrature; order sets the highest cumulative Born order. All orders retain the linearized dielectric contrast and the equal-volume Green self cell. Decreasing successive terms do not certify convergence.
- Parameters:
source (Source)
sampling (AngularSampling)
order (int)
- ensemble(*, medium, grid, sampling=None, ensemble_sampling=None)[source]#
Average numerical scattering over independently generated finite samples.
Grid selects the voxel domain. AngularSampling selects output directions and integration quadrature; the solver’s sampling is used when omitted. EnsembleSampling selects ordered independent seeds and defaults to four consecutive seeds starting at zero. Deterministic structures use solve.
Intensities are averaged after coherent summation within each realization. Standard errors quantify realization uncertainty, not discretization or finite-size error. One realization has unknown (NaN) uncertainty. Integrated coefficients are finite-sample cross sections divided by voxel-box volume.
- Parameters:
medium (Medium)
grid (Grid)
sampling (AngularSampling | None)
ensemble_sampling (EnsembleSampling | None)
- Return type:
- solve(*, target, sampling=None)[source]#
Compute scattering from a volume using angular or Cartesian sampling.
Pass AngularSampling for solid-angle integration, or Directions for an unnormalized set of observation directions. The target is an explicit voxelized Volume; random statistics use ensemble instead.
- Parameters:
target (Volume)
sampling (AngularSampling | Directions | None)
- Return type:
- solve_cut(*, target, angles=None, directions=None)[source]#
Compute an unnormalized angular cut through a fixed Volume.
Supply polar angles on the x-z meridian or a Directions configuration, but not both. This is a convenience wrapper for
solve(target=..., sampling=Directions(...)). Explicit angular units are required. Default angles span 0 to pi with 121 samples. Cut amplitudes and intensities retain every requested direction. A cut alone cannot determine a solid-angle integral; integrated coefficients and normalized phase functions are unavailable.- Parameters:
target (Volume)
angles (Quantity | None)
directions (Directions | None)
- Return type:
Unitful scattering results, validated archives, and Matplotlib plots.
- class bornsim.results.BornResult(*, directions, amplitudes, differential, term_differential, field_norms, warnings)[source]#
Store numeric SI results for one finite-volume Born calculation.
- Parameters:
directions (numpy.ndarray) – Dimensionless observation vectors, shape (observation, 3).
amplitudes (numpy.ndarray) – Complex isolated amplitudes in metres, shape (order, observation, incident polarization, vector component).
differential (numpy.ndarray) – Cumulative scattering in m^-1 sr^-1, shape (order, observation).
term_differential (numpy.ndarray) – Isolated-term scattering in m^-1 sr^-1, same shape as differential.
field_norms (numpy.ndarray) – Dimensionless relative field norms, shape (order,).
warnings (tuple of str) – Numerical-resolution and Born-term diagnostics.
- directions, amplitudes, differential, term_differential, field_norms
Arrays described above, without attached units.
- Type:
numpy.ndarray
- warnings#
Numerical diagnostics.
- Type:
tuple of str
See also
bornsim.series.BornSeries.solveProduce this numeric result.
bornsim.results.ResultUnitful result with plotting support.
Notes
Row zero denotes first order. Cumulative intensities include interference between amplitudes; isolated intensities must not be summed to reconstruct them. Integrated coefficients are not inferred from observation samples.
- class bornsim.results.Result(*, source, kind, angular=None, differential=None, sample_volume=None, azimuth_averaged=False, azimuth_stderr=None, angles=None, directions=None, azimuths=None, mu_s=None, g=None, mu_s_prime=None, amplitudes=None, term_differential=None, stderr=None, field_norms=None, warnings=(), realizations=None, provenance=None)[source]#
Store unitful scattering, diagnostics and reproducible settings.
Full solves retain
differentialandterm_differentialwith shape (order, polar angle, azimuth). Cuts and explicitly averaged results use (order, observation).amplitudesalways adds polarization and Cartesian axes of sizes two and three to the differential shape. Ensembles retain intensities and standard errors, without coherent amplitudes.angulargroups read-only coordinates, intensities, amplitudes and phase densities.azimuth_average()explicitly averages intensities and retains the covariance-correct standard error of per-realization averaged curves. Plots of full results select one sampled meridian by default.Angles use radians, amplitudes metres, differential data m^-1 sr^-1, integrated coefficients m^-1, and sample_volume m^3. Dimensional arrays require explicit units. Every full phase density uses its order’s solid-angle integral. Cuts have no inferred integrated coefficients or phase normalization.
Numerical coefficients are finite-sample cross sections divided by the entire voxel-box volume.
differential_cross_sectionrestores area per steradian using stored sample_volume.Source and kind are required. Supply angular or individual differential data, never both. Results are immutable; provenance returns a fresh copy. Other fields describe sampling, isolated terms, integrated moments, uncertainty, diagnostics and provenance. NaN errors represent one realization; NaN anisotropy represents zero scattering. Arrays and JSON-compatible provenance are copied and validated. Use differential and amplitudes for both cuts and full solves.
Save and load use schema 2 archives; unsupported schemas are rejected. Numerical cumulative intensities retain coherent amplitude interference. Isolated intensities must not be summed to reconstruct cumulative results.
- Parameters:
source (Source)
kind (str)
angular (AngularData)
g (Quantity | None)
mu_s_prime (Quantity | None)
field_norms (Quantity | None)
warnings (tuple)
realizations (int | None)
- property amplitudes#
Read-only angular data owned by angular.
- property angles#
Read-only angular data owned by angular.
- property azimuth_averaged#
Read-only angular data owned by angular.
- property azimuth_stderr#
Read-only angular data owned by angular.
- property azimuths#
Read-only angular data owned by angular.
- property differential#
Read-only angular data owned by angular.
- property differential_cross_section#
Finite-sample d-sigma/d-Omega in square metres per steradian.
- property directions#
Explicit cut coordinates; full-grid vectors are angular.directions.
- classmethod load(*, path)[source]#
Load and validate a BornSim result archive without using pickle.
- Parameters:
path (str or pathlib.Path) – Archive created by
Result.save.- Returns:
result – Restored quantities, complex amplitudes, diagnostics, and original provenance. Loading does not change the recorded package version.
- Return type:
- Raises:
ValueError – If the schema is unsupported, metadata or arrays are missing or inconsistent, or result validation fails.
OSError – If the archive cannot be read.
- meridian(*, azimuth, method='exact')[source]#
Select a physical azimuth from full data without interpolation.
- property mu_s#
Read-only angular data owned by angular.
- property phase_function#
Return directional phase density per steradian.
Full solves retain (order, polar angle, azimuth). Explicitly averaged results retain (order, polar angle). Cuts have no normalization. Every order uses its own full solid-angle scattering coefficient.
- plot(*, terms=False, log_y=False, title=None, azimuth=0)[source]#
Build a Matplotlib figure of differential scattering curves.
- Parameters:
terms (bool, optional) – Plot isolated numerical terms instead of cumulative curves. Default is False. Isolated terms omit interference and ensemble error bars.
log_y (bool, optional) – Use a logarithmic scattering axis. Default is False.
azimuth (int, optional) – Index of the sampled meridian; default zero. Use azimuth_average() explicitly to plot averaged intensities.
title (str, optional) – Override the default title.
- Returns:
figure – Figure with angles in degrees and scattering in m^-1 sr^-1. Explicit direction samples use input indices and markers. Ensemble cumulative curves show one-standard-error bars where sampling error is known.
- Return type:
matplotlib.figure.Figure
- Raises:
ValueError – If
terms=Trueand isolated-term curves are unavailable.
Notes
The figure is registered with pyplot and returned without displaying it. Call
matplotlib.pyplot.show()to display open figures orfigure.savefig(path)to export a PNG, SVG, or PDF. Zero values cannot be displayed on a log axis. A one-realization ensemble has unknown sampling error, so no error bars are shown. This method plots differential scattering, not a normalized phase function.
- plot_cross_section(*, volume=None, area_unit='nanometer**2', terms=False, log_y=False, title=None, azimuth=0)[source]#
Build differential cross-section curves for a finite sample.
- Parameters:
volume (Volume, optional) – Optional sample. Normally the recorded sample_volume supplies the conversion without retaining the input voxel field.
area_unit (str, optional) – Display area unit; default ‘nanometer**2’.
terms (bool, optional) – Show isolated terms, which omit interference and sampling error bars, instead of cumulative coherent curves. Default False.
log_y (bool, optional) – Use a logarithmic scattering axis. Default False.
azimuth (int, optional) – Index of the sampled meridian; default zero. Use azimuth_average() explicitly to plot averaged intensities.
title (str, optional) – Override the default title.
- Returns:
figure – Angle or direction curves, with ensemble standard errors scaled by the same sample volume. Returned without displaying it.
- Return type:
matplotlib.figure.Figure
- Raises:
TypeError – If volume is not a Volume.
ValueError – If recorded grid dimensions differ, area_unit is invalid, or isolated curves are unavailable.
Notes
Uses dσ/dΩ = V * differential. These are finite-sample cross sections, not intrinsic infinite-medium transport coefficients. An optional supplied volume must agree with the recorded physical volume and grid. Stored scattering data and its SI units remain unchanged.
- plot_field_norms(*, log_y=True)[source]#
Plot relative Born field-term norms for each available realization.
- Parameters:
log_y (bool, optional) – Use a logarithmic vertical axis. Default is True. Zero norms cannot be displayed on a logarithmic axis.
- Returns:
figure – One trace per realization versus one-based field-term order. Norms are dimensionless relative to the incident field.
- Return type:
matplotlib.figure.Figure
- Raises:
ValueError – If no numerical field norms are available.
Notes
The figure preserves per-realization diagnostics. Decreasing field terms do not certify Born convergence or remove discretization error.
- plot_phase_function(*, view='angular', order=None, log_y=False, azimuth=0, backend=None)[source]#
Plot normalized phase functions as angular curves, polar cuts, or a surface.
- Parameters:
view ({'angular', 'polar', '3d'}, optional) – Angular curves select a sampled meridian by default. Polar cuts use its opposite azimuth for the other half of the plane. The 3D surface uses p(theta, phi), with radius and color in sr^-1, retaining directional asymmetry. Analytical distributions are independent of phi.
order (int, optional) – One-based cumulative order. Angular and polar views show all orders by default; the 3D view shows only the highest order.
log_y (bool, optional) – Logarithmic vertical scale for angular curves. Default is False; only supported with
view='angular'.backend ({'plotly', 'matplotlib'}, optional) – The 3D view defaults to Plotly. Angular and polar views use Matplotlib. Select ‘matplotlib’ explicitly for a static 3D figure.
- Returns:
figure – Returned without displaying it. Call
figure.show()for Plotly, ormatplotlib.pyplot.show()for Matplotlib. The 3D coordinates are probability-density radii, not spatial positions.- Return type:
matplotlib.figure.Figure or plotly.graph_objects.Figure
- Raises:
ValueError – If phase normalization is unavailable, the view or order is invalid, directional data are unavailable for a numerical 3D view, log scaling is requested for another view, or polar/3D data do not span the full [0, pi] range with at least three angles.
Notes
Numerical 3D surfaces retain every sampled azimuth; no azimuth averaging is applied. For one realization this displays the fixed sample’s directional distribution; multiple realizations display the ensemble intensity average. Older archives lacking directional data must be recomputed for a directional 3D view. Angular and polar views select sampled meridians; averaging requires azimuth_average(). A flat phase function produces a spherical surface; forward scattering extends towards +z. Samples are sorted by angle for rendering. Error bars for normalized ratios are omitted because the required covariance is not stored.
- property provenance#
Return a fresh metadata copy; edits cannot change this result.
- property sample_volume#
Read-only angular data owned by angular.
- save(*, path)[source]#
Save data, units, warnings, and provenance in a versioned NPZ archive.
- Parameters:
path (str or pathlib.Path) – Destination file, typically ending in
.npz. The exact path is used without appending an extension. An existing file is replaced.- Returns:
path – Destination of the compressed NumPy archive.
- Return type:
pathlib.Path
- Raises:
ValueError – If result fields were modified into an invalid state.
OSError – If the destination cannot be written.
Notes
Arrays are stored in SI units, including complex Born amplitudes. Metadata is JSON, and loading never enables pickle. The archive stores result data rather than the original voxel field; retain that field separately for manual volumes. A field SHA-256 identifies the input. Generated volumes also record their medium and seed. Exact seeded reproduction depends on the recorded implementation versions.
- property stderr#
Read-only angular data owned by angular.
- property term_differential#
Read-only angular data owned by angular.
Consistent unitful directional scattering data for solves and cuts.
- class bornsim.angular_data.AngularData(*, differential, directions=None, amplitudes=None, angles=None, azimuths=None, term_differential=None, stderr=None, azimuth_stderr=None, mu_s=None, sample_volume=None, kind='volume', azimuth_averaged=False, meridian_azimuth=None)[source]#
Group coordinates, amplitudes, intensities and normalization.
Full angular data have shape (order, polar angle, azimuth). Cuts and explicitly averaged curves have shape (order, observation). Directions have the same observation axes and a final Cartesian axis of size three. Amplitudes add incident-polarization and vector axes of sizes two and three. Ensemble amplitudes are absent: intensities, not amplitudes, are averaged across realizations. Arrays are copied, unitful and read-only.
- Parameters:
differential (Quantity)
directions (Quantity | None)
amplitudes (Quantity | None)
angles (Quantity | None)
azimuths (Quantity | None)
term_differential (Quantity | None)
stderr (Quantity | None)
azimuth_stderr (Quantity | None)
mu_s (Quantity | None)
sample_volume (Quantity | None)
kind (str)
azimuth_averaged (bool)
meridian_azimuth (Quantity | None)
- property differential_cross_section#
Finite-sample differential cross section in square metres per sr.
- meridian(*, azimuth, method='exact')[source]#
Select a sampled meridian using an angle, preserving normalization.
Azimuth requires explicit angular units and is periodic modulo 2*pi. exact requires a sampled azimuth within 1e-10 radians; nearest explicitly selects the closest sample. No interpolation or azimuth averaging is performed. meridian_azimuth records the actual selected angle.
- property phase_function#
Density per steradian, normalized by the full solid-angle integral.
Numerical engine#
Reusable finite-volume vector Born-series engine.
Time convention exp(-i omega t). Relative dielectric contrast is linearized as 2 n0 delta_n at every order. Amplitudes interfere coherently before the unpolarized intensity is evaluated.
- class bornsim.series.BornSeries(*, grid, background_refractive_index, wavelength, directions, order=3)[source]#
Own the fixed geometry and propagation operator for numerical Born orders.
- Parameters:
grid (Grid) – Shared spatial configuration, also used by the input Volume. Required for generation and scattering.
background_refractive_index (float) – Positive uniform background refractive index, dimensionless.
wavelength (Quantity) – Positive vacuum wavelength; explicit length units are required.
directions (Directions) – Validated, immutable Cartesian unit observation vectors.
order (int, optional) – Highest cumulative Born order, from 1 to 12; default 3.
- operator#
One open-boundary FFT Green operator reused for every compatible volume.
- Type:
bornsim.green.GreenOperator
Notes
The cached incident plane wave travels along +z with x and y polarizations. For each call,
E[0] = E_incandE[m] = K(chi*E[m-1]), wherechi = 2*n0*delta_refractive_indexand K integrates the background dyadic Green tensor multiplied byk0**2. Order-m far-field amplitude is computed fromchi*E[m-1]. Cumulative intensity is|sum_m f[m]|**2, averaged over the two polarizations and divided by the entire voxel-box volume.Geometry, incident field, and the Green tensor are reused; contrast, propagated fields, amplitudes, and diagnostics are local to each solve. Compatible volumes must have exactly the configured grid and background. Configuration is frozen to prevent stale propagation settings. Decreasing field terms do not establish universal convergence; the dielectric linearization is unchanged at higher orders.
- solve(*, volume)[source]#
Compute coherent scattering from one compatible finite refractive index volume.
- Parameters:
volume (Volume) – Fixed fluctuation field with positive linearized permittivity, matching this engine’s grid spacing, dimensions and background.
- Returns:
result – Fresh numeric SI amplitudes, cumulative and isolated intensities, relative field norms, and diagnostic messages.
- Return type:
- Raises:
TypeError – If volume is not a Volume.
ValueError – If grid or background differs from the engine configuration, or iteration produces nonfinite fields.
Notes
Each solve starts with the incident field. Amplitudes are summed before squaring, preserving interference. Intensities are finite-sample cross sections divided by the entire voxel-box volume, not intrinsic infinite-medium transport coefficients. The dielectric contrast is
2*n0*delta_refractive_indexat every order. Growing field terms generate a diagnostic; decreasing terms do not certify convergence.Examples
>>> from bornsim.units import ureg >>> from bornsim import Directions, Grid, GaussianMedium >>> from bornsim.series import BornSeries ... ... >>> medium = GaussianMedium( ... background_refractive_index=1.33, ... refractive_index_std=0.01, ... correlation_length=100e-9 * ureg.meter, ... )
… >>> grid = Grid( … shape=(2, 2, 2), … spacing=50 * ureg.nanometer, … ) >>> volume = medium.to_volume( … grid=grid, … seed=42, … ) >>> directions = Directions(vectors=[[0.0, 0.0, 1.0]]) >>> engine = BornSeries( … grid=grid, … background_refractive_index=volume.background_refractive_index, … wavelength=633 * ureg.nanometer, … directions=directions, … order=2, … ) >>> result = engine.solve(volume=volume) >>> result.amplitudes.shape (2, 1, 2, 3)
Numerical ensemble quadrature and realization sampling uncertainty.
- bornsim.ensemble.ensemble_scattering(*, medium, wavelength, grid, sampling=None, order=3, ensemble_sampling=None)[source]#
Run numerical Born ensembles on an explicit Grid.
A random Medium generates one Volume per ordered seed in EnsembleSampling. AngularSampling defines output observations and independent integration quadrature. Complex amplitudes interfere within each realization; intensities are averaged across realizations. The returned dictionary contains numeric SI directional intensities, isolated-term intensities, integrated moments, standard errors, per-realization field norms, and diagnostic warnings. Angles and azimuths retain quantities from the sampling configuration.
Coefficients are finite-sample cross sections divided by voxel-box volume. Standard errors describe realization sampling; one realization produces NaN. Check spatial, angular, finite-size, and ensemble convergence separately.
- Parameters:
medium (Medium)
wavelength (Quantity)
grid (Grid)
sampling (AngularSampling | None)
order (int)
ensemble_sampling (EnsembleSampling | None)
- Return type:
dict[str, Any]
Units#
Shared TypedUnit registry and explicit input validation.
Validation preserves the supplied units. Dimensional inputs require quantities; dimensionless inputs may be bare numbers. Numerical kernels, plotting and serialization explicitly select units when they need magnitudes.
Archives#
Results save and load schema-2 NPZ archives with explicit SI units and no pickle. Unsupported schemas are rejected. Arrays and provenance are copied when results are constructed. Result.angular holds immutable directional data; .vectors on Directions exposes the observation configuration’s immutable Cartesian array.