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 |
|---|---|---|
|
nudel’s |
one of the two parsers is wrong |
|
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 as1+,2+.Suffix energy offsets. Manual V.18 allows
NUM+Aas well asA+NUM, and4172.3+Xwas 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.
|
manual V.20 |
nudel |
this package |
|---|---|---|---|
|
case (b): |
|
|
|
case (c): |
|
|
|
“π=+ and J≤5” |
no assignment |
|
|
as above |
no assignment |
|
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 usesNone. (Normalising also makes the JSON valid —NaNis 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.