Testing

pip install -e ".[dev]"
pytest

No network needed: the Livechart transport is injected and the flat-file tests run against embedded fixed-column fixtures.

Validation against an independent parser

The values in tests/data/ensdf180_oracle.json were produced by nudel, an independently written ENSDF parser. nudel is not a dependency — not required, not optional. Its output was captured once and committed, so the independence travels with the repository and pytest alone runs the comparison.

This is deliberately not a snapshot of our own output. A self-snapshot detects change; if the parser were wrong when it was taken, the file would preserve the bug and the test would defend it. These numbers came from a codebase that does not share our assumptions, so a disagreement means one of the two is actually wrong.

file

whose values

what a failure means

ensdf180_oracle.json

nudel’s

one of the two parsers is wrong

ensdf180_characterization.json

ours

our output changed; correctness unaddressed

The oracle holds per-level fields for a committed excerpt plus a per-dataset digest over the full A=180 chain — 59 datasets, 3179 levels — in ~11 KB. Set ENSDF_PATH to a directory holding ensdf.180 and the chain digests are checked too; otherwise that one test skips.

To refresh it — the only time nudel is ever installed:

pip install git+https://github.com/op3/nudel
python tools/generate_golden.py ~/ensdf/ensdf.180

The file records nudel’s version, the chain file’s SHA-256 and the generation date, so a future disagreement is attributable.

Bugs this has caught

Neither was reachable by a self-snapshot.

  • Spin-parity inheritance. ENSDF attaches a parity only where it is written; this parser used to infer one across commas, so 1,2+ read as 1+,2+.

  • Suffix energy offsets. Manual V.18 allows NUM+A as well as A+NUM, and 4172.3+X was parsing as no energy at all — 498 levels in the A=180 chain alone.

Where nudel is wrong

nudel is the oracle, not an authority. Where the two disagree the manual decides, and in four J-field patterns it decides against nudel. Levels matching these are held out of the oracle rather than frozen — enshrining a value we believe to be wrong would be worse than having none — and our behaviour is pinned instead by spec-derived tests in test_nook.py. The patterns live in tests/_fingerprint.py:DIVERGENT_J.

J field

manual V.20

nudel

this package

3+ TO 6-

case (b): 3+, 4±, 5±, 6-

3-,4-,5-,6-

3+, 4±, 5±, 6-

3+ TO 6

case (c): 3+, 4±, 5±,

3+,4+,5+,6+

3+, 4±, 5±,

LE 5+

“π=+ and J≤5”

no assignment

5+, constraint="LE"

GE 4

as above

no assignment

4, constraint="GE"

In both range cases nudel propagates one endpoint’s parity across the whole range, where the manual leaves the interior undetermined. For the operator forms it yields nothing.

Two further cases are losses rather than disagreements: nudel discards NOT 3- and NATURAL/UNNATURAL entirely, where this package records them in SpinParity.excluded and SpinParity.natural_parity. Discarding is safe, so these stay in the oracle — the fields simply have no counterpart.

Physics invariants

Some code has no oracle available. nudel does not parse bands at all, so column 77, the BAND() documentation records and the ASCII markup rendering have nothing external to check them against — and a self-snapshot would only freeze whatever they currently produce.

Nuclear structure supplies one. Two quasiparticles couple to K = |Ω_p − Ω_n| or Ω_p + Ω_n — the Gallagher–Moszkowski doublet — with parity the product of the two orbital parities, which for a Nilsson label [N n_z Λ] is (−1)^N. Those three pieces of the parser are decoded independently, so if any one were misread the bandheads would stop satisfying the relations.

All four doublets in 180Ta hold, on both K and parity:

configuration

bands

K^π

|Ω_p−Ω_n|

Ω_p+Ω_n

π_p·π_n

π7/2[404] ν9/2[624]

A, F

1+, 8+

1

8

+

π9/2[514] ν9/2[624]

K, Q

0−, 9−

0

9

π7/2[404] ν5/2[512]

L, J

1−, 6−

1

6

π7/2[404] ν1/2[510]

f, h

3−, 4−

3

4

test_band_assignments_obey_the_coupling_rules asserts this against the committed excerpt, and refuses to pass if fewer than four doublets are found — otherwise a parsing failure that produced no bands at all would look like success. A companion test feeds it a deliberately wrong bandhead to confirm the invariant is sensitive.

A second, weaker check: fitting E(J) = E₀ + A[J(J+1) − K(K+1)] over the lowest members gives A = 10.3–11.1 keV across bands A, K, F and Q. The same deformed core should give the same A; a few percent of spread is Coriolis mixing, not a parsing error. This one is left as a note rather than a test, since the tolerance would have to be loose enough to be uninformative.

A third kind of oracle: the data checking itself

The mass-surface closure in limitations is neither a frozen oracle nor a spec-derived test — it is an identity the data must satisfy internally, so it validates numbers that no external reference is available for. Its tests build a synthetic survey from chosen mass excesses, so closure holds by construction and any failure comes from the code rather than from the numbers.

Reconciliations

Two representation differences are normalised in tests/_fingerprint.py rather than in each test:

  • nudel spells “no value” as float('nan') where this package uses None. (Normalising also makes the JSON valid — NaN is not legal JSON.)

  • nudel uses the tropical year (31 556 926 s), this package the Julian year (31 557 600 s). That is 21 ppm, far below any quoted half-life precision, so half-lives are kept out of the digest and compared with a tolerance.