Skip to content

Commit 31642a7

Browse files
committed
[fix.db.rho.D8+] the version hashed the code and not the numbers the code reads, so 22 stale-geometry cubes were served under a rule that would not have drawn them
Reported by the physics session, who deleted the 22 rows by hand: the MEP box now grows until the contour closes across the whole slider range. Every cube's GEOMETRY changed with it. The method version did not move. Reads go through app.v_field_cube_servable, which keys on METHOD currency — so the cache kept serving old-geometry cubes under the new rule, and the failure is silent by construction because a stale cube is a perfectly valid cube. Nothing crashes, nothing looks wrong, and the picture is of a box today's code would not draw. WHY IT ESCAPED, and this is the generalisable part: unit_version() hashed inspect.getsource() of the listed functions. The geometry is decided by module-level CONSTANTS — PAD_MAX, PAD_STEP, FIXED_ISO, GRID_MAX_DIM, ISO_ENCLOSED_FRACTION, ISO_SLIDER_FLOOR — which those functions READ and do not CONTAIN. A version that covers source but not constants misses exactly the edit most likely to happen: tuning a number IS the common change, rewriting a function is the rare one. Constants are now part of the unit and are declared per unit, which makes SCOPE the design decision rather than an accident: classical geometry consts -> fields.mep, fields.mlp only cubegen grid + cost consts -> the four quantum units only That asymmetry is load-bearing. A classical padding tweak must NOT invalidate a converged Fe-heme SCF (minutes of CPU), or the next person stops tuning. Asserted in both directions rather than hoped for. POSITIVE CONTROLS, because a version that has never been shown to move on a constant is exactly the state this bug came from: PAD_MAX +1.0 -> {fields.mep, fields.mlp} (and nothing else) CUBE_GRID_ORB +1 -> the four fields.qm.* units (and nothing else) PAD_STEP +0.5 -> zero quantum units invalidated a constant that does not exist -> LookupError naming it, because a name that silently vanishes from the module would quietly leave the version, which is how this hole would come back 5/5 green. The tests restore the module state and assert the restore. FIRST ACT OF THE FIX IS A ONE-TIME FULL INVALIDATION: servable went 57 -> 0, because every cached cube predates today's geometry rule. That is the correct outcome — the alternative is continuing to serve them. Verified the cache works going forward in the same breath: a fresh ethanol MEP returned computed, then db, and servable climbed to 2. The 57 orphaned rows are reclaimable with bin/dirac-sweep by hand; nothing deletes them automatically. NEXT: wire app.job (still 0 rows, no writer) so a long compute is visible while it runs — backend/jobs.py is written, not yet called.
1 parent 43d21f9 commit 31642a7

2 files changed

Lines changed: 172 additions & 7 deletions

File tree

