Limitations

Not parsed

  • Decay-scheme normalisation beyond the basics. R (reference) records are decoded only via parse_references; keynumbers are not expanded inline.

  • Delayed-particle (D) records are decoded but lightly modelled — the intermediate-level energy and width are typed, the rest stays in raw.

  • ENSDF/2 records (I statistics, indexed levels) are not handled.

  • Most adopted datasets carry two Q records: NNDC prepends one from the newest mass evaluation, then a comment reading “current evaluation has used the following Q record”, then the older set the evaluator worked from. This package takes the first (newer masses) and keeps the rest in metadata["Q_records"]. If you need internal consistency with the evaluation’s own derived quantities, use the later one.

Approximations

  • Asymmetric errors propagate branch-by-branch in quadrature. That is the usual convention, not a rigorous treatment of skewed distributions; if you need that for log ft limits, you want sampling.

  • multiply() symmetrises only in to_ufloat(), never internally.

  • NP for delayed particles is applied without BR, inferred from a single dataset (see Normalization.factor_for). Worth checking against the manual before relying on it.

  • Gamma placement without an FL= continuation is a heuristic energy match.

Data quality in the files themselves

Evaluated files carry occasional transcription errors, and a survey that absorbs them silently puts them straight into a figure. survey.inconsistencies() flags records that contradict themselves — nothing is corrected, because guessing which of two numbers is wrong is not the package’s job:

from nook.survey import survey, inconsistencies

for state, why in inconsistencies(survey()):
    print(state.nuclide, why)

Most checks are statements that cannot be true of any nuclide: a stable nuclide with a negative separation energy; a stable nuclide with a positive Q(β⁻); a neutron-unbound nuclide with a measurable half-life, since neutron emission from an unbound state is prompt; a separation energy of tens of MeV.

The mass surface

The strongest check needs no threshold at all. Separation energies and beta Q-values are differences of the same mass excesses, so they close a loop:

S(n)[Z, A] − S(n)[Z+1, A]  ==  Q(β⁻)[Z, A−1] − Q(β⁻)[Z, A]

Both sides reduce to the same four masses, so this holds exactly, whatever the masses are. A loop that fails to close means at least one of the four numbers is wrong — and it catches errors no single-record check can, because each value looks entirely reasonable on its own.

Across a full distribution the measured loops are sharply bimodal: they close to a median of 0.02 σ, or they fail by more than 20 σ, with nothing in between. So the threshold is not delicate — 5 σ and 20 σ select the same set. Loops containing a value flagged SY or CA are skipped, since extrapolations are not expected to close; those are looser by two orders of magnitude (median 10 keV against 0.4 keV).

Two identities are used. The second is what makes S(p) checkable:

S(p)[Z, A] − S(n)[Z, A]  ==  Q(β⁻)[Z−1, A−1] + Δ(¹H) − Δ(n)

It closes to a median of 0.35 keV over 1739 testable instances.

closure_failures() reports the residual and how many sigma it represents.

Repairing transcription errors

Off by default. Correcting evaluated data is dangerous, and repair() is built so that a correction is never a guess about what a number ought to be:

from nook.repair import repair

fixed, changes = repair(survey())
for change in changes:
    print(change)
# 112Sn s_n: -10788 -> 10788 keV (negate); loop residual 21575 -> 1 keV

Four constraints make it defensible:

  1. A fixed vocabulary. Only negate and powers of ten — the two ways a transcription actually fails. A value wrong by a factor of three has no transcription story behind it and is left alone.

  2. Verified, not assumed. A candidate is accepted only if it closes a mass-surface loop that was open, which is evidence independent of any opinion about the value. It must also not break a loop that was closing.

  3. Only what an identity constrains. Q(α) enters neither identity, so ¹¹⁷Ru’s −91800 keV stays exactly as the file has it, however obviously wrong. SY/CA values are never touched: an extrapolation is uncertain, not wrong, and rewriting one would be a fabrication.

  4. Recorded and visible. Every change carries the original value, the transform, and the residual before and after. Repaired quantities are listed in NuclideSummary.repaired, so downstream code can tell which numbers are no longer the file’s.

On a full distribution this makes 22 repairs — 21 sign flips and one dropped exponent — taking closure failures from 29 to 5. Twenty of the 22 land within 1 keV of closure, from residuals of 10–27 MeV.

An independent check the repair machinery knows nothing about: afterwards, the Cd, Sn and Te isotopic chains show the correct odd-even staggering, with the even-N A=112 members sitting ~2.5 MeV above their A=111 and A=113 neighbours. That is the neutron pairing gap, and nothing in the repair logic is aware pairing exists.

What survives is the honest residue: nine nuclides, each written up with its diagnosis in suspicious entries. The check can localise an error to a loop, and sometimes — as with ²¹⁰Ac, implicated by three independent loops — to a single value; it still cannot always say what the right number is.

Four things this found in one ENSDF snapshot:

  • A=112. Eleven of the fifteen Q records in ensdf.112 have S(n) and S(p) negated. ¹¹²Sn reads S(n) = −10788 ± 5 when the true value is +10.788 MeV — magnitude right, sign wrong. ¹¹¹Sn and ¹¹³Sn are correct with the same AME2012 reference, so it is localised to that file. On a chart of nuclides it appears as a diagonal streak, because A = constant is a diagonal in N–Z.

  • ¹¹⁷Ru has Q(α) = −91800 keV, an order of magnitude out.

  • A=173. The closure test alone catches this one. ¹⁷³Ir’s S(n) field reads 1.0960 where the value should be ≈10960 keV — the exponent is missing, and the loop residual is 10960.9 keV, exactly the lost factor. The same corruption appears in ⁶³Ga’s superseded record.

  • A=210/211. ²¹⁰Ac, ²¹⁰Ra and ²¹¹Ac sit in loops missing closure by 5–10 MeV at 30–70 σ. Each value is individually plausible; only the relationship between them is broken.

A flag is not always an error. ²⁴N, ³³Ne and ³⁹Mg are flagged because their S(n) is slightly negative while they live tens of nanoseconds — but those values carry the SY operator, meaning they are extrapolated from systematics with an uncertainty that straddles zero. The message says (from systematics) so the two cases are distinguishable: an extrapolation to treat with care, versus a measured value quoted to ±5 keV with the wrong sign.

plot_chart(robust=True) is the default for the same reason — one sign-flipped record is enough to stretch a colour scale until the real structure washes out.

Livechart specifics

  • The ground_states and levels endpoints spell shared columns differently (ENSDFauthors vs ENSDF_authors, unc_md vs unc_mn). Both spellings are accepted, but a third would land in .raw with the typed field left None.

  • The levels endpoint emits unc_hl twice — once for the native unit, once for seconds. Repeats are suffixed .1 in header order rather than silently collapsed.

  • Livechart applies a RIPL offset (ripl_shift) to some floating levels. This package does not apply it; the shift is available in .raw.

  • The neutron’s radius entry is a mean-square charge radius in fm², not an rms radius in fm like every other nuclide. Passed through as given.

  • The Livechart paths are tested against captured responses, not live calls.