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

Benchmark

BenchmarkSuite

Experiment with measurement \(m\), uncertainty \(\sigma_m\), calculated value \(c\), and sensitivity \(S\)

Application

ApplicationSuite

System of interest — same shape as Benchmark but \(m\) is optional

Covariance

CovarianceSuite

Nuclear data covariance for one or more nuclides

—

AssimilationSuite

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

title

str

Unique name

kind

str

Observable type (e.g. "keff")

m

float

Measured value

dm

float

Measurement uncertainty (\(1\sigma\))

c

float

Calculated value

dc

float

Calculation uncertainty (\(1\sigma\))

s

Sensitivity

Energy-group sensitivity profiles

flux

FluxSpectrum | None

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

Chi2Filter(threshold)

Keeps benchmarks where \(\chi^2 \le\) threshold

Chi2NuclearDataFilter(threshold, cov_matrix)

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)