‎backend/method_registry.py‎

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
UNITS: dict[str, dict] = {
5252
'fields.mep': {
5353
'fns': ['field_mep', 'write_cube', 'prepare_mol'],
54+
# The numbers that DECIDE THE GEOMETRY of the cube. They are read by
55+
# field_mep and contained in none of the functions above, so before they
56+
# were listed here a change to the box-growing rule left the version
57+
# untouched and the servable view kept serving old-geometry cubes.
58+
'consts': ['PAD_MAX', 'PAD_STEP', 'FIXED_ISO', 'GRID_MAX_DIM',
59+
'ISO_ENCLOSED_FRACTION', 'ISO_SLIDER_FLOOR'],
5460
'exec_class': 'interactive',
5561
'in_schema': {'type': 'object', 'required': ['molfile'],
5662
'properties': {'molfile': {'type': 'string'},
@@ -70,6 +76,8 @@
7076
},
7177
'fields.mlp': {
7278
'fns': ['field_mlp', 'write_cube', 'prepare_mol'],
79+
'consts': ['PAD_MAX', 'PAD_STEP', 'FIXED_ISO', 'GRID_MAX_DIM',
80+
'ISO_ENCLOSED_FRACTION', 'ISO_SLIDER_FLOOR'],
7381
'exec_class': 'interactive',
7482
'in_schema': {'type': 'object', 'required': ['molfile'],
7583
'properties': {'molfile': {'type': 'string'}}},
@@ -109,6 +117,12 @@
109117
def _qm_unit(kind: str, quantity: str, units: str) -> dict:
110118
return {
111119
'fns': _QM_FNS,
120+
# A quantum cube's grid comes from pyscf's cubegen at these fixed
121+
# resolutions, and the cost model decides whether it runs at all. Both
122+
# are numbers, both change the output or the refusal, and neither lives
123+
# inside a listed function.
124+
'consts': ['CUBE_GRID_MEP', 'CUBE_GRID_ORB', 'CUBE_MEP_FIXED',
125+
'CUBE_MEP_MARGINAL', 'CUBE_ORB_FIXED', 'CUBE_ORB_MARGINAL'],
112126
'exec_class': 'interactive',
113127
'in_schema': {'type': 'object', 'required': ['molfile'],
114128
'properties': {'molfile': {'type': 'string'},
@@ -133,12 +147,29 @@ def _qm_unit(kind: str, quantity: str, units: str) -> dict:
133147

134148
# ── version = hash of the compute unit, machine-derived ─────────────────────
135149

136-
def unit_version(module, fns: Iterable[str]) -> tuple[str, bytes]:
137-
"""(short hex version, full sha256) over the SOURCE of the named functions.
138-
139-
Sorted, so declaration order in UNITS cannot change a version. Source only:
140-
a docstring edit does change it, which is the conservative direction — the
141-
dangerous direction is a behaviour change that does NOT change it.
150+
def unit_version(module, fns: Iterable[str],
151+
consts: Iterable[str] = ()) -> tuple[str, bytes]:
152+
"""(short hex version, full sha256) over the SOURCE of the named functions
153+
AND THE VALUES of the named module-level constants.
154+
155+
Sorted, so declaration order in UNITS cannot change a version. Source only
156+
for functions: a docstring edit does change the version, which is the
157+
conservative direction — the dangerous direction is a behaviour change that
158+
does NOT change it.
159+
160+
⚠ CONSTANTS WERE THE HOLE, and it cost 22 cached rows that had to be deleted
161+
by hand. The geometry of an MEP cube is decided by module-level numbers —
162+
PAD_MAX, PAD_STEP, FIXED_ISO, GRID_MAX_DIM — that the listed functions READ
163+
but do not CONTAIN. So the box-growing rule changed, every cube's geometry
164+
changed with it, and the method version did not move: reads go through
165+
app.v_field_cube_servable, which keys on method currency, and it happily
166+
served cubes with the OLD geometry under the NEW rule. The failure is silent
167+
by construction, because a stale cube is a perfectly valid cube.
168+
169+
Hashing source-but-not-constants means the version misses exactly the edit
170+
most likely to happen. Tuning a number IS the common change; rewriting a
171+
function is the rare one. `repr` is used deliberately: 12.0 and 12 are
172+
different values here and must produce different versions.
142173
"""
143174
h = hashlib.sha256()
144175
for name in sorted(fns):
@@ -149,6 +180,14 @@ def unit_version(module, fns: Iterable[str]) -> tuple[str, bytes]:
149180
f'{module.__name__} — the UNITS table has drifted from the code')
150181
h.update(name.encode())
151182
h.update(inspect.getsource(fn).encode())
183+
for name in sorted(consts):
184+
if not hasattr(module, name):
185+
raise LookupError(
186+
f'compute unit names the constant {name!r}, which does not exist '
187+
f'in {module.__name__} — a constant that vanished silently stops '
188+
f'being part of the version, which is how this hole was made')
189+
h.update(name.encode())
190+
h.update(repr(getattr(module, name)).encode())
152191
digest = h.digest()
153192
return digest.hex()[:12], digest
154193

@@ -157,7 +196,8 @@ def plan(module) -> list[dict]:
157196
"""What register_all would write. Pure; no DB. This is the smoke test."""
158197
out = []
159198
for method_id, spec in sorted(UNITS.items()):
160-
version, digest = unit_version(module, spec['fns'])
199+
version, digest = unit_version(module, spec['fns'],
200+
spec.get('consts', ()))
161201
out.append({'method_id': method_id, 'version': version, 'sha256': digest,
162202
'exec_class': spec['exec_class'], 'fns': sorted(spec['fns']),
163203
'in_schema': spec['in_schema'], 'out_schema': spec['out_schema'],
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
"""A method version that hashes code but not the numbers the code READS is a
3+
version that misses the edit most likely to happen.
4+
5+
THE INCIDENT, 2026-08-11: the MEP box-growing rule changed (the box now grows
6+
until the contour closes across the whole slider range). Every cube's GEOMETRY
7+
changed with it. The method version did not move, because the geometry is decided
8+
by module-level constants — PAD_MAX, PAD_STEP, FIXED_ISO, GRID_MAX_DIM — which
9+
the hashed functions read and do not contain. Reads go through
10+
app.v_field_cube_servable, which keys on METHOD currency, so it kept serving
11+
old-geometry cubes under the new rule and 22 rows had to be deleted by hand.
12+
13+
The failure is silent by construction: a stale cube is a perfectly valid cube.
14+
Nothing crashes, nothing looks wrong, and the picture is of a box that today's
15+
code would not have drawn.
16+
17+
Tuning a number IS the common change; rewriting a function is the rare one. So
18+
these tests assert the version moves for BOTH, and — the part that matters —
19+
that it moves for the right SCOPE: a classical geometry constant must not
20+
invalidate the quantum units, or every tuning pass throws away real SCF work.
21+
22+
Run: backend/env/bin/python backend/tests/test_method_version_covers_constants.py
23+
"""
24+
from __future__ import annotations
25+
26+
import os
27+
import sys
28+
29+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
30+
31+
import field_server as fs # noqa: E402
32+
import method_registry as mr # noqa: E402
33+
34+
PASS, FAIL = [], []
35+
36+
37+
def check(name, fn):
38+
try:
39+
fn()
40+
PASS.append(name)
41+
print(f'PASS {name}')
42+
except AssertionError as e:
43+
FAIL.append(name)
44+
print(f'FAIL {name}\n {e}')
45+
46+
47+
def versions() -> dict:
48+
return {u['method_id']: u['version'] for u in mr.plan(fs)}
49+
50+
51+
def moved_when(attr: str, delta):
52+
"""Which method versions change when a module constant changes."""
53+
before = versions()
54+
original = getattr(fs, attr)
55+
setattr(fs, attr, delta(original))
56+
try:
57+
after = versions()
58+
finally:
59+
setattr(fs, attr, original)
60+
assert versions() == before, (
61+
f'restoring {attr} did not restore the versions — this test mutates '
62+
f'module state and must leave it exactly as found')
63+
return {k for k in before if before[k] != after[k]}
64+
65+
66+
def test_a_geometry_constant_moves_the_classical_units():
67+
moved = moved_when('PAD_MAX', lambda v: v + 1.0)
68+
assert moved == {'fields.mep', 'fields.mlp'}, (
69+
f'PAD_MAX moved {sorted(moved)}; expected exactly the two classical grid '
70+
f'units. This is the exact hole that served 22 stale-geometry cubes: if '
71+
f'this set is EMPTY the version does not cover constants at all.')
72+
73+
74+
def test_a_grid_constant_moves_the_quantum_units_and_only_those():
75+
moved = moved_when('CUBE_GRID_ORB', lambda v: v + 1)
76+
assert moved == {'fields.qm.homo', 'fields.qm.lumo', 'fields.qm.density',
77+
'fields.qm.mep_qm'}, (
78+
f'CUBE_GRID_ORB moved {sorted(moved)}; expected exactly the four quantum '
79+
f'units. SCOPE is the point: a classical tuning pass that invalidated '
80+
f'converged SCF results would make everyone stop tuning.')
81+
82+
83+
def test_a_classical_constant_does_not_invalidate_quantum_work():
84+
moved = moved_when('PAD_STEP', lambda v: v + 0.5)
85+
quantum = {m for m in moved if m.startswith('fields.qm.')}
86+
assert not quantum, (
87+
f'a classical padding step invalidated {sorted(quantum)} — an Fe-heme SCF '
88+
f'is minutes of CPU and must not be thrown away by a change that cannot '
89+
f'affect it')
90+
91+
92+
def test_the_registry_refuses_a_constant_that_does_not_exist():
93+
"""A named constant that vanishes must RAISE, not silently stop counting.
94+
95+
Silent removal is how the original hole would come back: the name disappears
96+
from the module, the hash quietly covers one thing less, and nothing says so.
97+
"""
98+
try:
99+
mr.unit_version(fs, ['field_mep'], ['NO_SUCH_CONSTANT_XYZ'])
100+
except LookupError as e:
101+
assert 'NO_SUCH_CONSTANT_XYZ' in str(e), (
102+
f'the refusal does not name the missing constant: {e}')
103+
return
104+
raise AssertionError(
105+
'unit_version accepted a constant that does not exist — a name that '
106+
'vanishes would silently leave the version, which is the failure mode '
107+
'this whole file is about')
108+
109+
110+
def test_every_declared_constant_exists_today():
111+
"""The declarations must be true right now, not aspirational."""
112+
for method_id, spec in mr.UNITS.items():
113+
for const in spec.get('consts', ()):
114+
assert hasattr(fs, const), (
115+
f'{method_id} declares the constant {const!r}, which is not in '
116+
f'field_server today')
117+
118+
119+
for name, fn in list(globals().items()):
120+
if name.startswith('test_'):
121+
check(name, fn)
122+
123+
print('─' * 100)
124+
print(f'{len(PASS)} passed · {len(FAIL)} failed')
125+
sys.exit(1 if FAIL else 0)

0 commit comments

Comments
 (0)