pub fn calibrate_energy(
energies_nominal: &[f64],
transmission: &[f64],
uncertainty: &[f64],
isotopes: &[ResonanceData],
abundances: &[f64],
assumed_flight_path_m: f64,
temperature_k: f64,
resolution: Option<&InstrumentParams>,
) -> Result<CalibrationResult, PipelineError>Expand description
Calibrate the energy axis of a TOF neutron measurement.
Given a measured 1D transmission spectrum and known sample composition (e.g. natural Hf), finds the (L, t₀) that minimize chi² by aligning the ENDF resonance positions with the measured dips.
§Search strategy
Issue #634: the former three-phase (L, t₀) grid scan — ~900 (L, t₀)
candidates × a ~25-evaluation golden-section density search each,
i.e. ~35 000 forward evaluations, >10 min on production windows — is
replaced by a staged global-then-local search built on the
fit_energy_scale Levenberg–Marquardt path:
- a coarse joint
(t₀, L_scale)scan (7 × 7 candidates, each with an exact golden-section density inlog10(n)over the documented[1e-5, 1e-2]atoms/barn band — the per-candidate density is what keeps the anchor JOINT, like the old grid); - a direct dip-match anchor: measured dip positions (plateau-robust
depth-weighted run centres, so saturated flat-bottom dips locate
correctly) matched to the known resonance energies and solved as an
affine TOF map by least squares — the discriminating anchor along
the
(t₀, L_scale)degeneracy valley; - a fine joint pit-scan (11 × 11 at 0.25 µs / 0.05 % steps, narrow-band golden density) around EACH anchor — the compact descendant of the old Phase-2/3 grids, needed because the chi² landscape’s sub-bin aliasing pits are ~±0.3 µs wide;
- a multi-start descent from both fine anchors (per anchor: its own density plus log-spaced starts; per start: a direct joint LM AND an exact-density ↔ alignment-only-LM alternation), with every candidate scored by the original valid-bins chi² and the argmin returned.
Net cost is ~4× fewer forward evaluations than the old grid on
production windows, and the LM refinement removes the old grid’s
resolution floor (0.001 % L, 0.05 µs t₀) — the optimum is continuous.
The internal LM fits disable the fitters’ peak-match seed
(with_energy_scale_seed(false)): stages 1–3 already provide a
stronger anchor, and the seed’s strict-local-minimum dip detector
mislocates saturated flat-bottom dips.
Each LM fit constrains L_scale to ±1 % (ENERGY_SCALE_L_SCALE_*);
when a fit rails on that box (a larger true offset), the search
re-anchors on the corrected grid and composes the affine TOF maps, so
the documented ±1.5 % flight-path band remains covered.
The golden-section seed runs on a slightly wider band (~5e-6 to
~2e-2), and if the fitted density optimum saturates that band —
effectively at 1e-5 or 1e-2, or anywhere beyond — the function
returns Err(PipelineError::InvalidParameter) rather than a silent
boundary-saturated answer, because a true minimum at or past the edge
almost always means the real optimum lies outside the supported
interval and the caller should supply a better initial estimate
or check the sample composition.
§Arguments
energies_nominal— Energy grid computed with assumed L (ascending, eV)transmission— Measured transmission values (same length)uncertainty— Per-bin uncertainty (same length)isotopes— ENDF resonance data for each isotopeabundances— Natural abundance fractions (same length as isotopes, sum ≤ 1)assumed_flight_path_m— The L used to computeenergies_nominaltemperature_k— Sample temperature for Doppler broadeningresolution— Optional instrument resolution function. When provided, the forward model includes Doppler + resolution broadening, producing more accurate (L, t₀) fits. Without resolution, fitted parameters absorb the missing broadening and may be biased.
§Returns
CalibrationResult with the fitted (L, t₀, n_total) and corrected energies.