PES loop: an unmeasurable E0 sensitivity is not a measured zero, and a blind screen is not convergence - #208
Conversation
38ef8e5 to
02e785f
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #208 +/- ##
==========================================
+ Coverage 82.02% 82.05% +0.03%
==========================================
Files 75 75
Lines 12081 12146 +65
Branches 2595 2605 +10
==========================================
+ Hits 9909 9966 +57
- Misses 1556 1560 +4
- Partials 616 620 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…st round records, and stop claiming convergence for a blind screen A transition state with no statmech modes on a bimolecular channel cannot respond to an E0 perturbation at all: can_tst() is False (it is literally len(conformer.modes) > 0), every k(E) comes from the inverse Laplace transform, and there the TS E0 enters only as a threshold gate that never binds for an association -- perturbing it is an exact no-op, verified by control experiment on the r002 CHO2 run (I-030). The loop nevertheless reported those channels as having "measured a ln(k) response below the min_delta_ln_k floor" and called the stop 'converged', while pointing the reader at a round record that was never written. Three changes, none of which moves a candidate between the queue and the skips (a run makes byte-identical QM decisions before and after): * t3/pdep/parser.py: PDepNetwork.ts_labels_with_statmech -- which transitionState(...) blocks declare statmech (a non-empty modes list, or the by-reference (label, path) form), detected from the AST at parse. * t3/pdep/pes_rounds.py: e0_sensitivity_is_measurable (structural test: statmech-bearing, or unimolecular where the ILT gate can bind -- round 0's isomerization saddle measured a real -12% without modes, so "no modes" alone would misclassify it), a closed SKIP_* classification vocabulary, and numeric ts_label/classification/coefficient/delta_ln_k fields on every SkippedChannel: the reported structural zero is carried verbatim, never clamped -- the classification is what says it is not a measurement. * t3/pdep/pes_loop.py: every RoundRecord is persisted as it is made to its round's round_record.yml (yaml.safe_dump, best-effort like the diagram), so the termination message's pointer resolves to a real artifact; the evidence-screen stop names which criterion fired, with per-class counts and a note when the round budget was exhausted by the same round; and a stop with unmeasurable candidates remaining terminates at the same point but reports the new status 'unmeasurable' -- "converged" was not demonstrated for a screen that was blind to those channels. What to DO about them (barrier-height ranking, seeded statmech, ...) is an open decision this change deliberately reports rather than takes.
There was a problem hiding this comment.
Pull request overview
This PR hardens the standalone PES exploration loop’s handling of master-equation E0 sensitivity screening by distinguishing structurally unmeasurable sensitivities from genuinely small measured responses, persisting round records as durable artifacts, and making termination reasons explicit about which criterion fired.
Changes:
- Add structural measurability classification for dln(k)/dE0 (statmech-declaring TS or unimolecular↔unimolecular) and propagate it through candidate splitting + evidence attachment.
- Persist every
RoundRecordimmediately toround_record.yml(YAML-safe, JSON-safe types) in each round directory. - Introduce a new terminal status (
unmeasurable) and improve termination messaging with per-class counts and explicit stop criteria.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
t3/pdep/parser.py |
Parse-time AST detection of statmech-bearing transitionState(...) blocks; store ts_labels_with_statmech on PDepNetwork. |
t3/pdep/pes_rounds.py |
Add measurability predicate + machine-readable skip classifications and numeric fields on skips; stamp measurability on candidates. |
t3/pdep/pes_loop.py |
Persist RoundRecord YAML per round; add unmeasurable termination status and richer termination reasons keyed to skip classifications. |
tests/test_pdep/test_parser.py |
Tests for AST-based statmech declaration detection across DSL spellings. |
tests/test_pdep/test_pes_rounds.py |
Tests for measurability classification and skip classification/numeric field behavior. |
tests/test_pdep/test_pes_loop.py |
End-to-end loop tests for unmeasurable termination semantics and record persistence behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| reason = (f'round {round_index}: every remaining QM candidate of network ' | ||
| f'{network.network_id!r} fell to the evidence screen: ' | ||
| f'{"; ".join(clauses)}. Criterion: the min_delta_ln_k floor. ' | ||
| f'Per-candidate measured values: ' | ||
| f'{round_record_path(paths)}.{budget_note}') |
02e785f to
e63a181
Compare
What
The standalone PES exploration loop ranks QM candidates by a master-equation
dln(k)/dE0. For a transition state with no statmech modes on a bimolecular channel that sensitivity is structurally uncomputable, not small:can_tst()is False, every k(E) comes from the inverse Laplace transform, and there the TSE0enters only as a threshold gate that never binds for an association — perturbing it is an exact no-op (verified by control experiment on the r002 CHO2 run: both[H] + O=C=Oentrance channels left every k(T,P) bit-identical under E0 perturbations of 8.368, 20 and 100 kJ/mol). The loop nevertheless reported those channels as having "measured a ln(k) response below the min_delta_ln_k floor", called the stop'converged', and pointed the reader at a round record it never wrote.Three changes — none of which moves a candidate between the queue and the skips; a run makes identical QM decisions before and after (replayed on the real r002 artifacts: round 0 queues exactly
TS2, the isomerization saddle, on bothmainand this branch):Classify the unmeasurable case at its source (
t3/pdep/parser.py,t3/pdep/pes_rounds.py). The parser now records whichtransitionState(...)blocks declare statmech (a non-emptymodeslist, or the by-reference(label, path)form), from the AST, never by executing the file.e0_sensitivity_is_measurableis the structural test: measurable iff the TS declares statmech (RRKM engages) or the channel is unimolecular on both sides (the ILT gate can bind — r002 round 0's isomerization saddle measured a real −12% ln(k) response without modes and was rightly queued, so "no modes" alone would misclassify it). The structural test was chosen over inspecting the SA value because no epsilon separates a structural zero that survived floating point (~1.66e-18in one round, exactly0.0in the next, on the same channels) from a genuinely small response. EverySkippedChannelnow carries a machine-readableclassification(no_transition_state/already_computed/barrierless/no_evidence/below_floor/unmeasurable) plus numericcoefficientanddelta_ln_kfields — the reported structural zero is carried verbatim, never clamped or substituted; the classification is what says it is not a measurement.Persist the round record (
t3/pdep/pes_loop.py). EveryRoundRecordis written, at the moment it is made, to its round's own directory asround_record.yml(yaml.safe_dumpover JSON-safe types; best-effort with a loud warning, exactly like the diagram). The termination message's pointer now resolves to a real artifact.The termination names which criterion fired. The evidence-screen stop reports per-class counts ("1 (TS1) measured a ln(k) response below the min_delta_ln_k floor (1.000e-03); 2 (TS2, TS3) are structurally UNMEASURABLE …"), notes when the round budget was exhausted by that same round (previously "converged" and "ran out of rounds" were indistinguishable from outside), and — when unmeasurable candidates remain — terminates at the same point with the same queueing but the new status
'unmeasurable'instead of'converged'/'no_candidates', because convergence was not demonstrated for a screen that was structurally blind to those channels.PES.py's exit code is unchanged (only'failed'is nonzero).What this deliberately does NOT do
min_delta_ln_kand all energy/rate parameters untouched.Verification
run_pes_loop's real termination path over r002 round 1'snetwork0_reduced.py+sa_coefficients.yml(only the explore/SA seams stubbed to return the real artifacts): statusunmeasurable,round_record.ymlwritten, TS2/TS3 (the[H] + O=C=Oentrance channels, mapped by reaction) classifiedunmeasurable, and every record number equals the raw yml's max-|coefficient| row (withdelta_ln_k = |coeff| × 8368).modes=[HarmonicOscillator(...)]to TS2's block in a scratch copy flips its classification tobelow_floor(and flips rmgpycan_tst()to True); reverting flips it back. Queued set[]in both halves.python -m pytest tests/test_pdep tests/test_pes_cli.py -q: 1975 passed, 0 failed (baselineofficial/main,tests/test_pdep: 1931 passed, 0 failed). 20 new tests acrosstest_parser.py,test_pes_rounds.py,test_pes_loop.py.