Changing numerical and scientific code
Scientific reliability has priority over cleanup, abstraction, and speed. A numerical refactor is successful only when it preserves the intended physical quantity, units, shapes, conventions, tolerances, and supported domain.
Start from current behaviour
Before changing a formula or algorithm:
identify the current implementation and every caller;
capture current behaviour with focused characterization tests;
record units, shapes, axis order, dtype, and edge-case behaviour;
identify an analytical, legacy, published, experimental, or independent computational reference;
make the smallest possible change;
compare old and new results over ordinary and difficult cases;
optimize only after the validated implementation is stable.
A characterization test is not automatically a scientific validation. It protects against accidental behavioural drift while the correctness of that behaviour is assessed separately.
Units, conventions, and shapes
Every public numerical function should make the following unambiguous:
units of every input and output;
whether values are per cell, formula unit, atom, mole, or unit volume;
tensor convention and coordinate frame;
axis order and array shape;
whether masks and uncertainties align with the leading dimensions;
expected dtype.
For example, (nT, nP, 6, 6) and (nP, nT, 6, 6) are not interchangeable
merely because they contain the same number of values. Axis order is part of
the scientific contract.
Precision policy
Use the central precision helpers in quantas.core.numerics when generic
serialization or normalization needs to preserve the Quantas policy.
Scientific arrays use float64 and complex arrays use complex128.
Do not:
introduce a user-selectable single-precision mode;
downcast HDF5 values to save space;
round arrays before storing them;
use report precision as numerical precision;
mix dtype changes with an unrelated refactor.
Tolerances are scientific decisions
A tolerance should have:
a name;
a unit or a clear dimensionless meaning;
a documented default;
a test at and around the threshold;
a rationale connected to numerical noise or physical resolution;
an explicit consequence when the threshold is exceeded.
Do not use a larger tolerance to hide an unstable tensor, an ill-conditioned fit, a discontinuous mode assignment, or an invalid external-code output.
Validation, extrapolation, and failure
Distinguish these states:
validThe requested quantity is defined and inside the supported numerical domain.
extrapolatedA value was calculated outside the sampled support according to a documented policy. Preserve a mask and warning.
not applicableThe quantity has no physical definition for the selected method or state.
unresolvedThe base quantity may be valid, but a branch, derivative, or decomposition is ambiguous, for example a shear polarization at a degeneracy.
invalidThe requested result cannot be interpreted safely.
A finite number is not necessarily a valid result. Do not replace invalid states with zero, clip unstable eigenvalues, or silently copy a fallback value unless the method explicitly defines and records that fallback.
Progress callbacks
Core and numerical objects do not emit Quantas events. A long loop may accept a simple callback:
def solve_states(
states: list[object],
progress_callback: Callable[[int, int], None] | None = None,
) -> list[object]:
results = []
for current, state in enumerate(states, start=1):
results.append(solve_one(state))
if progress_callback is not None:
progress_callback(current, len(states))
return results
The calculator translates this into EventLevel.PROGRESS with a normalized
fraction. This preserves reuse in tests, notebooks, and future frontends.
Uncertainties and covariance
When a result includes uncertainty:
state whether it is observational, fitted, propagated, or heuristic;
preserve covariance when derived quantities share parameters;
use Jacobians or resampling consistently with the documented method;
distinguish statistical uncertainty from model discrepancy;
preserve masks when an uncertainty is unavailable;
test deterministic seeds for stochastic propagation.
Do not create a zero uncertainty for a missing covariance. Zero means a
known exact value; unavailable means something different.
Optimization and performance
Use NumPy and SciPy first. Before adding an accelerator:
profile a realistic workflow;
identify the dominant operation;
establish a numerical baseline;
improve array layout, repeated allocation, or algorithmic complexity;
measure wall time and memory again;
verify identical scientific results.
Numba or another accelerator must not be introduced without measured benefit and a maintenance plan. A faster algorithm that changes branch selection, tolerances, or reproducibility is a scientific change, not a transparent optimization.
Batch sizes and sampling densities are different
A batch size generally controls memory and throughput. A sampling density controls scientific resolution. Document them separately.
Changing a batch size should leave results unchanged. Changing an angular, pressure, temperature, or interpolation grid may alter extrema or derivatives and requires convergence testing.
Recommended scientific test ladder
For a new numerical method, build evidence in this order:
exact identities and input validation;
analytical or manufactured cases;
invariance tests, such as rotation or unit conversion;
comparison with the legacy implementation;
comparison with an independent package or published reference;
real-data regression with frozen provenance;
CLI/API and HDF5 round-trip equivalence;
performance measurement after correctness is established.
Document tolerances separately for numerical regression and scientific acceptance. A test tolerance chosen to accommodate platform floating-point noise is not automatically an acceptable disagreement with experiment.