N-spline unfolding (Islamgulov & Lartsev, 2008)#
The unfold_nspline method implements the neutron spectrum unfolding
approach of
R. F. Islamgulov, V. D. Lartsev, Reconstruction of neutron spectra from activation measurements in the form of N-splines, Atomic Energy 104(5), 295–302 (May 2008) (RFNC — VNIITF named after E. I. Zababakhin).
Method outline#
The activation measurements are described by the integral system
which is a classical ill-posed inverse problem. Instead of solving for \(n\) bin values of \(\varphi(E)\) directly, the paper parameterises the spectrum by a specialised neutron spline (N-spline) with basis functions
i.e. piecewise functions whose logarithm is linear both in \(\ln E\) and in \(E\). This family contains the classical model spectra — \(1/E\), Maxwellian evaporation \(\exp(-E/T)\), fission-like \(\sqrt{E}\exp(-bE)\), two-component Maxwell + slowed-down representations — as particular members, so the basis is close to complete for reactor and accelerator spectra while using only \(3M\) parameters.
Continuity of the spline value and of its derivative at the interior knots (Eqs. 3–4 of the paper),
is assembled into the block matrix \(D\) (Eq. 5),
\(X = (a, q, r)^T\), giving the constraint system \(DX = 0\)
(build_continuity_matrix()). The pointwise
approximation of a tabulated spectrum (Eqs. 6–7) is a weighted
least-squares problem in the log domain with weights
\(w_{ki} = 1/\varepsilon_{ki}\) subject to \(DX = 0\):
solved via the KKT (Lagrange multiplier) system
(fit_nspline()).
The activation equations are then solved by the directed divergence minimisation loop (generalised MIRD algorithm of Lartsev, Preprint RFNC-VNIITF No. 216, 2005; Tarasko, Preprint FEI No. 1446, 1983). With normalised measured activations \(p_i = Q_i/\sum_j Q_j\) the functional
(measured \(p_i\) vs calculated \(pN_i\)) is decreased by the flux-conserving gradient iteration
where \(\bar R_n\) is the flux-weighted mean of \(R_n\) and the step \(\Delta\mu_n\) starts from the paper’s conservative value \(0.1/\sup|R_n - \bar R_n|\) and is halved while \(H\) increases (backtracking). After every iteration the current spectrum is re-fitted by the N-spline — the paper’s regularisation, which makes the loop act on \(3M\) spline parameters instead of \(n\) bin values and avoids the nonlinearity / local-minimum difficulties of a direct spline fit to the activation integrals.
Stopping criteria and quality control#
Iterations stop when \(H\) reaches the level corresponding to the measurement errors,
or when the relative decrease of \(H\) per iteration falls below
tol. The acceptability of the reconstruction is measured by the
paper’s mean-squared residual
with \(Qr_i\) the activation integrals recalculated from the unfolded spectrum.
Knots#
Knot sets used in the paper for the BARS-5, IGRIK (channel and surface)
and YAGUAR reactors are available as
NSPLINE_KNOT_PRESETS
("BARS5_channel", "IGRIK_channel", "IGRIK_surface",
"YAGUAR_channel"). knots=None (default) builds a log-uniform
grid, and explicit knot sequences (MeV) are accepted as well. User and
preset knots are clipped to the energy grid range and the outer knots
are extended so that the spline domain always spans the whole grid, as
in the paper.
API#
- bssunfold.core.unfold_nspline.unfold_nspline(detector_names: list[str], n_energy_bins: int, E_MeV: ndarray, sensitivities: dict[str, ndarray], cc_icrp116: dict[str, ndarray], save_result_callback, readings: dict[str, float], initial_spectrum: ndarray | None = None, knots: str | Sequence[float] | None = None, continuity: str = 'C0C1', relative_uncertainty: float = 0.1, max_iterations: int = 200, tol: float = 0.001, step_theta: float = 0.1, smoothing: bool = True, n_segments: int | None = None, calculate_errors: bool = False, noise_level: float = 0.01, n_montecarlo: int = 100, save_result: bool = False, random_state: int | None = None) dict[str, Any][source]#
Unfold neutron spectrum using the N-spline method (2008).
Detector-facing wrapper of the Islamgulov & Lartsev N-spline / directed-divergence unfolding algorithm (Atomic Energy 104(5), 2008).
- Parameters:
detector_names (List[str]) – Names of available detectors.
n_energy_bins (int) – Number of energy bins.
E_MeV (np.ndarray) – Energy grid (MeV).
sensitivities (Dict[str, np.ndarray]) – Detector sensitivity arrays.
cc_icrp116 (Dict[str, np.ndarray]) – ICRP-116 conversion coefficients.
save_result_callback (callable) – Callback to save result to history.
readings (Dict[str, float]) – Detector readings.
initial_spectrum (Optional[np.ndarray], optional) – Initial spectrum guess (the paper recommends a Monte-Carlo calculated spectrum); flat when
None.knots (str / Sequence[float] / None, optional) – Knot preset name (
NSPLINE_KNOT_PRESETS: “BARS5_channel”, “IGRIK_channel”, “IGRIK_surface”, “YAGUAR_channel”), explicit knot sequence orNonefor an automatic log-uniform grid.continuity (str, optional) – Spline continuity:
"C0C1"(default),"C0"or"none".relative_uncertainty (float, optional) – Relative measurement uncertainty dQ/Q used in the stopping criteria and the
nevstatistic (default: 0.1).max_iterations (int, optional) – Iteration budget (default: 200).
tol (float, optional) – Relative H-decrease stopping tolerance (default: 1e-3).
step_theta (float, optional) – Conservative step factor (default: 0.1).
smoothing (bool, optional) – Per-iteration N-spline smoothing (default: True).
n_segments (int, optional) – Number of spline segments when
knots=None.calculate_errors (bool, optional) – Calculate Monte-Carlo errors (default: False).
noise_level (float, optional) – Noise level for Monte-Carlo (default: 0.01).
n_montecarlo (int, optional) – Number of Monte-Carlo samples (default: 100).
save_result (bool, optional) – Save result to history (default: False).
random_state (int, optional) – Random seed for reproducibility.
- Returns:
Standardized unfolding results dictionary enriched with the method diagnostics (
H,H_history,H_target,nev,nev_limit,acceptable,stop_reason,fluence,mean_energy,knots,knots_source).- Return type:
Dict[str, Any]
- bssunfold.core.unfold_nspline.solve_nspline(A: ndarray, b: ndarray, x0: ndarray | None = None, E_MeV: ndarray | None = None, knots: str | Sequence[float] | None = None, sigma_rel: ndarray | None = None, continuity: str = 'C0C1', max_iterations: int = 200, tol: float = 0.001, step_theta: float = 0.1, smoothing: bool = True, n_segments: int | None = None) tuple[ndarray, int, bool][source]#
Solve the unfolding problem using the N-spline method.
Thin standard-API wrapper around
solve_nspline_full()returning the usual(spectrum, iterations, converged)tuple used by the bssunfold solver protocol.- Parameters:
A (np.ndarray) – Response matrix
(m, n).b (np.ndarray) – Measurement vector
(m,).x0 (np.ndarray, optional) – Initial spectrum guess
(n,)(flat whenNone).E_MeV (np.ndarray, optional) – Energy grid (MeV), positive; required.
knots (str / Sequence[float] / None, optional) – Knot preset name, explicit knots or
None(auto grid).sigma_rel (np.ndarray, optional) – Relative measurement uncertainties (default: 0.1).
continuity (str, optional) – Spline continuity:
"C0C1"(default),"C0"or"none".max_iterations (int, optional) – Iteration budget (default: 200).
tol (float, optional) – Relative H-decrease stopping tolerance (default: 1e-3).
step_theta (float, optional) – Conservative step factor (default: 0.1, the paper’s value).
smoothing (bool, optional) – Per-iteration N-spline smoothing (default: True).
n_segments (int, optional) – Number of segments when
knots=None.
- Returns:
(spectrum, iterations, converged).- Return type:
Tuple[np.ndarray, int, bool]
- bssunfold.core.unfold_nspline.solve_nspline_full(A: ndarray, b: ndarray, x0: ndarray | None = None, E_MeV: ndarray | None = None, knots: str | Sequence[float] | None = None, sigma_rel: ndarray | None = None, continuity: str = 'C0C1', max_iterations: int = 200, tol: float = 0.001, step_theta: float = 0.1, smoothing: bool = True, n_segments: int | None = None) dict[str, Any][source]#
Full N-spline unfolding with diagnostics (Islamgulov & Lartsev, 2008).
Iteratively minimises the directed divergence H between the measured and calculated normalised activations, smoothing the spectrum through an N-spline fit at every iteration (the paper’s regularisation). Uses the paper’s stopping criteria (H at the measurement-error level or stalled relative decrease) and reports the
nevresidual statistic with the acceptance boundnev <= 1 + 2/sqrt(N).- Parameters:
A (np.ndarray) – Response matrix
(m, n)– activation responses of the detectors on the energy grid.b (np.ndarray) – Measured readings / activation integrals
(m,).x0 (np.ndarray, optional) – Initial spectrum guess
(n,).Nonemeans a flat spectrum. In the spirit of the paper this should be a Monte-Carlo calculated spectrum when available.E_MeV (np.ndarray, optional) – Energy grid (MeV). Required; must be positive.
knots (str / Sequence[float] / None, optional) – Knot preset name (see
NSPLINE_KNOT_PRESETS), explicit knot sequence orNone(automatic log-uniform grid).sigma_rel (np.ndarray, optional) – Relative measurement uncertainties dQ_i/Q_i
(m,)used by the stopping criteria andnev(default: 0.1 for every detector).continuity (str, optional) –
"C0C1"(default),"C0"or"none".max_iterations (int, optional) – Iteration budget (default: 200).
tol (float, optional) – Relative-decrease stopping tolerance for H (default: 1e-3).
step_theta (float, optional) – Conservative initial step factor: dmu = step_theta / sup|R-Rbar| (the paper’s 0.1); backtracking halves it while H increases.
smoothing (bool, optional) – Re-fit the N-spline after every iteration (default: True, the paper’s procedure;
Falsereduces to the plain MIRD loop).n_segments (int, optional) – Number of spline segments when
knots=None.
- Returns:
Diagnostics dictionary with keys
spectrum,iterations,converged,stop_reason,H,H_history,H_target,nev,nev_limit,acceptable,Qr,relative_residuals,fluence,mean_energy,knots,knots_source,continuityandparams.- Return type:
Dict[str, Any]
- bssunfold.core.unfold_nspline.fit_nspline(E: ndarray, phi: ndarray, knots: str | Sequence[float] | None = None, rel_err: ndarray | None = None, continuity: str = 'C0C1', n_segments: int | None = None) tuple[ndarray, dict[str, Any]][source]#
Approximate a pointwise spectrum by an N-spline (Eqs. 2, 5-7).
Solves the weighted log-domain least-squares problem with continuity constraints of the paper:
min_X sum_j w_j^2 (a_kj + u_j q_kj + E_j r_kj - ln phi_j)^2 s.t. D X = 0, w_j = 1 / eps_j,
via the KKT (Lagrange multiplier) system
[[G^T W G, D^T], [D, 0]] [X; lam] = [G^T W Y; 0].
- Parameters:
E (np.ndarray) – Energy grid (MeV), positive values.
phi (np.ndarray) – Spectrum values on the grid (non-negative; zeros are floored and down-weighted).
knots (str / Sequence[float] / None, optional) – Knot preset name, explicit knots or
Nonefor an automatic log-uniform grid (default).rel_err (np.ndarray, optional) – Relative pointwise errors eps_j; weights w_j = 1/eps_j (paper’s Eq. 7).
Nonemeans unit weights.continuity (str, optional) –
"C0C1"(default),"C0"or"none"– seebuild_continuity_matrix().n_segments (int, optional) – Number of segments when
knots=None(default: adaptive).
- Returns:
(N_E, info)whereN_Eis the fitted spline onEandinfocarriesknots,a/q/rparameter arrays, the weighted RMS log-residual and the continuity option.- Return type:
Tuple[np.ndarray, Dict[str, Any]]
- bssunfold.core.unfold_nspline.build_continuity_matrix(knots: Sequence[float], continuity: str = 'C0C1') ndarray[source]#
Build the spline continuity matrix
Dof Eq. (5).The N-spline parameter vector is
X = (a, q, r)^Twitha = (a_1..a_M),q = (q_1..q_M),r = (r_1..r_M). The continuity conditions at the interior knots read (Eqs. 3-4):C0: a_k - a_{k+1} + u (q_k - q_{k+1}) + E (r_k - r_{k+1}) = 0 C1: (q_k - q_{k+1}) + E (r_k - r_{k+1}) = 0,
with
u = ln EandEthe knot value, and are assembled intoD = [[A, B, C], [0, A, C]], D X = 0.
- Parameters:
knots (Sequence[float]) – Knot sequence (M = len(knots) - 1 segments).
continuity (str, optional) –
"C0C1"(default) – continuous value and derivative;"C0"– continuous value only (only the first block row of D is kept, as noted in the paper);"none"– no continuity.
- Returns:
Matrix of shape
(rows, 3M)whererowsis 0, M-1 or 2(M-1) depending oncontinuity.- Return type:
np.ndarray
- bssunfold.core.unfold_nspline.nspline_eval(E: ndarray, a: Sequence[float], q: Sequence[float], r: Sequence[float], knots: Sequence[float]) ndarray[source]#
Evaluate the N-spline
N(E) = exp(a_k + q_k ln E + r_k E).- Parameters:
E (np.ndarray) – Evaluation energies (MeV), must be positive.
a (Sequence[float]) – Length-M parameter vectors.
q (Sequence[float]) – Length-M parameter vectors.
r (Sequence[float]) – Length-M parameter vectors.
knots (Sequence[float]) – The M+1 knot values.
- Returns:
N-spline values (positive by construction).
- Return type:
np.ndarray
- bssunfold.core.unfold_nspline.auto_knots(E_MeV: ndarray, n_segments: int = 12) tuple[float, ...][source]#
Build a log-uniform knot grid spanning the energy range of
E_MeV.- Parameters:
E_MeV (np.ndarray) – Energy grid (MeV), must contain positive values.
n_segments (int, optional) – Number of spline segments (default: 12); the returned tuple has
n_segments + 1knots frommin(E)tomax(E).
- Returns:
Strictly increasing knot sequence (MeV).
- Return type:
Tuple[float, …]
Usage#
from bssunfold import Detector
det = Detector()
result = det.unfold_nspline(
readings,
knots="BARS5_channel", # or None (auto), or explicit knots
continuity="C0C1", # spline continuity option
relative_uncertainty=0.05,
max_iterations=300,
)
print(result["nev"], result["acceptable"]) # paper's statistic
print(result["H_history"]) # convergence trace
See examples/41-nspline.ipynb for a worked comparison with GRAVEL on
a synthetic spectrum, and examples/42-nspline-iaea.ipynb for
unfolding an IAEA Compendium Monte-Carlo BSA spectrum
(t4-14-s.txt_1) from GSF Bonner-sphere readings.
Notes and limitations#
nevuses unweighted relative residuals: with readings spanning many orders of magnitude the weakest detector dominates the statistic (its contribution to \(H\) is negligible because \(H\) works with normalised activations). In the paper’s activation-foil applications all \(Q_i\) are comparable and \(nev \approx 1\)–2.Energy regions where all detector responses vanish are shaped by the C0/C1 spline continuation (prior-driven); the pointwise smoothing weights follow the paper’s \(w = 1/\varepsilon\) philosophy with sensitivity-graded errors.
The method is verified by
tests/test_nspline.py(32 tests): exact interpolation properties, continuity constraints, synthetic-spectrum recovery, thenevstatistic and theDetectorintegration.