Examples#
Basic Usage#
import pandas as pd
from bssunfold import Detector
# Load response functions
rf_df = pd.read_csv('response_functions.csv')
# Initialize detector
detector = Detector(rf_df)
# Define readings
readings = {
'sphere_1': 150.2,
'sphere_2': 120.5,
'sphere_3': 95.7
}
# Unfold with cvxpy
result_cvxpy = detector.unfold_cvxpy(
readings,
regularization=0.001,
calculate_errors=True
)
# Unfold with Landweber
result_landweber = detector.unfold_landweber(
readings,
max_iterations=500,
tolerance=1e-5,
calculate_errors=True
)
# Unfold with MLEM
result_mlem = detector.unfold_mlem_odl(
readings,
max_iterations=500,
calculate_errors=True
)
# Unfold with GRAVEL (no extra deps)
result_gravel = detector.unfold_gravel(
readings,
max_iterations=200,
tolerance=1e-6
)
# Unfold with MAXED (maximum entropy, no extra deps)
result_maxed = detector.unfold_maxed(
readings,
sigma_factor=0.1
)
# Unfold with Bayes (D'Agostini, no extra deps)
result_bayes = detector.unfold_bayes(
readings,
max_iterations=200,
tolerance=1e-3
)
# Unfold with TSVD (truncated SVD, no extra deps)
result_tsvd = detector.unfold_tsvd(
readings,
k=5,
method='l_curve'
)
# Unfold with StatReg (Turchin, no extra deps)
result_statreg = detector.unfold_statreg(
readings,
unfoldermethod='EmpiricalBayes'
)
Parametric Unfolding#
The parametric methods model the neutron spectrum as a weighted sum of thermal, epithermal, and fast components (FRUIT-style model). They are especially useful when the spectrum shape can be approximated by these three components.
import pandas as pd
from bssunfold import Detector
detector = Detector(pd.read_csv('response_functions.csv'))
readings = {"0in": 0.0003, "2in": 0.0099, "3in": 0.0536, "5in": 0.1841}
# Simple lmfit-based parametric unfolding
result_param = detector.unfold_parametric(
readings,
parametric_method='thermal+epithermal+fast',
optimizer='lmfit',
calculate_errors=True,
)
# SQP via cvxpy (requires initial_params or auto-scan)
result_cvxpy = detector.unfold_parametric(
readings,
parametric_method='thermal+epithermal+fast',
optimizer='cvxpy',
solver_backend='cvxpy:ECOS',
calculate_errors=True,
)
# Combined: lmfit first, then QP refinement
result_combined = detector.unfold_parametric(
readings,
parametric_method='thermal+epithermal+fast',
optimizer='combined',
solver_backend='cvxpy',
calculate_errors=True,
)
# Standalone parametric solver with custom initial guess
from bssunfold.core.unfold_parametric import solve_parametric_cvxpy
result = solve_parametric_cvxpy(
A_matrix=detector.response_functions.values,
b_readings=np.array([readings[k] for k in detector.sphere_names]),
E=detector.energy_bins,
parametric_method='thermal+epithermal+fast',
initial_params={'P_th': 1e5, 'P_epi': 1e5, 'P_f': 1e5,
'b': 0.5, 'beta_prime': 0.5, 'alpha': 2.0, 'beta': 0.5},
max_iter=20,
tolerance=1e-6,
solver_backend='auto',
)
BON95 Parametric Unfolding#
The BON95 method models the lethargy spectrum E*Phi(E) as a linear combination of four components (thermal, epithermal, intermediate, fast) with shape parameters found by grid search and linear coefficients solved by weighted NLS. After parametric fitting, the result is refined by directed-divergence (I-divergence) iterations.
import pandas as pd
from bssunfold import Detector
detector = Detector(pd.read_csv('response_functions.csv'))
readings = {"0in": 0.0003, "2in": 0.0099, "3in": 0.0536, "5in": 0.1841}
# BON95 parametric unfolding (grid search + directed-divergence)
result_bon95 = detector.unfold_parametric2(
readings,
b_range=(0.5, 2.0, 5), # epithermal exponent grid
Tf_range=(0.5, 10.0, 5), # fast peak energy grid (MeV)
c_range=(0.5, 3.0, 4), # fast peak width grid
noise_level=0.05, # 5% measurement uncertainty
calculate_errors=True,
)
# Compare with FRUIT parametric
result_fruit = detector.unfold_parametric(readings, optimizer='lmfit')
print("BON95 spectrum shape:", result_bon95['spectrum'].shape)
print("FRUIT spectrum shape:", result_fruit['spectrum'].shape)
# Standalone BON95 solver
from bssunfold.core.unfold_parametric2 import solve_parametric2
import numpy as np
E = detector.E_MeV
ln_steps = np.zeros(len(E))
log_e = np.log10(E + 1e-15)
ln_steps[0] = log_e[1] - log_e[0]
ln_steps[-1] = log_e[-1] - log_e[-2]
ln_steps[1:-1] = (log_e[2:] - log_e[:-2]) / 2.0
ln_steps *= np.log(10)
A = np.array([detector.sensitivities[n] for n in readings])
b = np.array([readings[n] for n in readings])
spectrum, success, msg, nfev = solve_parametric2(A, b, E, ln_steps)
print(f"Converged: {success}, message: {msg}")
SQP Optimizers#
unfold_parametric2 supports multiple optimizers via the optimizer
parameter. The default is "grid" (exhaustive grid search + NLS).
Three additional SQP-based solvers are available:
"cvxpy"— sequential quadratic programming via cvxpy"qpsolvers"— sequential quadratic programming via qpsolvers"combined"— grid search followed by SQP refinement
# Grid search (default) — thorough but slow for fine grids
result = detector.unfold_parametric2(
readings, optimizer="grid",
b_range=(0.5, 2.0, 5), Tf_range=(0.5, 10.0, 5), c_range=(0.5, 3.0, 4),
)
# CVXPY SQP — fast, no grid needed
result = detector.unfold_parametric2(
readings, optimizer="cvxpy",
initial_guess=(1.0, 2.0, 1.5), # (b, Tf, c) initial guess
)
# QPSolvers SQP — alternative backend (requires OSQP, SCS, or similar)
result = detector.unfold_parametric2(
readings, optimizer="qpsolvers",
solver_backend="osqp",
initial_guess=(1.0, 2.0, 1.5),
)
# Combined — grid search for coarse optimum, then SQP refinement
result = detector.unfold_parametric2(
readings, optimizer="combined",
b_range=(0.5, 2.0, 5), Tf_range=(0.5, 10.0, 5), c_range=(0.5, 3.0, 4),
)