Skip to content

PES loop: an unmeasurable E0 sensitivity is not a measured zero, and a blind screen is not convergence - #208

Merged
alongd merged 1 commit into
mainfrom
i031-unmeasurable-not-zero
Aug 26, 2026
Merged

alongd merged 1 commit into
mainfrom
i031-unmeasurable-not-zero

Conversation

@alongd

@alongd alongd commented Aug 26, 2026

Copy link
Copy Markdown
Member

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 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: both [H] + O=C=O entrance 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 both main and this branch):

  1. Classify the unmeasurable case at its source (t3/pdep/parser.py, t3/pdep/pes_rounds.py). The parser now records which transitionState(...) blocks declare statmech (a non-empty modes list, or the by-reference (label, path) form), from the AST, never by executing the file. e0_sensitivity_is_measurable is 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-18 in one round, exactly 0.0 in the next, on the same channels) from a genuinely small response. Every SkippedChannel now carries a machine-readable classification (no_transition_state / already_computed / barrierless / no_evidence / below_floor / unmeasurable) plus numeric coefficient and delta_ln_k fields — the reported structural zero is carried verbatim, never clamped or substituted; the classification is what says it is not a measurement.

  2. Persist the round record (t3/pdep/pes_loop.py). Every RoundRecord is written, at the moment it is made, to its round's own directory as round_record.yml (yaml.safe_dump over JSON-safe types; best-effort with a loud warning, exactly like the diagram). The termination message's pointer now resolves to a real artifact.

  3. 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

  • No replacement ranking criterion (barrier height, uncertainty, energy windows, seeded provisional statmech — all under an open decision, I-030).
  • No change to which transition states get queued, ever.
  • No clamping/flooring/hiding of structural zeros.
  • No control-flow change past the current termination point; min_delta_ln_k and all energy/rate parameters untouched.

Verification

  • Replayed run_pes_loop's real termination path over r002 round 1's network0_reduced.py + sa_coefficients.yml (only the explore/SA seams stubbed to return the real artifacts): status unmeasurable, round_record.yml written, TS2/TS3 (the [H] + O=C=O entrance channels, mapped by reaction) classified unmeasurable, and every record number equals the raw yml's max-|coefficient| row (with delta_ln_k = |coeff| × 8368).
  • Mutation, both ways: adding modes=[HarmonicOscillator(...)] to TS2's block in a scratch copy flips its classification to below_floor (and flips rmgpy can_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 (baseline official/main, tests/test_pdep: 1931 passed, 0 failed). 20 new tests across test_parser.py, test_pes_rounds.py, test_pes_loop.py.

@alongd
alongd force-pushed the i031-unmeasurable-not-zero branch from 38ef8e5 to 02e785f Compare August 26, 2026 18:51
@github-actions
github-actions Bot requested a lite review from Copilot August 26, 2026 19:54
@codecov-commenter

codecov-commenter commented Aug 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.05%. Comparing base (e60a3b7) to head (e63a181).

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     
Flag Coverage Δ
unittests 82.05% <ø> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RoundRecord immediately to round_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.

Comment thread t3/pdep/pes_loop.py
Comment on lines +721 to +725
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}')
@alongd
alongd force-pushed the i031-unmeasurable-not-zero branch from 02e785f to e63a181 Compare August 26, 2026 20:01
@alongd
alongd merged commit f7d32b3 into main Aug 26, 2026
4 checks passed
@alongd
alongd deleted the i031-unmeasurable-not-zero branch August 26, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants