User Guide#
Data model overview#
ANDALUS is built around a small set of composable data classes that mirror the components of the
GLLS workflow. Each item class has a corresponding suite class that aggregates multiple items and
exposes combined pd.Series/pd.DataFrame properties.
Item class |
Suite class |
Holds |
|---|---|---|
|
|
Experiment with measurement \(m\), uncertainty \(\sigma_m\), calculated value \(c\), and sensitivity \(S\) |
|
|
System of interest — same shape as |
|
|
Nuclear data covariance for one or more nuclides |
— |
|
Orchestrator that holds all three suites and runs GLLS |
Sensitivity#
Sensitivity is a pd.DataFrame subclass with a four-level MultiIndex (ZAI, MT, E_min_eV, E_max_eV)
and two columns: the sensitivity values and their standard deviations.
Loading from Serpent#
from andalus import Sensitivity
s = Sensitivity.from_serpent(
sens0_path="data/hmi001_sens0.m",
title="HMI-001",
kind="keff",
zailist=[922350, 922380], # optional subset of nuclides
pertlist=[18, 102], # optional subset of reactions (MT numbers)
)
Plotting#
ax = s.plot_sensitivity(
zais=[922350],
perts=["total fission", "capture"],
)
Persistence#
s.to_hdf5("sensitivities.h5")
s2 = Sensitivity.from_hdf5("sensitivities.h5", title="HMI-001")
Benchmark#
Benchmark is a frozen dataclass. Once created it cannot be modified; use the suite-level
update_c() method when you need to propagate new calculated values.
Fields#
Field |
Type |
Description |
|---|---|---|
|
|
Unique name |
|
|
Observable type (e.g. |
|
|
Measured value |
|
|
Measurement uncertainty (\(1\sigma\)) |
|
|
Calculated value |
|
|
Calculation uncertainty (\(1\sigma\)) |
|
|
Energy-group sensitivity profiles |
|
|
Flux spectrum (optional) |
Loading from Serpent#
from andalus import Benchmark
b = Benchmark.from_serpent(
title="HMI-001",
m=1.0000,
dm=0.0005,
sens0_path="data/hmi001_sens0.m",
results_path="data/hmi001_res.m",
)
Attach a flux spectrum by passing flux_det=(detector_name, det_path):
b = Benchmark.from_serpent(
title="HMI-001",
m=1.0000,
dm=0.0005,
sens0_path="data/hmi001_sens0.m",
results_path="data/hmi001_res.m",
flux_det=("FLUX", "data/hmi001_det0.m"),
)
Persistence#
b.to_hdf5("benchmarks.h5")
b2 = Benchmark.from_hdf5("benchmarks.h5", title="HMI-001")
Application#
Application has the same shape as Benchmark but m and dm are optional. Use it for any
nuclear system whose response you want to predict (but may not have a measurement for).
from andalus import Application
app = Application.from_serpent(
title="my-reactor",
sens0_path="data/reactor_sens0.m",
results_path="data/reactor_res.m",
# m and dm are optional
)
Covariance#
Covariance is a pd.DataFrame subclass that holds the multigroup cross-section covariance matrix
for a single nuclide (ZAI). The index is a MultiIndex (MT, E_min_eV, E_max_eV).
Loading from ERRORR files#
NJOY ERRORR output files are read via SANDY:
from andalus import Covariance
cov = Covariance.from_errorr(
files={"tape33": "data/u235.errorr"},
zai=922350,
mts=[18, 102], # fission and capture
)
Persistence#
cov.to_hdf5("covariances.h5")
cov2 = Covariance.from_hdf5("covariances.h5", zai=922350)
Diagnostics#
print(cov.correlation()) # correlation matrix
print(cov.is_unrealistic_uncertainty(threshold=10)) # True if any σ > 1000 %
CovarianceSuite#
CovarianceSuite wraps a block-diagonal covariance matrix that spans multiple nuclides and
reactions. Its .matrix attribute is the global pd.DataFrame with a five-level MultiIndex
(ZAI, MT, E_min_eV, E_max_eV).
from andalus import CovarianceSuite
# Load from HDF5 (most common)
cov_suite = CovarianceSuite.from_hdf5(
"covariances.h5",
zais=[922350, 922380, 942390],
mts=[2, 18, 102],
)
# Build from individual Covariance objects
from andalus import Covariance
cov_suite = CovarianceSuite.from_dict(
{
922350: cov_u235,
922380: cov_u238,
}
)
# Inspect uncertainties
print(cov_suite.get_uncertainties())
# Visualise uncertainty for U-235 capture (MT=102)
cov_suite.plot_uncertainty(zai=922350, mt=102)
AssimilationSuite#
AssimilationSuite is the top-level orchestrator. It holds a BenchmarkSuite, an
ApplicationSuite, and a CovarianceSuite, and exposes the full GLLS workflow.
Construction#
from andalus import AssimilationSuite
suite = AssimilationSuite(
benchmarks=benchmark_suite,
applications=application_suite,
covariances=covariance_suite,
)
The recommended approach for reproducible workflows is to use from_yaml():
suite = AssimilationSuite.from_yaml("assimilation.yaml")
Aggregated properties#
All suite classes expose combined properties as pd.Series or pd.DataFrame:
suite.m # measured values (benchmarks only)
suite.dm # measurement uncertainties (absolute)
suite.c # calculated values (benchmarks + applications)
suite.dc # calculation uncertainties (absolute)
suite.s # sensitivity matrix (rows = cases, cols = nuclear data)
Running GLLS#
posterior = suite.glls()
Pass include_sensitivity_uncertainty=True to account for the statistical uncertainty on the
sensitivity profiles themselves (not yet tested):
posterior = suite.glls(include_sensitivity_uncertainty=True)
Inspecting the posterior#
posterior.summarize()
# Per-benchmark chi-squared
print(posterior.individual_chi_squared())
# Total chi-squared (with and without nuclear data term)
print(posterior.chi_squared(nuclear_data=False))
print(posterior.chi_squared(nuclear_data=True))
# Adjusted calculated values for applications
print(posterior.applications.c)
# Nuclear data adjustment vector
print(posterior.xs_adjustment)
Similarity analysis#
The \(c_k\) matrix quantifies how similar two cases are in terms of their nuclear data sensitivity:
ck = suite.ck_matrix() # square DataFrame, rows and cols = all case titles
print(ck)
# Similarity of all cases with a specific application
print(suite.ck_target("my-reactor"))
Uncertainty propagation#
Propagate the prior nuclear data uncertainty to the calculated response vector:
uncertainty = suite.propagate_nuclear_data_uncertainty()
print(uncertainty) # pd.Series, one value per case
Exporting to ACE#
After running GLLS, export the adjusted nuclear data to ACE format for use in Serpent or other
Monte Carlo codes. Two methods are available; to_ace_direct is the recommended option.
to_ace_direct edits an already-processed ACE file’s XSS array directly (via
andalus.ace.ACE.perturb/perturb_nu/perturb_chi), taking a fraction of a second per nuclide.
It does not talk to ENDF or run NJOY, instead only an ASCII ACE file per adjusted ZAI is needed.
Point it at either a directory of ACE files named by ZAID (ace_dir) or a Serpent-style xsdata
file for libraries with non-standard naming, such as JEFF-4.0 (xsdata_path):
posterior.to_ace_direct(
out_dir="adjusted_ace",
xsdata_path="path/to/jeff40.xsdata",
temperature=300,
create_xsdata=True,
)
to_ace instead reprocesses each nuclide from its base ENDF6 file via SANDY/NJOY, which can
take several minutes per nuclide, but only requires an ENDF6 library name/path rather than pre-built ACE
files (requires NJOY on PATH):
posterior.to_ace(
library="jeff_40",
temperature=300,
create_xsdata=True,
parallel=True,
)
Both methods accept only_zais_applications=True to limit the export to nuclides that actually
appear in the application sensitivities. If you do not possess of sensitivities while they are available in your application, but you did not calculate them. The calculated response and the predicted response using linear approximations could be different.
Filtering benchmarks#
The filter() method on suites accepts any callable Benchmark → bool, including the built-in
filter objects.
Built-in filters#
Filter |
Description |
|---|---|
|
Keeps benchmarks where \(\chi^2 \le\) threshold |
|
Keeps benchmarks where the nuclear-data-inclusive \(\chi^2 \le\) threshold |
Composing filters#
Filter objects support boolean operators:
from andalus import Chi2Filter, Chi2NuclearDataFilter
f_exp = Chi2Filter(3.0)
f_ndata = Chi2NuclearDataFilter(3.0, suite.covariances.matrix)
# Both conditions must hold
filtered = suite.filter(f_exp & f_ndata)
# At least one condition must hold
filtered = suite.filter(f_exp | f_ndata)
# Negate a filter
filtered = suite.filter(~f_exp)
Custom filters#
Any callable that accepts a Benchmark and returns a bool works:
# Keep only fast-spectrum benchmarks
fast_only = lambda b: b.flux is not None and b.flux.ealf > 1e4 # EALF > 10 keV
filtered = suite.filter(fast_only)
HDF5 storage#
ANDALUS uses pandas HDF5 tables as its on-disk format, making it easy to accumulate large
libraries of benchmarks and covariances across sessions.
# Save
benchmark.to_hdf5("benchmarks.h5")
application.to_hdf5("applications.h5")
covariance.to_hdf5("covariances.h5")
# Load individual items
b = Benchmark.from_hdf5("benchmarks.h5", title="HMI-001")
a = Application.from_hdf5("applications.h5", title="my-reactor")
c = Covariance.from_hdf5("covariances.h5", zai=922350)
# Load entire suites
bs = BenchmarkSuite.from_hdf5("benchmarks.h5", titles=None) # all
bs = BenchmarkSuite.from_hdf5("benchmarks.h5", titles=["HMI-001"]) # subset
The HDF5 layout is:
{kind}/
{title}/
sensitivity ← pandas table (Sensitivity)
flux ← pandas table (FluxSpectrum, optional)
attrs: m, dm, c, dc
Utility functions#
sandwich#
The core \(\mathbf{s}_1^T \mathbf{M} \, \mathbf{s}_2\) operation used throughout uncertainty propagation:
from andalus import sandwich
# Propagate covariance through two sensitivity vectors
result = sandwich(s1, cov_matrix, s2)
# Auto-transpose: s2 defaults to s1 when omitted
variance = sandwich(s, cov_matrix)
sandwich_binwise / uncertainty_reactionwise#
Decompose the total uncertainty into contributions per energy bin or per reaction (ZAI/MT pair):
from andalus.utils import sandwich_binwise, uncertainty_reactionwise
bin_contributions = sandwich_binwise(s, cov)
rxn_contributions = uncertainty_reactionwise(s, cov)