Gamma-Ray Spectroscopy
Spectrum smoothing, peak counting, energy/efficiency calibration, X-ray line algebra, SDEF decay-source cards, and the SPE readers behind Nucleide's spectroscopy toolkit.
Overview
Gamma-ray pulse-height spectra arrive as counts per channel plus a header
(recording times, detector id, calibration fits). Answering “how many counts
sit under this peak, and at what energy” needs a small pipeline: smooth the
spectrum, delimit the peak, subtract the background, map channels to energies,
correct for detector efficiency, and account for X-ray lines. Nucleide
implements exactly the upstream analysis subset (E1–E8) in
crates/spectroscopy, with every caller-side constant kept explicit, plus the
E9 decay-source composition: caller-supplied decay lines normalized into
emission probabilities and rendered as an MCNP SDEF card.
Mathematical details
E1 — Rectangular smoothing
With window width m (odd, m >= 3) and ext = (m - 1) / 2:
smooth[i] = (1/m) * sum(counts[i-ext..=i+ext]) for ext <= i < N - ext.
The first and last ext channels have no full neighbourhood and are copied
unchanged.
E2 — Five-point smoothing
smooth[i] = (c[i-2] + c[i+2] + 2*c[i+1] + 2*c[i-1] + 3*c[i]) / 9
for 2 <= i < N - 2; the first and last two channels are copied. The
weights are the low-statistics recommendation cited by the upstream module
as “G.W. Phillips, Nucl. Instrum. Methods 153 (1978), 449” — carried here as
a citation string only, with no journal-verification claim.
E3 — Background under a peak (m == 1 only)
low = sum(counts[c1-2..c1]), high = sum(counts[c2..c2+2]), and
bg = (low + high) * (c2 - c1 + 1) / 6. Only the m == 1 two-sided
estimator exists; any other m is an error. There is no Compton-continuum
or step model.
E4 — Gross counts
gc = sum(counts[c1..c2]), half-open: channel c2 is excluded.
E5 — Net counts
nc = gc - bg.
E6 — Energy calibration
ebin[ch] = a0 + a1*ch + a2*ch^2 from the calib_e_fit triplet. FWHM
coefficients are parsed from the file header but never evaluated.
E7 — Detector-efficiency evaluation (energy in MeV)
With caller-supplied coefficients a_i:
fit == 1:eff = exp(sum a_i * (ln E)^i);fit == 2:eff = exp(sum a_i * (1/E)^i);- anything else is an error.
E7-fit — Efficiency-coefficient fit (extension of E7, not a new equation)
The upstream module ships no efficiency-calibration fitting routine, so the
fit has no upstream parity — it is pinned by synthetic closed-form gates
only. Over caller points (E_i, eff_i, w_i) with E_i > 0, eff_i > 0,
and caller-supplied weights w_i >= 0, the fit solves the log-space linear
least squares
y_i = ln eff_i, X[i][j] = (ln E_i)^j (fit == 1) or (1/E_i)^j
(fit == 2), min_β sum w_i (X_i β − y_i)^2,
returning order + 1 coefficients that feed the E7 evaluation above. The
solve runs through the workspace least-squares kernel (nucleide-linalg
lstsq, faer column-pivoted QR); a zero weight skips its row, all-zero
weights and fewer points than coefficients are errors. This is the one
coefficient-fitting path in the toolkit — every other coefficient vector
(energy fits, X-ray constants, decay lines) stays a caller input.
E8 — X-ray line algebra
From caller-supplied atomic constants (K/L fluorescence yields, the
kb_to_ka / ka2_to_ka1 ratios, the K-fill probability, and the four line
energies) plus K/L conversion coefficients:
- with
k_conv:xk = k_fluor * k_conv,xka = xk / (1 + kb_to_ka),xka1 = xka / (1 + ka2_to_ka1),xka2 = xka - xka1,xkb = xk - xka, and — whenl_convis also present —xl = (l_conv + k_conv * prob) * l_fluor; - with only
l_conv:xl = l_conv * l_fluor; - with neither, all intensities are zero.
Output order is always Ka1, Ka2, Kb, L. No atomic table is vendored, so
every constant is an explicit input; an absent conversion is None
(the upstream NaN sentinel).
E9 — Decay-source normalization and SDEF cards
The upstream point-source emitter renders one monoenergetic particle at a
point: SDEF POS=<x> <y> <z>, VEC=<u> <v> <w> DIR=1 on a continuation
line only when a direction component is nonzero, then ERG=<E>, WGT=<w>,
and PAR=<designator>. E9 generalizes it to a caller-supplied line list
(E_i, I_i) — no line data is vendored, every pair is an input. With
duplicates merged by summing and bins sorted ascending:
p_i = I_i / sum_j I_j, so the emitted probabilities sum to 1.0.
Behavior is specified, not inherited: empty input, negative energies or
intensities, and non-finite values are errors; zero-intensity lines are
dropped (a zero-probability SP1 entry has unverified transport meaning);
input order is irrelevant.
Line lists also arrive from files: the runtime decay-lines TSV interchange
(energy_MeV intensity pairs, # comments, blank lines skipped) parses to
the same (E_i, I_i) pairs E9 consumes — same validation, no vendored data.
No legacy line-list reader ships: upstream line families query a compiled
data store with no text format to mirror, ENSDF text is out of proportion,
ICRP-107 exports are license-blocked, and ENDF mean energies are not lines.
The card keeps the upstream field order. One surviving line keeps the
upstream inline ERG=<E>; several lines switch to the discrete-distribution
form ERG=D1, with the energies on an SI1 L card and the probabilities on
a paired SP1 D card, wrapped at 80 columns onto 5-space continuations.
Numbers render in C++ default six-significant-digit form, matching the
upstream C++ emitter. The PAR= designator comes from the nucleide-nuclei
particle dialect (MCNP5 n/p/e; MCNP6 adds h for protons).
The distribution syntax is verified surface — the card shapes lex, parse,
and round-trip through an MCNP input parser. The sampling semantics behind
them (what the L/D flags select, SPn-omitted defaults, zero/negative
probability handling, MCNP renormalization) are not verified from local
files and are the caller’s responsibility against the MCNP manual.
Re-checked against the parser suite: it exercises only this surface, so the
semantics stay unverified.
Algorithm
Smoothing and counting are single passes over plain vectors. Channel bounds
c1/c2 index the counts vector positionally: a nonzero
start_chan_num never shifts the slice (the upstream quirk), while the
c2 <= max(channels) range check runs first and out-of-range slice stops
clamp like Python slices.
The .spe readers reproduce the upstream grammars quirk-for-quirk. Dollar
format requires the $SPEC_ID: magic (else an unsupported-format error),
resolves duplicate tags first-wins, raises on missing tags, reads $MEAS_TIM:
as live-then-real, takes $DATA: start last with last + 1 one-float lines
after it, and reads the $MCA_CAL:/$SHAPE_CAL: triplets two lines below
each tag (a trailing keV token is ignored). $ROI:, $PRESETS:, and
$ENER_FIT: are never read. Plain format rejects the dollar magic, reads
key: value headers (unknown keys ignored, later keys overwrite), parses
channel: counts lines after the SPECTRUM marker, splits double-spaced
Energy Fit:/FWHM Fit: values at single-space indices 0/2/4, and re-joins
the three colon parts of the acquisition time.
Worked example
Counts [2, 5, 1, 6, 3, 8, 4], peak window c1 = 2, c2 = 5:
- E1 (
m = 5):[2, 5, 17/5, 23/5, 22/5, 8, 4]. - E2:
[2, 5, 30/9, 39/9, 42/9, 8, 4]. - E3:
low = 7,high = 12,bg = 19 * 4 / 6 = 76/6. - E4:
gc = 1 + 6 + 3 = 10. E5:nc = 10 - 76/6 = -16/6.
Assumptions and limitations
- Counts, channels, fits, atomic constants, decay lines, and efficiency-fit points/weights are all caller inputs; nothing is fetched. Coefficient fitting exists on exactly one path (E7-fit above); every other coefficient vector is an input, never fitted.
- FWHM fits are parsed, never evaluated: no evaluation form for the triplet is pinned by the upstream analysis subset, so no evaluator exists here.
- X-ray energies are in keV; efficiency energies are in MeV; SDEF decay-line energies are in MeV.
- Smoothing edges copy rather than extrapolate.
- E9 renders the SDEF/distribution card shapes only; MCNP sampling semantics (distribution flags, defaults, renormalization) are the caller’s responsibility against the MCNP manual.
Related work beyond Nucleide
Peak search, peak fitting, activity calculations, and plotting all sit
outside this toolkit: none exist in the upstream analysis subset, and
plotting stays user-side (the upstream plot helpers are an isolated GUI
module). The E7-fit efficiency-coefficient fit is not peak fitting — it fits
the detector-response curve to caller calibration points. Decay-spectrum
line data also stays out: no evaluated line library is vendored, so E9
sources are built purely from caller-supplied lines. The
validation/spectroscopy_vs_pyne.py harness checks the toolkit two ways:
synthetic closed-form checks in-repo and a live PyNE cross-check in the
validation container.