Parse MCNP Output
Nucleide reads several common MCNP-family files without running MCNP itself.
This tutorial shows the Python API; equivalent Rust types live in the mcnp-io
crate.
xsdir
An xsdir file indexes the cross-section tables available to MCNP.
from nucleide.mcnp import read_xsdir
xs = read_xsdir("fixtures/mcnp/xsdir/dummy_xsdir")
print(xs.datapath)
for t in xs.tables[:5]:
print(t.name, t.zaid())
meshtal
A meshtal file contains one or more FMESH tallies.
from nucleide.mcnp import read_meshtal
mt = read_meshtal("fixtures/mcnp/meshtal/mcnp_meshtal_single_meshtal.txt")
print(mt.version, mt.histories)
t4 = mt.tallies[4]
print(t4.dims(), t4.num_ves())
result_array() / totals_array() expose the same tables as NumPy
arrays (requires NumPy at runtime; to_list() / totals_list() stay
the NumPy-free plain-copy path). Each returns a (result, rel_error)
pair: result_array() yields owned writable C-order float64
(ve, group) arrays with ve = (i * ny + j) * nz + k, while
totals_array() yields (num_ves,) totals pairs:
import numpy as np
from nucleide.mcnp import read_meshtal
mt = read_meshtal("fixtures/mcnp/meshtal/mcnp_meshtal_single_meshtal.txt")
t = mt.tallies[4]
result, rel_err = t.result_array()
print(result.shape, result.dtype, result.flags["C_CONTIGUOUS"])
totals, totals_err = t.totals_array()
print(totals.shape, totals.dtype)
Two hardening notes for deck-adjacent work: cell-FILL universe
matrices are capped at 1 000 000 cells (hostile i:j ranges fail with
a cap error before any allocation — larger lattices must be built
programmatically), and raw nucid integers outside the validated
(Z, A, state) domain render the diagnostic Z{z}A{a}[m{s}] fallback
in to_name / ARMI labels instead of panicking (validate untrusted
integers with try_from_nucid / is_valid on the Rust side).
MCTAL, WWINP, PTRAC, and SSW
from nucleide.mcnp import read_mctal, read_ptrac, read_ssw, read_wwinp
k = read_mctal("fixtures/mcnp/mctal/synthetic_kcode5.mctal")
ww = read_wwinp("fixtures/mcnp/wwinp/mcnp_wwinp_wwinp_np.txt")
pt = read_ptrac("fixtures/mcnp/ptrac/mcnp_ptrac_i4_little.ptrac")
ss = read_ssw("fixtures/mcnp/ssw/mcnp_surfsrc_onetrack.w")
read_mctal returns the header, the standard tally bodies (tallies, each
with per-card {count, values, variant, flag} bins plus (value, rel_error)
vals pairs, an optional tfc fluctuation-chart block, and a total), the
rectangular/cylindrical/spherical mesh tallies (mesh_tallies, with the
ni/nj/nk mesh counts and cora/corb/corc bounds), and the kcode
cycles. Total/cumulative card variants (ut/uc/…) and the c/e/t
third-token flags are stored verbatim, never interpreted. Radiograph and
point-detector specials plus perturbation bodies are not yet supported —
reading them raises a clear error instead of parsing silently. tally_vals_array(number) exposes one tally’s
pairs as a NumPy (n_pairs, 2) array (mesh_tally_vals_array covers mesh
tallies).
Writing SSW files
Use write_ssw to write a modified surface-source file back to disk
(writes scratch files, so this snippet is excluded from code execution;
tests/test_mcnp.py covers the writer):
from nucleide.mcnp import write_ssw
write_ssw(ss, "path/to/output.ssw")
Input decks: parse, edit, write back
DeckProblem parses a full input deck (message/title/cell/surface/data
blocks) into typed cell/surface cards plus material and passthrough data
cards, and writes it back byte-identical when unedited — only cards touched
through the setters re-render canonically (paths are placeholders, so this
snippet is excluded from code execution):
from nucleide.mcnp import read_deck
deck = read_deck("path/to/model.i")
print(deck.title, [c["num"] for c in deck.cells])
deck.set_cell_density(1, -10.0)
open("path/to/model_edited.i", "w").write(deck.dumps())
SDEF source cards
parse_sdef reads one legacy SDEF source definition plus its SIn/SPn/
SBn distribution cards into typed dicts. The accepted keyword set is
POS/CELL/SURF/VEC/DIR/ERG/NRM/PAR/WGT/TME, and only the
discrete distribution forms (SIn L, SPn D, SBn D) parse — non-discrete
options, dangling distribution references, and length mismatches all raise a
ValueError:
from nucleide.mcnp import parse_sdef
parsed = parse_sdef("SDEF POS=0 0 0\n ERG=D1\nSI1 L 0.662 1.17\nSP1 D 0.5 0.5")
print(parsed["pos"], parsed["erg"]) # 0 0 0, D1
print(parsed["distributions"][0]["si"]) # 0.662 1.17
print(parsed["card"]) # canonical re-render of the full card
Unknown keywords are not dropped silently: they land in the ignored list as
drift notes — entries in the drift report, the running list of every judgment
call the parser made. A deck read through read_deck/parse_deck exposes the
same view as the sdef property (None when the deck has no source card),
and cards emitted by the spectroscopy SDEF source round-trip byte-identical:
from nucleide import mcnp, spectroscopy
bins, card = spectroscopy.sdef_decay_source([(0.662, 2.0), (1.33, 1.0)])
assert mcnp.parse_sdef(card)["card"] == card
deck = mcnp.parse_deck("msg\ntitle\n1 0 -1\n\n1 so 1.0\n\n" + card + "\n")
print(deck.sdef["erg"]) # D1
See Emit an SDEF decay source for the emitter side.
L3 semantics: universes, lattices, tallies, and validation
DeckProblem also exposes typed semantic views over the raw cards —
MODE particles, TRn transforms, auto-created universes (U/-U),
LAT/FILL assignments, importances, volumes, and F/FM/E tallies —
plus validate(), which centralizes duplicate-number, dangling-link,
redundant-definition, and write-time-state checks (lattice/fill
cross-checks are nucleide-defined: LAT without FILL, and a FILL
matrix without LAT, are errors). Particle/mode mismatches are notes,
not errors:
from nucleide.mcnp import read_deck
deck = read_deck("fixtures/mcnp/inp/deck_l3.txt")
print(deck.mode) # {'particles': 'N P'}
print([(u["number"], u["cells"]) for u in deck.universes])
print(deck.fills[0]["universes"], deck.tallies[0]["e_bins"])
deck.validate()
print(deck.validation_notes())
deck.set_cell_universe(4, 5, True) # writes U=-5
deck.validate()
Sample files
Sample reference files live under fixtures/mcnp/. The test suite checks
that Nucleide reproduces them byte-for-byte where exact reproduction is
intended; see the
sample data files index for the full list.
ENDL, SSW combining, PTRAC export, and mesh data
The same module covers the remaining legacy touchpoints (all Python-side except where noted):
import tempfile
from nucleide.mcnp import (
combine_ssw_files,
meshtal_mesh_data,
ptrac_event_rows,
read_endl,
write_ptrac_hdf5,
)
tmp = tempfile.mkdtemp(prefix="nucleide-mcnp-")
ref_w = "fixtures/mcnp/ssw/mcnp_surfsrc_onetrack.w"
lib = read_endl("fixtures/endl/synthetic_eedl.txt")
print(lib.nuclides()) # [820000000]
print(lib.get_rx(820000000, 9, 10, 0)[:1]) # integrated table rows
combine_ssw_files(f"{tmp}/merged.w", [ref_w, ref_w])
rows = ptrac_event_rows("fixtures/mcnp/ptrac/mcnp_ptrac_i4_little.ptrac") # 19 PtracEvent columns, no HDF5 needed
The HDF5 writer needs your h5py (bytes never asserted here; covered by
tests/test_mcnp_io.py), so it is excluded from code execution:
from nucleide.mcnp import write_ptrac_hdf5
write_ptrac_hdf5("fixtures/mcnp/ptrac/mcnp_ptrac_i4_little.ptrac", "run.h5")
```python
from nucleide.mcnp import meshtal_mesh_data
mesh = meshtal_mesh_data("fixtures/mcnp/meshtal/mcnp_meshtal_single_meshtal.txt", as_numpy=True)
print(mesh["tallies"][4]["result"].shape) # (ve, groups)
meshtal_mesh_data returns plain dicts/arrays only — MOAB tagging stays
caller-side. See the
Python API reference for the
full signatures.