Introduction
NEREIDS (Neutron rEsonance REsolved Imaging Data Analysis System) is a Rust-based library for neutron resonance imaging at the VENUS beamline, Spallation Neutron Source (SNS), Oak Ridge National Laboratory (ORNL).
What It Does
NEREIDS provides end-to-end analysis for time-of-flight (TOF) neutron resonance imaging: input hyperspectral TOF data, output spatially resolved isotopic and elemental composition maps.
The analysis pipeline:
- Load raw TOF imaging data (TIFF stacks, NeXus/HDF5, or pre-normalized transmission)
- Normalize sample and open-beam measurements to transmission
- Configure isotopes of interest using ENDF nuclear data
- Fit resonance models to measured transmission spectra
- Map fitted parameters (areal density, temperature) across each pixel
Interfaces
NEREIDS ships in several interfaces:
| Deliverable | Use case |
|---|---|
Rust library (nereids-* crates) | Embed in Rust applications, maximum performance |
Python bindings (pip install nereids) | Jupyter notebooks, scripting, integration with NumPy/SciPy |
Desktop GUI (nereids-gui) | Interactive analysis with visual feedback |
MCP server (nereids-mcp) | Local AI-agent assisted analysis through manifest-driven workflows |

Relationship to SAMMY
NEREIDS implements the same physics as SAMMY (a Fortran code for multilevel R-matrix analysis of neutron data, ORNL/TM-9179/R8), rewritten in Rust with modern tooling. All physics modules reference specific SAMMY source files and equation numbers in their documentation.
Key formalisms from SAMMY:
- Reich-Moore R-matrix (LRF=3)
- Breit-Wigner, single- and multi-level (LRF=1/2)
- R-Matrix Limited (LRF=7)
- Free Gas Model Doppler broadening
- Gaussian + exponential resolution broadening
- Unresolved Resonance Region (LRU=2)
Next Steps
- Install NEREIDS for your platform
- Try the Rust quickstart or Python quickstart
- Explore the GUI walkthrough for interactive analysis
Installation
Rust Library
Add the top-level orchestration crate (re-exports all lower-level crates):
[dependencies]
nereids-pipeline = "0.1"
Or add individual crates (nereids-core, nereids-endf, nereids-physics,
nereids-fitting, nereids-io) for finer-grained dependency control.
Requirements: Rust edition 2024 (rustc 1.85+).
Optional: HDF5 support
The nereids-io crate has an optional hdf5 feature for NeXus file support:
[dependencies]
nereids-io = { version = "0.1", features = ["hdf5"] }
This requires the HDF5 C library to be installed on your system.
Python Bindings
pip install nereids
Requirements: Python 3.10+ and NumPy.
Optional extras published by the nereids package:
pip install "nereids[mcp]" # installs the MCP server dependency
pip install "nereids[gui]" # pulls in the GUI wheel package when available
MCP Server
The MCP server is installed as an optional Python extra:
pip install "nereids[mcp]"
nereids-mcp
See the MCP server chapter for client configuration and manifest-driven workflows.
Desktop GUI
Python Wheel
pip install "nereids[gui]"
nereids-gui
The [gui] extra pulls in the separately-published nereids-gui wheel,
which is what provides the nereids-gui console script (it is not declared
in the base nereids package). If the install resolves but nereids-gui
is not found on PATH, the nereids-gui wheel has not been published for
your platform/Python version — verify with:
which nereids-gui # should print a path; empty output means missing
pip show nereids-gui # should print metadata; "not installed" means the
# extra resolved a different way
You can also install the GUI distribution directly:
pip install nereids-gui
nereids-gui
macOS (Homebrew)
brew tap ornlneutronimaging/nereids
brew install --cask nereids
From Source
git clone https://github.com/ornlneutronimaging/NEREIDS.git
cd NEREIDS
cargo run --release -p nereids-gui
Building from source requires CMake (for HDF5) and a Rust toolchain.
Linux system dependencies
NEREIDS uses GTK 3 for native file dialogs (no xdg-desktop-portal
daemon needed) and the standard egui/winit/wgpu stack for the rest of
the UI. Desktop Linux distros usually ship these, but minimal /
container / server installs do not.
Debian / Ubuntu (apt):
sudo apt-get install -y \
libgtk-3-0 libxcursor1 libx11-xcb1 libxi6 libxrandr2 \
libxinerama1 libxxf86vm1 libxkbcommon-x11-0 libwayland-client0 \
libgl1 libgl1-mesa-dri libegl1
libgtk-3-0 is portable across Debian and all current Ubuntu LTS
releases. On Ubuntu 24.04 (Noble) it pulls in libgtk-3-0t64 under
the hood via the t64 transitional package. libgl1-mesa-dri is
needed even with LIBGL_ALWAYS_SOFTWARE=1 (below) because the
software rasteriser is shipped as a Mesa DRI driver.
Contributors building from source additionally need the GTK 3
development headers and pkg-config:
sudo apt-get install -y libgtk-3-dev pkg-config
Fedora / RHEL (dnf):
sudo dnf install -y \
gtk3 libXcursor libXi libXrandr libXinerama libxkbcommon-x11 \
libwayland-client libwayland-cursor \
mesa-libGL mesa-libEGL mesa-dri-drivers
Contributors building from source additionally need gtk3-devel and
pkgconf-pkg-config.
Headless / Docker / VM fallback:
If the GUI fails at startup with a GL initialisation error (common in
Docker without GPU passthrough, or over SSH-X without GLX), force
software rasterisation by setting LIBGL_ALWAYS_SOFTWARE=1 before
launching the GUI:
export LIBGL_ALWAYS_SOFTWARE=1
cargo run --release -p nereids-gui # from source
# or, if installed as a binary:
nereids-gui
Development Setup
For contributors working on NEREIDS itself:
git clone https://github.com/ornlneutronimaging/NEREIDS.git
cd NEREIDS
# Build everything
cargo build --workspace
# Run tests
cargo test --workspace --exclude nereids-python
# Build Python bindings (requires pixi)
pixi run build
pixi run test-python
See Contributing for the full development workflow.
Quickstart: Rust
This example loads ENDF resonance data for U-238, computes a theoretical transmission spectrum, and fits it to recover the areal density.
The snippets below are spliced from
crates/nereids-fitting/examples/quickstart.rs
so the rendered page cannot drift out of sync with the live crate APIs:
the example is compile-checked by cargo check --workspace --examples in
CI. Run the full example locally with
cargo run --example quickstart -p nereids-fitting (first run requires
network access to fetch ENDF/B-VIII.1).
Setup
# Cargo.toml
[dependencies]
nereids-core = "0.1"
nereids-endf = "0.1"
nereids-physics = "0.1"
nereids-fitting = "0.1"
Load ENDF Data
use nereids_core::types::Isotope;
use nereids_endf::parser::parse_endf_file2;
use nereids_endf::retrieval::{EndfLibrary, EndfRetriever, mat_number};
use nereids_fitting::lm::{LmConfig, levenberg_marquardt};
use nereids_fitting::parameters::{FitParameter, ParameterSet};
use nereids_fitting::transmission_model::TransmissionFitModel;
use nereids_physics::transmission::{SampleParams, forward_model};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Download and cache ENDF/B-VIII.1 data for U-238 (Z=92, A=238).
let isotope = Isotope::new(92, 238)?;
let retriever = EndfRetriever::new();
let mat = mat_number(&isotope, EndfLibrary::EndfB8_1).expect("U-238 has a known MAT number");
let (_path, endf_text) = retriever.get_endf_file(&isotope, EndfLibrary::EndfB8_1, mat)?;
let resonance_data = parse_endf_file2(&endf_text)?;
println!(
"U-238: {} resonances, AWR = {:.1}",
resonance_data.total_resonance_count(),
resonance_data.awr,
);
Compute a Forward Model
#![allow(unused)]
fn main() {
// Energy grid: 1 to 30 eV (covers the 6.67 eV and 20.9 eV resonances).
let energies: Vec<f64> = (0..2000)
.map(|i| 1.0 + (i as f64) * 29.0 / 2000.0)
.collect();
// Sample: U-238 at 0.001 atoms/barn, room temperature.
let sample = SampleParams::new(300.0, vec![(resonance_data.clone(), 0.001)])?;
// No instrument resolution broadening for this example.
let transmission = forward_model(&energies, &sample, None)?;
// `transmission[i]` is T(E_i) in [0, 1], with dips at resonance energies.
}
Fit a Measured Spectrum
#![allow(unused)]
fn main() {
// Simulate measured data (in practice, load from TIFF/NeXus).
let measured_t = transmission.clone();
let sigma: Vec<f64> = vec![0.01; measured_t.len()];
// Set up the fit model: one density parameter at index 0.
let model = TransmissionFitModel::new(
energies.clone(),
vec![resonance_data],
300.0, // temperature_k
None, // no instrument resolution
(vec![0], vec![1.0]), // density_indices, density_ratios
None, // no temperature fitting
None, // no precomputed cross-sections
)?;
// Initial guess: density = 0.0005 atoms/barn (non-negative constraint).
let mut params = ParameterSet::new(vec![FitParameter::non_negative("U-238 density", 0.0005)]);
let config = LmConfig::default();
let result = levenberg_marquardt(&model, &measured_t, &sigma, &mut params, &config)?;
println!("Fitted density: {:.6} atoms/barn", result.params[0]);
println!("Reduced chi-squared: {:.3}", result.reduced_chi_squared);
println!("Converged: {}", result.converged);
Ok(())
}
}
Next Steps
- See the API Reference for the full API
- Explore the Python quickstart for a NumPy-based workflow
Quickstart: Python
This example uses the NEREIDS Python bindings to load ENDF data, compute a forward model, and fit a transmission spectrum.
Setup
pip install nereids numpy matplotlib
Load ENDF Data and Compute Transmission
import nereids
import numpy as np
# Load ENDF/B-VIII.1 resonance data for U-238
u238 = nereids.load_endf(92, 238)
print(f"U-238: {u238.n_resonances} resonances, AWR = {u238.awr:.1f}")
# Energy grid: 1 to 30 eV
energies = np.linspace(1.0, 30.0, 2000)
# Compute transmission for 0.001 atoms/barn at 300 K
transmission = nereids.forward_model(
energies,
[(u238, 0.001)],
temperature_k=300.0,
flight_path_m=25.0,
delta_t_us=5.0,
delta_l_m=0.005,
)
Plot the Spectrum
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 4))
plt.plot(energies, transmission, linewidth=0.8)
plt.xlabel("Energy (eV)")
plt.ylabel("Transmission")
plt.title("U-238 Forward Model (0.001 at/barn, 300 K)")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Spatial Mapping
For imaging data (3D transmission arrays), use the typed API:
# transmission_3d: shape (n_energies, height, width)
# uncertainty_3d: shape (n_energies, height, width)
data = nereids.from_transmission(transmission_3d, uncertainty_3d)
result = nereids.spatial_map_typed(
data,
energies,
[u238],
temperature_k=300.0,
flight_path_m=25.0,
delta_t_us=5.0,
delta_l_m=0.005,
)
# result.density_maps[0] is a 2D array of U-238 areal density at each pixel
# result.converged_map shows which pixels converged
print(f"Converged: {result.n_converged}/{result.n_total} pixels")
For raw count data (Poisson-optimal fitting):
data = nereids.from_counts(sample_counts_3d, open_beam_counts_3d)
result = nereids.spatial_map_typed(data, energies, [u238])
Single-Spectrum Fitting
Use fit_spectrum_typed(...) for pre-normalized transmission spectra:
uncertainty = np.full_like(transmission, 0.01)
fit = nereids.fit_spectrum_typed(
transmission,
uncertainty,
energies,
[(u238, 0.0005)],
temperature_k=300.0,
)
print(fit.densities, fit.reduced_chi_squared)
Use fit_counts_spectrum_typed(...) for raw sample/open-beam counts:
fit = nereids.fit_counts_spectrum_typed(
sample_counts_1d,
open_beam_counts_1d,
energies,
[(u238, 0.0005)],
c=sample_charge / open_beam_charge,
)
print(fit.densities, fit.deviance_per_dof)
See the Python API reference for argument details, shape contracts, and result objects.
TIFF and NeXus Data
TIFF and NeXus loaders use the spectral axis first:
stack = nereids.load_tiff_stack("transmission_stack.tif")
sample = nereids.load_nexus_histogram("sample.nxs")
open_beam = nereids.load_nexus_histogram("open_beam.nxs")
energies = nereids.tof_to_energy_centers(
sample.tof_edges_us,
sample.flight_path_m or 25.0,
)
NeXus counts are loaded in ascending TOF order. Reverse axis 0 before fitting
against the ascending energy centers returned by tof_to_energy_centers(...).
The Data I/O and NeXus/TOF chapter covers the full workflow.
Detectability Analysis
Check whether a trace isotope is detectable in a given matrix:
fe56 = nereids.load_endf(26, 56) # matrix: Fe-56
ag107 = nereids.load_endf(47, 107) # trace: Ag-107
report = nereids.trace_detectability(
matrix=fe56,
matrix_density=0.01,
trace=ag107,
trace_ppm=100.0,
energies=energies,
i0=1e6,
)
print(f"Detectable: {report.detectable}")
print(f"Peak SNR: {report.peak_snr:.1f} at {report.peak_energy_ev:.2f} eV")
Next Steps
- See the GUI walkthrough for interactive analysis
- Use the MCP server for local AI-agent assisted workflows
- Browse the Python API reference
- Read Data I/O and NeXus/TOF for TIFF, NeXus, and TOF data handling
- Explore the Architecture chapter for the crate structure
- Browse the API Reference for the full Rust docs
Python API Reference
The nereids Python package is a PyO3 layer over the Rust pipeline. This
page is a curated narrative reference covering the typed APIs Python users
reach for most often, with argument tables, array-shape contracts, and
dispatch rules.
For the exhaustive auto-generated reference (every function, class, and
attribute exported by the package), see the generated Python API
reference built by pdoc from the
installed wheel and the shipped nereids/__init__.pyi type stubs.
Install the base package with:
pip install nereids
Optional extras are:
pip install "nereids[mcp]" # MCP server console script
pip install "nereids[gui]" # GUI wheel dependency, when available for your platform
Data Objects
ResonanceData
Returned by load_endf(...), load_endf_file(...), and
create_resonance_data(...).
Important properties:
| Property | Type | Meaning |
|---|---|---|
z | int | Atomic number. |
a | int | Mass number. |
awr | float | Atomic weight ratio. |
n_resonances | int | Resonance count across parsed ranges. |
target_spin | float | Target spin from the first range. |
scattering_radius | float | Effective scattering radius in fm. |
l_values | list[int] | Orbital angular momentum values present in the data. |
FitResult
Returned by fit_spectrum_typed(...) and fit_counts_spectrum_typed(...).
| Property | Type | Meaning |
|---|---|---|
densities | NDArray[float64] | Fitted areal densities in atoms/barn. |
uncertainties | NDArray[float64] | One-sigma density uncertainties; entries may be NaN when covariance is unavailable. |
reduced_chi_squared | float | Pearson chi-squared per degree of freedom for LM/transmission paths. |
deviance_per_dof | float or None | Primary goodness-of-fit for counts-KL fits. |
converged | bool | Whether the optimizer converged. |
iterations | int | Iteration count. |
temperature_k | float or None | Fitted temperature when fit_temperature=True. |
t0_us, l_scale | float or None | Fitted energy-scale parameters when fit_energy_scale=True. |
InputData
Opaque typed 3D input for spatial mapping. Create it with:
data = nereids.from_transmission(transmission, uncertainty)
data = nereids.from_counts(sample_counts, open_beam_counts)
The spectral axis is always axis 0, so arrays have shape
(n_energy, height, width).
SpatialResult
Returned by spatial_map_typed(...).
| Property | Type | Meaning |
|---|---|---|
density_maps | list[NDArray[float64]] | One (height, width) density map per isotope or isotope group. |
uncertainty_maps | list[NDArray[float64]] | Per-pixel density uncertainty maps. |
chi_squared_map | NDArray[float64] | Per-pixel reduced chi-squared for LM/transmission paths. |
deviance_per_dof_map | NDArray[float64] or None | Primary GOF map for counts-KL spatial fits. |
converged_map | NDArray[bool_] | Per-pixel convergence flags. |
n_converged, n_failed, n_total | int | Pixel fit counts. |
temperature_map | NDArray[float64] or None | Fitted temperature map when enabled. |
anorm_map, background_maps | NDArray[float64] / list[...] or None | SAMMY Anorm and the polynomial background [BackA, BackB, BackC] per pixel when background=True. |
back_d_map, back_f_map | NDArray[float64] or None | SAMMY exponential background BackD / BackF per pixel when background=True and fit_back_d=True / fit_back_f=True. Counts-KL spatial runs always return None for both (the joint-Poisson dispatch never fits the exponential tail). |
t0_us_map, l_scale_map | NDArray[float64] or None | Energy-scale maps when enabled. |
NexusData
Returned by load_nexus_histogram(...) and load_nexus_events(...).
| Property | Type | Meaning |
|---|---|---|
counts | NDArray[float64] | Counts cube with shape (n_tof, height, width). |
tof_edges_us | NDArray[float64] | TOF bin edges in microseconds, length n_tof + 1. |
flight_path_m | float or None | Flight path from NeXus metadata when available. |
dead_pixels | NDArray[bool_] or None | Dead-pixel mask, True means dead. |
n_rotation_angles | int | Number of rotation angles in histogram input. |
event_total, event_kept | int or None | Event loader statistics. |
ENDF Loading
u238 = nereids.load_endf(92, 238, library="endf8.1")
u238_local = nereids.load_endf_file("examples/data/u238_ex027.endf")
load_endf(...) fetches and caches evaluated nuclear data. Supported library
names include endf8.0, endf8.1, jeff3.3, jendl5, tendl2023, and
cendl3.2. First use can require network access; cached files are reused
afterwards. load_endf_file(...) parses a local ENDF file and does not
download data.
Forward Modeling
import numpy as np
import nereids
u238 = nereids.load_endf(92, 238)
energies = np.linspace(1.0, 30.0, 2000)
transmission = nereids.forward_model(
energies,
[(u238, 0.001)],
temperature_k=300.0,
flight_path_m=25.0,
delta_t_us=5.0,
delta_l_m=0.005,
)
forward_model(...) returns a 1D float64 transmission spectrum on the
input energy grid. Pass either isotopes=[(ResonanceData, density), ...] or
groups=[(IsotopeGroup, density), ...], but not both. Gaussian resolution is
enabled by the flight_path_m, delta_t_us, and delta_l_m parameters.
Tabulated resolution can be supplied with resolution=load_resolution(...).
Single-Spectrum Fitting
Transmission Data
result = nereids.fit_spectrum_typed(
transmission,
uncertainty,
energies,
[(u238, 0.0005)],
temperature_k=300.0,
solver="lm",
)
Shape contract:
transmission,uncertainty, andenergiesare 1D arrays with the same length.energiesis in eV and should be ascending.isotopessupplies(ResonanceData, initial_density)pairs.
Keyword arguments:
| Option | Meaning |
|---|---|
temperature_k=293.6 | Sample temperature in kelvin. |
fit_temperature=False | Fit sample temperature in addition to densities. |
max_iter=200 | Maximum optimizer iterations. |
solver="lm" | "lm", "kl", "auto", "poisson", or "joint_poisson". "poisson" and "joint_poisson" are aliases used by the counts dispatch and accepted here for symmetry. |
background=False | Enable SAMMY-style transmission background parameters. |
fit_back_d=False, fit_back_f=False | Fit optional exponential background terms. |
back_d_init=0.01, back_f_init=1.0 | Initial exponential background values. |
fit_energy_scale=False | Fit TOF energy-scale parameters t0_us and l_scale. |
t0_init_us=0.0, l_scale_init=1.0 | Initial energy-scale values. |
energy_scale_flight_path_m=25.0 | Nominal flight path for energy-scale fitting. |
resolution=... | Tabulated resolution from load_resolution(...). Mutually exclusive with the Gaussian parameters below — pass either resolution= (tabulated) or the flight_path_m/delta_t_us/delta_l_m trio (Gaussian), never both. |
flight_path_m=..., delta_t_us=..., delta_l_m=... | Gaussian resolution parameters (mutually exclusive with resolution=). |
fit_energy_range=(emin, emax) | Restrict the cost function to an energy window. |
groups=[...] | Fit isotope groups instead of individual isotopes. |
initial_densities=[...] | Initial density guesses when fitting groups. |
tzero_jacobian="..." | Select the TZERO Jacobian implementation. |
Raw Counts
result = nereids.fit_counts_spectrum_typed(
sample_counts,
open_beam_counts,
energies,
[(u238, 0.0005)],
solver="auto",
c=1.0,
)
solver="auto", "kl", "poisson", and "joint_poisson" all route counts
data to the counts-KL dispatch. Use c=Q_s / Q_ob when sample and open-beam
counts have different proton charge or dwell-time normalization. The primary
GOF for this path is FitResult.deviance_per_dof.
Counts fitting accepts the same temperature, background, group, resolution,
energy-scale, and fit_energy_range options as transmission fitting. Counts
specific options are:
| Option | Meaning |
|---|---|
detector_background=... | Optional 1D detector background spectrum; required when fit_alpha_2=True. |
fit_alpha_1=False, fit_alpha_2=False | Fit counts-domain nuisance/background terms. |
alpha_1_init=1.0, alpha_2_init=1.0 | Initial nuisance/background values. |
c=1.0 | Proton-charge ratio Q_s / Q_ob. |
enable_polish=True/False/None | Override counts-KL polish behavior; None uses the dispatcher default. |
Spatial Mapping
Pre-Normalized Transmission Cubes
data = nereids.from_transmission(transmission_3d, uncertainty_3d)
result = nereids.spatial_map_typed(
data,
energies,
[u238],
initial_densities=[0.0005],
solver="auto",
)
For from_transmission(...) inputs the default solver="lm" and solver="auto"
both route to LM (this is the dispatcher contract in __init__.pyi:
“from_transmission + solver="lm" (default for transmission) → LM”). The
explicit solver="kl" opt-in for from_transmission runs the legacy
Poisson-NLL-on-transmission path. density_maps[0] is the fitted U-238 map.
Raw Count Cubes
data = nereids.from_counts(sample_counts_3d, open_beam_counts_3d)
result = nereids.spatial_map_typed(
data,
energies,
[u238],
initial_densities=[0.0005],
solver="auto",
c=1.0,
)
solver="auto" uses counts-KL for from_counts(...) data and populates
deviance_per_dof_map.
Shape contract:
sample_counts_3d,open_beam_counts_3d,transmission_3d, anduncertainty_3duse shape(n_energy, height, width).energies.shape == (n_energy,).dead_pixels, when supplied, uses shape(height, width)withTruemarking pixels to skip.
Keyword arguments:
| Option | Meaning |
|---|---|
temperature_k=293.6, fit_temperature=False | Fixed or fitted sample temperature. |
initial_densities=[...] | Initial density guesses. |
dead_pixels=... | (height, width) skip mask. |
max_iter=200 | Maximum per-pixel optimizer iterations. |
solver="auto" | Dispatch from input type unless explicitly set. |
background=False | Enable SAMMY-style background for LM/transmission paths. |
fit_back_d=False, fit_back_f=False | Fit the SAMMY exponential background tail (BackD * exp(-BackF / √E)). Requires background=True. Per-pixel back_d_map / back_f_map are populated on the returned SpatialResult (issue #538). |
back_d_init=0.01, back_f_init=1.0 | Initial values for the exponential tail. |
fit_alpha_1=False, fit_alpha_2=False | Fit counts-domain nuisance/background terms. |
alpha_1_init=1.0, alpha_2_init=1.0 | Initial nuisance/background values. |
c=1.0 | Proton-charge ratio for counts-KL spatial fitting. |
enable_polish=True/False/None | Override counts-KL polish behavior; None auto-disables polish for multi-pixel maps. |
fit_energy_scale=False | Fit per-pixel t0_us and l_scale maps. |
t0_init_us=0.0, l_scale_init=1.0 | Initial energy-scale values. |
energy_scale_flight_path_m=25.0 | Nominal flight path for energy-scale fitting. |
resolution=... | Tabulated resolution from load_resolution(...). Mutually exclusive with the Gaussian parameters below — pass either resolution= (tabulated) or the flight_path_m/delta_t_us/delta_l_m trio (Gaussian), never both. |
flight_path_m=..., delta_t_us=..., delta_l_m=... | Gaussian resolution parameters (mutually exclusive with resolution=). |
groups=[...] | Fit isotope groups instead of individual isotopes. |
tzero_jacobian="..." | Select the TZERO Jacobian implementation. |
fit_energy_range=(emin, emax) | Restrict the cost function to an energy window. |
TIFF and NeXus I/O
stack = nereids.load_tiff_stack("transmission_stack.tif")
folder_stack = nereids.load_tiff_folder("frames", pattern="frame_*.tif")
sample = nereids.load_nexus_histogram("sample.nxs")
open_beam = nereids.load_nexus_histogram("open_beam.nxs")
energies = nereids.tof_to_energy_centers(
sample.tof_edges_us,
sample.flight_path_m or 25.0,
)
See Data I/O and NeXus/TOF for ordering and pairing rules.
Element and Utility APIs
nereids.element_symbol(92) # "U"
nereids.element_name(92) # "Uranium"
nereids.parse_isotope_str("U-238") # (92, 238)
nereids.natural_abundance(92, 238)
nereids.natural_isotopes(26)
nereids.tof_to_energy(tof_us, flight_path_m)
nereids.energy_to_tof(energy_ev, flight_path_m)
How This Page Is Generated
The published docs site renders three things side by side:
| Site path | Source | What it shows |
|---|---|---|
/ (this page) | Hand-maintained docs/guide/src/python-api.md | Curated narrative tour of the typed APIs |
/python/ | pdoc over the installed nereids wheel and nereids/__init__.pyi stubs | Auto-generated exhaustive reference |
/api/ | cargo doc (rustdoc) | Rust crate API reference |
To rebuild the whole site locally:
pixi run doc-build # depends on: doc-guide, doc-api, doc-python
pixi run doc # serves target/book/ at http://localhost:8000
doc-python invokes pdoc -o target/book/python --no-show-source nereids
after pixi run build has produced an importable wheel. Whenever
bindings/python/python/nereids/__init__.pyi or PyO3 docstrings in
bindings/python/src/lib.rs change, both the auto-generated python/
reference and any affected sections of this curated page should be reviewed
in the same PR.
This page does not execute notebooks or compile-test Python snippets. The
Rust quickstart on this site IS compile-tested by cargo check --workspace --examples (see crates/nereids-fitting/examples/quickstart.rs).
Data I/O and NeXus/TOF
This page documents the Python-facing TIFF, NeXus, normalization, and TOF energy-grid behavior outside the MCP-specific workflow page.
Axis Convention
NEREIDS uses the spectral axis first:
(n_energy_or_tof, height, width)
This applies to TIFF stacks, NeXus counts, normalized transmission cubes,
uncertainty cubes, and the arrays passed to from_counts(...) and
from_transmission(...).
For single spectra, use 1D arrays with shape (n_energy,).
TIFF Stacks
Use load_tiff_stack(...) for a multi-frame TIFF:
import nereids
transmission = nereids.load_tiff_stack("transmission_stack.tif")
# transmission.shape == (n_frames, height, width)
Use load_tiff_folder(...) for a directory of single-frame TIFF files:
transmission = nereids.load_tiff_folder("frames", pattern="frame_*.tif")
Folder frames are sorted lexicographically by filename. Use zero-padded names
such as frame_0001.tif, frame_0002.tif, and so on. The optional pattern
matches filenames, not full paths, and supports * and ?.
Normalization
Raw sample and open-beam arrays can be normalized to transmission:
transmission, uncertainty = nereids.normalize(
sample_counts,
open_beam_counts,
pc_sample=sample_proton_charge,
pc_ob=open_beam_proton_charge,
)
The formula is:
T = (C_sample / C_open_beam) * (PC_open_beam / PC_sample)
sample_counts and open_beam_counts must have identical shape. Optional
dark_current is a 2D (height, width) array.
For fitting raw counts directly, prefer from_counts(...) or
fit_counts_spectrum_typed(...) so the counts-KL dispatch can use the
counts-domain likelihood.
NeXus Histogram Loading
For agent-orchestrated NeXus workflows driven by a manifest, see MCP Server. This section covers the raw-Python loader.
Use probe_nexus(...) to inspect a file without loading full data:
meta = nereids.probe_nexus("sample.nxs")
print(meta.has_histogram, meta.has_events, meta.flight_path_m)
Use load_nexus_histogram(...) for pre-histogrammed data:
sample = nereids.load_nexus_histogram("sample.nxs")
open_beam = nereids.load_nexus_histogram("open_beam.nxs")
assert sample.counts.shape[0] == sample.tof_edges_us.shape[0] - 1
The loader reads VENUS/rustpix-style histogram data from
/entry/histogram/counts and returns:
counts:float64array with shape(n_tof, height, width).tof_edges_us: ascending TOF bin edges in microseconds.flight_path_m: optional file metadata.dead_pixels: optional(height, width)mask.
Histogram files may contain multiple rotation angles. The default
multi_angle_mode="error" rejects those files because silently summing
projection angles loses information. Choose explicitly:
summed = nereids.load_nexus_histogram("scan.nxs", multi_angle_mode="sum")
angle0 = nereids.load_nexus_histogram(
"scan.nxs",
multi_angle_mode="select",
angle_index=0,
)
NeXus Event Loading
Use load_nexus_events(...) when event data must be histogrammed at load
time:
events = nereids.load_nexus_events(
"events.nxs",
n_bins=2000,
tof_min_us=10.0,
tof_max_us=50000.0,
height=512,
width=512,
)
The event loader reads /entry/neutrons/event_time_offset, /x, and /y,
bins events into a linear TOF grid, and returns the same NexusData shape
contract as the histogram loader.
TOF Edges to Energy Centers
NeXus loaders return counts in ascending TOF order. Neutron energy decreases
as TOF increases, so direct TOF-bin conversion would be descending in energy.
tof_to_energy_centers(...) returns ascending energy centers suitable for
NEREIDS fitting:
flight_path_m = sample.flight_path_m or 25.0
energies = nereids.tof_to_energy_centers(
sample.tof_edges_us,
flight_path_m,
delay_us=0.0,
)
When pairing these energies with NeXus counts, keep arrays aligned. The MCP workflow reverses counts to the same ascending-energy order before fitting. For direct Python workflows, use this pattern:
energies = nereids.tof_to_energy_centers(sample.tof_edges_us, flight_path_m)
# load_nexus_histogram returns ascending TOF. Reverse axis 0 to align with
# ascending energy centers.
sample_counts = sample.counts[::-1, :, :]
open_beam_counts = open_beam.counts[::-1, :, :]
data = nereids.from_counts(sample_counts, open_beam_counts)
result = nereids.spatial_map_typed(data, energies, [u238], c=charge_ratio)
If you construct an energy grid yourself, make sure the grid and every
spectral array use the same order and that energies.shape[0] matches the
first array dimension.
Counts vs Transmission Fitting
Use counts APIs when you have raw sample and open-beam counts:
fit = nereids.fit_counts_spectrum_typed(
sample_counts_1d,
open_beam_counts_1d,
energies,
[(u238, 0.0005)],
c=charge_ratio,
)
Use transmission APIs when your data is already normalized:
fit = nereids.fit_spectrum_typed(
transmission_1d,
uncertainty_1d,
energies,
[(u238, 0.0005)],
)
For spatial maps, the same distinction is encoded by the input constructor:
counts_data = nereids.from_counts(sample_counts_3d, open_beam_counts_3d)
trans_data = nereids.from_transmission(transmission_3d, uncertainty_3d)
MCP Server
NEREIDS can run as a local Model Context Protocol (MCP) server so an AI agent can inspect neutron-resonance inputs, validate a dataset manifest, and launch spectrum or density-map fitting through the Python bindings. The MCP server is intended for local agent orchestration, for example a user prompt such as “help me process the data here” in a directory that contains a NEREIDS manifest.
The MCP interface is experimental. It is useful for demos and agent-assisted workflows, but the Python and Rust APIs remain the stable interfaces for scripted analysis.
See also: Data I/O and NeXus/TOF for the raw-Python NeXus, TIFF, normalization, and TOF energy-grid flow that the MCP workflow wraps.
Installation
Install the optional MCP dependency and run the stdio server:
pip install "nereids[mcp]"
nereids-mcp
From a source checkout, build the Python extension first:
pip install maturin
maturin develop --release -m bindings/python/Cargo.toml
pip install "fastmcp>=3.0"
python -m nereids.mcp
In the repository Pixi environment, fastmcp is intentionally not a default
dependency because it can conflict with conda metadata packages. Install it
manually in the environment when MCP support is needed.
Behavior when fastmcp is not installed
The two entry points differ on purpose:
| Entry point | Without fastmcp |
|---|---|
nereids.mcp.main() (called by the nereids-mcp console script) | Raises ImportError("fastmcp is required for the MCP server. Install it with: pip install nereids[mcp]"). |
nereids.mcp.mcp (lazy attribute) | Raises AttributeError with the same install instruction embedded in the message. |
The attribute path raises AttributeError rather than ImportError so that
attribute-walking tools (pdoc, IDE introspection, hasattr) can treat mcp
as absent rather than crashing. Callers who explicitly catch ImportError
to detect a “MCP not installed” state should switch to either calling
nereids.mcp.main() (which still raises ImportError) or using
hasattr(nereids.mcp, "mcp") as the feature-detection probe.
Client Configuration
An MCP client can launch the server with the nereids-mcp console script:
{
"mcpServers": {
"nereids": {
"command": "nereids-mcp"
}
}
}
Tools
The server exposes two groups of tools.
Low-level physics tools operate on an in-memory isotope registry:
list_isotopes(z)lists naturally occurring isotopes.load_endf(isotope, library="endf8.1")loads resonance data such as"U-238"or"Fe-56"into the registry.get_resonance_parameters(isotope)returns loaded resonance metadata.compute_cross_sections(...),compute_transmission(...),forward_model(...), anddetect_isotopes(...)run direct physics calculations.
Workflow tools operate on a dataset directory or a manifest path:
extract_resonance_manifest(dataset_path)reads the manifest and returns parsed metadata.validate_resonance_dataset(dataset_path)checks required paths, isotope entries, and resolution configuration.process_resonance_dataset(dataset_path, output_dir=None, max_pixels=None, dry_run=False)runs the analysis and writes compact result artifacts.
Dataset Manifests
The workflow tools look for one of these files in the dataset directory:
manifest_intermediate.mdsmcp_manifest.mdnereids_manifest.mdnereids_mcp.jsonanalysis.json
Markdown manifests must contain JSON-compatible frontmatter between ---
delimiters. Pure JSON manifests must contain the frontmatter object directly.
The workflow configuration may be placed under analysis, workflow, or
processing; otherwise the root object is treated as the workflow.
Minimal single-spectrum manifest:
---
{
"name": "u238-spectrum-demo",
"tool": "nereids",
"physics": "neutron-resonance",
"analysis": {
"mode": "single_spectrum",
"data": {
"kind": "transmission_npz",
"path": "spectrum.npz"
},
"isotopes": [
{
"isotope": "U-238",
"initial_density": 0.001,
"library": "endf8.1"
}
],
"fit": {
"solver": "lm",
"max_iter": 100
},
"resolution": {
"kind": "gaussian",
"flight_path_m": 25.0,
"delta_t_us": 0.5,
"delta_l_m": 0.005
},
"output": {
"directory": "output"
}
}
}
---
For synthetic demo data, resolution can be disabled:
"resolution": {"kind": "none"}
For real instrument data, use Gaussian resolution parameters or a tabulated resolution file. Synthetic data often does not need an instrument resolution file; real experiments normally do.
Supported Workflow Inputs
single_spectrum fits one spectrum. It supports:
counts_npzorcounts: a.npzfile withsample_counts,open_beam_counts, andenergies_evby default. Arrays may be 1D spectra or 3D cubes; 3D cubes are summed over pixels before fitting one spectrum.transmission_npz,transmission, orspectrum: a.npzor text file withtransmission, optionaluncertainty, and an energy grid.
density_map and spatial_map fit every pixel in a 3D cube. They support:
transmission_npzortransmission: a.npzfile with 3Dtransmissionand optional 3Duncertaintyarrays.transmission_tiffortiff: a multi-frame TIFF transmission stack plus anenergy_gridentry.counts_npzorcounts: a.npzfile with 3Dsample_countsandopen_beam_countsarrays.nexus_histogramornexus: sample and open-beam NeXus histogram files, configured withsample_pathandopen_beam_path.
Paired arrays must have matching shapes. The number of energy points must match the first axis of the data arrays. Energy grids must be strictly monotonic. See Data I/O and NeXus/TOF for the energy-ordering contract (descending grids are reversed with the aligned arrays before fitting).
For NeXus histogram inputs, MCP follows the conventions documented in
Data I/O and NeXus/TOF. Briefly: NeXus loaders return counts
in ascending TOF order; the workflow derives ascending energy centers with
tof_to_energy_centers(...) and reverses counts along axis 0 to match before
fitting. If the manifest does not specify flight_path_m, the loader metadata
is used when available, with a 25 m fallback. delay_us defaults to 0.
Example NeXus density-map manifest:
---
{
"name": "venus-nexus-density-map",
"tool": "nereids",
"analysis": {
"mode": "density_map",
"data": {
"kind": "nexus",
"sample_path": "sample.nxs",
"open_beam_path": "open_beam.nxs",
"flight_path_m": 25.0,
"delay_us": 0.0
},
"isotopes": [
{"isotope": "U-238", "initial_density": 0.001}
],
"fit": {
"solver": "lm",
"max_iter": 100
},
"resolution": {
"kind": "gaussian",
"flight_path_m": 25.0,
"delta_t_us": 0.5,
"delta_l_m": 0.005
}
}
}
---
Result Files
process_resonance_dataset(...) writes outputs under the configured output
directory, or under <dataset>/output by default.
For single_spectrum, it writes:
nereids_spectrum_fit.npznereids_mcp_result.json
For density_map or spatial_map, it writes:
nereids_density_map.npznereids_mcp_result.json
The JSON summary is strict JSON: non-finite fit values are represented as
null, not NaN or Infinity.
GUI Walkthrough
The NEREIDS desktop application provides interactive neutron resonance imaging analysis with visual feedback at every step.
The screenshots on this page cover the current guided workflow screens: landing, load, configure, analyze, results, studio, forward model, detectability, and periodic table. When workflow labels, solver controls, or project-file behavior changes, refresh these images together with this page.
Launch
# Homebrew (macOS)
brew install --cask ornlneutronimaging/nereids/nereids
# Or pip
pip install nereids-gui
nereids-gui
# Or from source
cargo run --release -p nereids-gui
Landing Page
The landing page presents three entry points:
- Load & Fit Data – open the wizard for single-spectrum or spatial-map fitting
- Forward Model – explore theoretical transmission spectra without loading data
- Detectability – estimate trace-isotope sensitivity before an experiment

Decision Wizard
After selecting Load & Fit Data, a short wizard asks:
- Fitting type: Single spectrum or spatial map
- Data format: “Raw Events (HDF5/NeXus)”, “Histogram, Pre-Normalization”, or “Transmission (Already Normalized)” — quoted as the wizard cards label them
The wizard configures a dynamic pipeline with only the steps relevant to your data format. Six distinct pipelines are available.
Pipeline Steps
Load
Select sample data, open beam, and spectrum files. Supports multi-frame TIFF stacks, TIFF folders, and NeXus/HDF5 event data. The GUI auto-detects the file format and loads data when all fields are filled.

Normalize
For histogram/pre-normalization pipelines, including TIFF pair and HDF5
sample/open-beam counts, the Normalize step computes transmission from sample
and open-beam measurements. Raw event pipelines bin events first. Transmission
pipelines skip normalization because T(E) = I/I0 is already supplied.
Configure
Select isotopes of interest from the periodic table. ENDF nuclear data is fetched automatically from IAEA servers and cached locally. Each isotope shows a status badge (Pending, Fetching, Loaded, Failed).
Configure beamline parameters (flight path, timing resolution) and solver settings (Levenberg-Marquardt or Poisson KL divergence).

Analyze
Run the fit. For spatial maps, a progress bar tracks per-pixel fitting with rayon parallelism. Click any pixel to inspect its individual fit. Fit feedback shows green (good fit) or red (failed) status.
Draw regions of interest (ROI) with Shift+drag. Multiple ROIs are supported with move, select, and delete operations.

Restricting the fit energy range (SAMMY EMIN/EMAX)
By default NEREIDS fits the entire loaded energy grid. The advanced solver
panel exposes a “Restrict fit energy range” checkbox (equivalent to
SAMMY’s EMIN/EMAX analysis limits) that limits the cost
function to a user-specified [E_min, E_max] window in eV. Common uses:
- Resolved-resonance region only — exclude the unresolved-resonance and high-energy tails where the model can’t fit;
- Single resonance triplet — focus on a specific feature for fine-grained density / temperature work;
- SAMMY parity — match the EMIN/EMAX restriction used in a reference SAMMY fit so the comparison is apples-to-apples.
When the checkbox is on, two grey dashed vertical lines on the spectrum plot mark the active boundaries (visible on the energy-eV axis). The reduced χ² and degrees-of-freedom reported in the fit details count only bins inside the active range.
Resolution-kernel margin (automatic): the broadening kernel pulls model
contributions from outside the user range. NEREIDS handles this transparently
by extending the data slice by ~5×FWHM on each side and masking the cost
function back to [E_min, E_max] — so resonances near the boundaries are
correctly broadened without the user picking a custom margin. This follows
the same endpoint-extension principle as SAMMY’s auxiliary grid (general
construction: user manual Sec. III.A.2(c); the quantitative
[Emin − Wmin, Emax + Wmax] statement, with W the resolution width at each
limit, appears in the Leal-Hwang procedure of Sec. III.B.2); NEREIDS uses a
deliberately conservative ~5×FWHM margin.
The setting persists in .nrd.h5 project files (Option<(f64, f64)>,
default None = full grid for backwards compatibility).
Results
View density maps for each fitted isotope. Summary statistics show convergence rate, median chi-squared, and isotope count. Open results in Studio for detailed exploration.

Studio Mode
Studio provides a “Final Cut”-style workspace for exploring results:
- Document tabs: switch between Analysis, Forward Model, and Detectability
- Main viewer: density map with colormap selection and colorbar
- Spectrum panel: click any pixel to see its fitted spectrum
- Bottom dock: Isotopes, Residuals, Provenance, and Export panels
- Inspector sidebar: per-pixel parameter values

Tools
Forward Model
Compute theoretical transmission spectra for arbitrary isotope mixtures. Adjust densities with sliders and see the spectrum update in real-time. Hero spectrum layout with per-isotope contribution lines.

Detectability
Analyze whether a trace isotope is detectable in a given matrix material. Multi-matrix support with resolution broadening. Shows a delta-T spectrum and verdict badges (DETECTABLE / NOT DETECTABLE / OPAQUE MATRIX).

Periodic Table
Interactive 18-column periodic table for selecting isotopes. Click an element to see its natural isotopes with abundance percentages. Supports multi-select with density input. ENDF availability hints are shown for each isotope based on the currently selected data library.

Project Files
Save and load analysis sessions as HDF5 project files (.nrd.h5):
- Cmd+S (macOS) / Ctrl+S (Linux): quick-save
- File > Save: save with dialog
- File > Open: load a saved project
Project files store raw data, pipeline configuration, and results. Embedded data mode bundles everything into a single portable file.
Notebook Status
The tutorial notebooks live under examples/notebooks/. They are intended as
user-facing examples, not as the primary API contract. The API contract is the
Python type stubs, Python tests, Rust tests, mdBook guide, and Rustdoc.
Verification Status
| Notebook group | Current status | Smoke-tested in CI | External requirements |
|---|---|---|---|
foundations/ | Current examples for cross-sections, broadening, URR, and transmission physics. | No. Covered indirectly by Rust/Python physics tests. | ENDF downloads on first run for notebooks that call load_endf(...). |
building_blocks/ | Current examples for ENDF loading, fitting, grouped isotopes, custom resolution, and TIFF I/O. | No. Core APIs are covered by tests/test_nereids.py. | ENDF downloads on first run; TIFF notebook may require local generated files. |
workflows/ | Current end-to-end synthetic workflows. | No. | ENDF downloads on first run. |
applications/ | Reference-data workflow. | No. | Requires external PLEIADES/Git LFS data, not bundled in normal PyPI installs. |
As of the docs workflow in this repository, notebooks are not executed by
pixi run doc-guide, pixi run doc-build, or GitHub Pages publishing.
Before using a notebook as release evidence, run it manually or add a
notebook execution job with controlled data and network policy.
First-Run Network Behavior
Notebooks that call nereids.load_endf(...) may need network access the
first time they fetch an isotope/library combination. ENDF files are cached
locally after retrieval. To avoid network access, use local fixtures and
nereids.load_endf_file(...) where practical.
Reference Data
The application notebook uses larger reference data from the PLEIADES test
data repository via tests/data/pleiades_data/. That data is a submodule and
uses Git LFS. It is not guaranteed to be available in a fresh source checkout
unless submodules and LFS objects have been initialized.
git submodule init
git submodule update
cd tests/data/pleiades_data
git lfs pull
Release Expectation
For a release, document one of these states in the release notes:
- notebooks were smoke-run locally with the exact package version,
- notebooks were not run and remain tutorial examples only, or
- a subset was run, with any skipped notebooks and data/network reasons listed explicitly.
Architecture
NEREIDS is organized as a Rust workspace with seven library crates, a GUI application, and Python bindings.
Crate Dependency Graph
endf-mat (standalone lookup tables)
|
nereids-core (types, constants)
/ | \
nereids-endf | nereids-io
(ENDF) | (TIFF, NeXus)
\ |
nereids-physics
(cross-sections)
\
nereids-fitting
(LM, Poisson)
|
nereids-pipeline ── nereids-io
(orchestration)
/ \
nereids-python nereids-gui
(PyO3 bindings) (egui desktop)
Crate Overview
| Crate | Purpose |
|---|---|
endf-mat | Zero-dependency lookup tables: element symbols, MAT numbers, natural abundances, ZA encoding |
nereids-core | Core types (Isotope, Resonance), physical constants, element data, error types |
nereids-endf | ENDF file retrieval from IAEA, local caching, File 2 resonance parameter parsing |
nereids-physics | Cross-section calculation (Reich-Moore, SLBW, RML, URR), Doppler/resolution broadening, Beer-Lambert transmission |
nereids-io | TIFF stack and NeXus/HDF5 loading, TOF-to-energy conversion, normalization, export |
nereids-fitting | Levenberg-Marquardt and Poisson KL divergence optimizers, transmission fit model |
nereids-pipeline | Single-spectrum fitting, per-pixel spatial mapping (rayon), trace detectability |
nereids-python | PyO3 Python bindings (not published to crates.io) |
nereids-gui | egui desktop application (not published to crates.io) |
Data Flow
The standard analysis pipeline processes data through these stages:
Raw TOF data (TIFF/NeXus)
│
▼
Normalization (sample / open_beam → transmission) [nereids-io]
│
▼
Energy conversion (TOF bin edges → energy centers) [nereids-io]
│
▼
ENDF data (fetch resonance parameters from IAEA) [nereids-endf]
│
▼
Forward model (cross-sections → broadening → T(E)) [nereids-physics]
│
▼
Fitting (minimize |T_measured - T_model|) [nereids-fitting]
│
▼
Spatial mapping (fit each pixel in parallel) [nereids-pipeline]
│
▼
Density maps, chi² maps, convergence maps [output]
Key Design Decisions
Exact SAMMY Physics
All physics modules implement the exact formalisms from the SAMMY Fortran code, with no ad-hoc approximations. Every module references specific SAMMY source files and equation numbers. See the Physics Reference for details.
Workspace Architecture
The workspace is structured so that each crate has a single responsibility
and minimal dependencies. nereids-core is the foundation with zero internal
dependencies. Higher-level crates compose lower-level ones.
See ADR 0001 for the full rationale.
Parallel Spatial Mapping
Per-pixel fitting uses rayon for data parallelism. The outer pixel loop runs on a dedicated thread pool to avoid deadlocking with inner parallel operations (cross-section calculation, broadening).
Python Bindings and MCP
The Python bindings expose a high-level API (load_endf, load_endf_file,
forward_model, fit_spectrum_typed, fit_counts_spectrum_typed,
spatial_map_typed) that maps directly to the Rust pipeline. Typed input
constructors (from_transmission, from_counts) select the fitting dispatch
for spatial maps. TIFF/NeXus I/O helpers (load_tiff_stack,
load_tiff_folder, probe_nexus, load_nexus_histogram,
load_nexus_events, tof_to_energy_centers) expose the same spectral-axis
conventions as nereids-io. NumPy arrays are zero-copy where possible via the
numpy crate integration.
The MCP server is a thin Python package layer over these bindings. It exposes low-level physics tools and manifest-driven workflow tools for local AI-agent orchestration; it does not add a separate fitting engine.
Physics Reference
NEREIDS implements exact SAMMY physics for neutron resonance imaging. This chapter is a navigation guide to the rustdoc API documentation, not a standalone physics textbook.
All implementations reference specific sections of the SAMMY manual and SAMMY Fortran source files. See the rustdoc for each module for detailed equations and citations.
Cross-Section Formalisms
| Formalism | ENDF LRF | Module | SAMMY Reference |
|---|---|---|---|
| Reich-Moore | LRF=3 | reich_moore | Manual Sec. II, rml/ |
| Breit-Wigner (single- and multi-level) | LRF=1,2 | slbw | Manual Sec. II, mlb/ |
| R-Matrix Limited | LRF=7 | rmatrix_limited | Manual Sec. II |
| Unresolved Resonance Region | LRU=2 | urr | Manual Sec. VIII.A, acs/ (FITACS) |
The urr module computes energy-averaged Hauser-Feshbach cross-sections from
the average resonance parameters. The width-fluctuation correction is not yet
implemented: the AMUN/AMUF degrees of freedom are parsed from ENDF File 2
but not yet used in the cross-section computation.
The penetrability and
channel modules provide the underlying
nuclear physics: hard-sphere phase shifts, penetrability factors, wave numbers,
and statistical spin weights.
Broadening Models
Doppler Broadening
Free Gas Model (FGM) convolution accounting for thermal motion of target nuclei.
- Module:
doppler - SAMMY reference:
fgm/module (Dopfgm), manual Sec. III.B.1 - Key function:
doppler_broaden()— exact Free Gas Model convolution integral in velocity space (manual Eq. III B1.7, w²-weighted integrand); no psi/chi (Voigt) approximation is used
Resolution Broadening
Instrument resolution broadening from flight-path uncertainty, timing jitter, and moderator pulse width.
- Module:
resolution - SAMMY reference:
convolution/module, manual Sec. III.C - Supports: Gaussian convolution, Gaussian + exponential tail, tabulated resolution functions
Transmission Model
Beer-Lambert transmission: T(E) = exp(-sum_i n_i sigma_i(E))
Where n_i is the areal density (atoms/barn) and sigma_i(E) is the broadened total cross-section for isotope i.
- Module:
transmission - SAMMY reference:
cro/,xxx/modules, manual Sec. II; transmission experiments Sec. III.E.1 - Handles multi-isotope samples with shared Doppler temperature (one global temperature parameter, optionally fitted jointly with densities)
Fitting Engines
Levenberg-Marquardt
Standard nonlinear least-squares minimization for Gaussian-distributed data.
- Module:
lm - SAMMY reference:
fit/module, manual Sec. IV - Parameters: areal densities with optional bounds, optional temperature fitting
Poisson KL Divergence
Maximum-likelihood fitting for low-count data where Gaussian statistics break down.
- Module:
joint_poisson– counts-domain joint-Poisson fit (conditional binomial deviance); the production path for counts data - Module:
poisson– transmission-domain Poisson likelihood (projected damped Gauss-Newton); used for the transmission + PoissonKL combination - Reference: TRINIDI approach (
trinidi/reconstruct.py)
ENDF Nuclear Data
Resonance parameters are sourced from evaluated nuclear data libraries (ENDF/B from NNDC with IAEA fallback; the other libraries from IAEA):
- Module:
retrieval– download and cache - Module:
parser– parse ENDF-6 File 2 - Module:
resonance– data structures
Supported libraries: ENDF/B-VIII.0, ENDF/B-VIII.1, JEFF-3.3, JENDL-5, TENDL-2023, CENDL-3.2.
Further Reading
- SAMMY User’s Guide (ORNL/TM-9179/R8)
- ENDF-6 Formats Manual (BNL-203218-2018-INRE)
- ENDF/B-VIII.0 (Nuclear Data Sheets, 2018)
Contributing
Development Setup
git clone https://github.com/ornlneutronimaging/NEREIDS.git
cd NEREIDS
cargo build --workspace
For Python binding development, use pixi:
pixi run build # maturin release build
pixi run test-python # pytest
Pre-Commit Checklist
Run these three commands before every commit:
cargo fmt --all
cargo clippy --workspace --exclude nereids-python --all-targets -- -D warnings
cargo test --workspace --exclude nereids-python
cargo fmtapplies formatting (not just--check)cargo clippytreats all warnings as errorsnereids-pythonis excluded because it requires PyO3/maturin build setup
Branch and PR Workflow
- Create a feature branch from
main - Make changes, commit with GPG signatures (
git commit -S) - Push and open a PR against
main - All PRs go through the review pipeline before merge
The repository uses a single remote (origin = ornlneutronimaging/NEREIDS).
All branches and PRs are pushed directly.
Code Guidelines
Physics Modules
- Implement exact SAMMY physics – no ad-hoc approximations
- Reference SAMMY source files and equation numbers in doc comments
- Validate against SAMMY’s own test cases as ground truth
General
- Validate configuration up-front in public entry points
- Guard NaN with
.is_finite()(NaN bypasses comparison guards) - Guard empty collections explicitly (
.is_empty()) - Use named constants instead of magic numbers
- Prefer
return Err(...)for input validation, notdebug_assert!
Testing
# Rust tests
cargo test --workspace --exclude nereids-python
# Python tests (requires pixi)
pixi run test-python
# Build docs locally
cd docs/guide && mdbook build && mdbook serve
Documentation and Release Checklist
Before a release or a documentation-focused PR, verify:
pixi run doc-guidebuilds the mdBook guide.pixi run doc-buildbuilds mdBook, builds Rustdoc, and copies Rustdoc totarget/book/api.- The Python API reference matches the shipped
bindings/python/python/nereids/__init__.pyistubs for public functions, arguments, array shapes, and result objects. - The Data I/O and NeXus/TOF page matches current TIFF,
NeXus, normalization, and
tof_to_energy_centers(...)behavior. - PyPI metadata in
pyproject.tomllists current optional extras, includingmcpandgui. - GUI installation docs cover both
pip install "nereids[gui]"and the directnereids-guipackage, plus Homebrew/source options. - GUI screenshots still match the current landing, guided workflow, studio, and tools screens.
- Notebook release notes state whether notebooks were smoke-run, which subset was run, and which require ENDF network access or external PLEIADES/Git LFS data.
- Rust quickstart snippets have been checked against current public APIs.
Project Structure
NEREIDS/
crates/
endf-mat/ # Element/MAT lookup tables
nereids-core/ # Core types and constants
nereids-endf/ # ENDF retrieval and parsing
nereids-physics/ # Cross-section physics
nereids-fitting/ # Optimization engines
nereids-io/ # Data I/O (TIFF, NeXus)
nereids-pipeline/ # Orchestration
bindings/python/ # PyO3 Python bindings
apps/gui/ # egui desktop application
docs/
guide/ # mdBook user guide (this site)
adr/ # Architecture decision records
references/ # Reference materials
Useful Commands
| Task | Command |
|---|---|
| Build all | cargo build --workspace |
| Run tests | cargo test --workspace --exclude nereids-python |
| Format | cargo fmt --all |
| Lint | cargo clippy --workspace --exclude nereids-python --all-targets -- -D warnings |
| Build Python | pixi run build |
| Test Python | pixi run test-python |
| Build docs | cd docs/guide && mdbook build |
| Build full docs site | pixi run doc-build |
| Serve docs | cd docs/guide && mdbook serve |
| Build rustdoc | cargo doc --workspace --no-deps --exclude nereids-python |