MCPL Particle Interchange
Nucleide reads and writes MCPL particle lists — the code-neutral interchange
format for particle tracks between Monte Carlo codes (see the
MCPL format page and Kittelmann et
al., Computer Physics Communications 218 (2017), pp. 17–42). This tutorial
covers the Python API; the Rust types live in the mcpl-io crate.
Particle units follow the published format: kinetic energy in MeV, position in cm, time in ms.
import os
import tempfile
from nucleide.mcpl import read_mcpl, write_mcpl
tmp = tempfile.mkdtemp(prefix="nucleide-mcpl-demo-")
demo_path = os.path.join(tmp, "demo.mcpl")
header = {
"srcname": "demo",
"comments": [],
"has_userflags": False,
"has_polarisation": False,
"double_prec": False,
"universal_pdgcode": None,
"universal_weight": None,
"blobs": [],
}
particles = [
{
"ekin": 2.5,
"polarisation": [0.0, 0.0, 0.0],
"position": [0.0, 0.0, 0.0],
"direction": [0.0, 0.0, 1.0],
"time": 0.0,
"weight": 1.0,
"pdgcode": 2112,
"userflags": 0,
}
]
write_mcpl(demo_path, header, particles)
mc = read_mcpl(demo_path)
print(mc.version, mc.nparticles, mc.srcname)
for p in mc.particles():
print(p["ekin"], p["pdgcode"], p["direction"])
A .gz suffix reads and writes through gzip transparently (compressed bytes
are encoder-dependent and never asserted). Files with per-file strides —
universal PDG codes or weights, polarisation vectors, user flags, and double
precision — are selected with the matching header flags; McplFile exposes
each flag (has_userflags, has_polarisation, double_prec,
universal_pdgcode, universal_weight) plus the header comments and
blobs verbatim.
SSW↔MCPL conversion (neutron/gamma-only v1) runs through
nucleide.mcpl.ssw2mcpl and nucleide.mcpl.mcpl2ssw. The SSW format stores
no per-track surface id or particle kind, so ssw2mcpl takes one explicit
surface id plus "neutron"/"gamma" kind per track (lengths must match the
file’s track count); energy maps verbatim in MeV, time maps shakes→ms, and
an options dict selects double precision, surf→userflags, gzip, a deck-embed
blob, srcname, and comments:
import tempfile
from nucleide.mcpl import mcpl2ssw, ssw2mcpl
tmp = tempfile.mkdtemp(prefix="nucleide-mcpl-")
mcpl_path = f"{tmp}/surface.mcpl"
ref_w = "fixtures/mcpl/ssw_conversion/reference.w"
n = ssw2mcpl(ref_w, mcpl_path, [100, 200], ["neutron", "gamma"])
m = mcpl2ssw(mcpl_path, ref_w, f"{tmp}/surface.w") # reference header cloned
mcpl2ssw clones the reference SSW header (code/version/deck passthrough,
counts patched to the converted tally) with surface ids from each particle’s
userflags unless surface= overrides them. Anything outside neutrons
(PDG 2112) and gammas (PDG 22) is an error, never a silent skip; see the
ssw module docs for the mapping table and the list of cases that are not
yet supported (each raises a clear error).
Merging, extracting, and statistics
Four utilities cover the day-to-day particle-list workflows (matching the
upstream mcpltool merge/extract/repair surface plus record statistics):
merge_mcpl(paths, out_path)concatenates compatible files. The first file’s header wins (srcname,comments,blobs) and a provenance comment is appended; header statistics comments (stat:sum:...) from the first file ride along verbatim and sums are never synthesized or updated. All inputs must agree on the header options except floating-point precision, which promotes to double when single- and double-precision files are mixed (the lossless direction); anything else raises a clear error.extract_mcpl(src_path, out_path, options=None)writes a subset with the source header preserved verbatim. The options dict selects either an index range ({"start": 1, "stop": 3}— the half-open interval[start, stop), each bound optional) or a predicate ({"predicate": f}withfover one particle dict); omitting options copies the whole file.mcpl_stats(path)returns record counts, energy moments (sum/min/max/mean in MeV), the total weight, and a per-PDG-code histogram.repair_mcpl(path)fixes a file whose writer never updated the particle count (for example after an interrupted job): the count is recomputed from the complete records, a partially written trailing record is ignored, and the file is rewritten in place.
from nucleide.mcpl import extract_mcpl, mcpl_stats, merge_mcpl
combined = f"{tmp}/combined.mcpl"
n = merge_mcpl([mcpl_path, mcpl_path], combined)
m = extract_mcpl(combined, f"{tmp}/neutrons.mcpl", {"predicate": lambda p: p["pdgcode"] == 2112})
stats = mcpl_stats(f"{tmp}/neutrons.mcpl")
print(stats["nparticles"], stats["ekin_mean"], stats["pdg_counts"])
Cross-tool byte compatibility beyond self-consistent round-trips is
cross-checked against the upstream mcpl package in the automated
validation harness; the check is skipped
when that package is not installed.