Support reactions where a species participates more than once (A + A) - #974
Closed
calvinp0 wants to merge 2 commits into
Closed
Support reactions where a species participates more than once (A + A)#974calvinp0 wants to merge 2 commits into
calvinp0 wants to merge 2 commits into
Conversation
calvinp0
force-pushed
the
fix_repeated_reactant_species
branch
from
August 12, 2026 17:02
ac1323f to
e5cf612
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #974 +/- ##
==========================================
+ Coverage 64.49% 64.58% +0.08%
==========================================
Files 119 119
Lines 39707 39731 +24
Branches 10289 10294 +5
==========================================
+ Hits 25610 25659 +49
+ Misses 11114 11087 -27
- Partials 2983 2985 +2
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:
|
calvinp0
force-pushed
the
fix_repeated_reactant_species
branch
from
August 22, 2026 07:58
e5cf612 to
de0105d
Compare
calvinp0
marked this pull request as ready for review
August 22, 2026 08:53
calvinp0
force-pushed
the
fix_repeated_reactant_species
branch
from
August 23, 2026 08:55
de0105d to
a999954
Compare
… + A) ARCReaction deduplicates r_species/p_species and the reactants/products label lists, keeping the multiplicity of a participant only in the reaction label (recoverable via get_species_count). Three consumers took the deduplicated lists at face value and broke for reactions such as OH + OH <=> H2O + O: * get_reactants_xyz / get_products_xyz combined one geometry per unique species, so the combined well geometry was short by the repeated species' atoms and no longer matched the atom map. Each species is now expanded by its occurrence count in the well; the count is 1 in the common case, leaving those reactions unchanged. * The AutoTST adapter built the reverse reaction from rxn.products/rxn.reactants directly, yielding the atom-imbalanced 'P1 + P2 <=> R1' instead of 'P1 + P2 <=> R1 + R1' and aborting the reverse direction of every same-species H_Abstraction. The reverse label lists are now expanded by get_species_count as well. * The project thermo library collected every converged species, and Arkane keys thermo entries by adjacency list plus multiplicity, so identical reactants raised a DatabaseError that aborted the library for the whole project. Isomorphic, same-multiplicity duplicates are now skipped.
…rding uncheckable TSs For reactions where a reactant participates more than once (e.g. OH + OH -> H2O + O), r_species is deduplicated, which broke the normal-mode-displacement check two ways. The expected atom count was sum(number_of_atoms for r_species), i.e. the deduplicated count (2 for OH + OH), so it never equalled the real TS atom count (4) and analyze_ts_normal_mode_displacement() bailed out before even looking at the mode. The forming, breaking and changed bonds it applies to the TS geometry come from ARCReaction.get_bonds(), which indexes into get_reactants_and_products(), so the count is now taken from that same call rather than from a parallel per-occurrence expansion alongside the function that defines it; the two now agree by construction instead of being able to drift. find_equivalent_atoms() only found equivalence WITHIN a molecule, so it missed the cross-molecule equivalence between identical copies (the two O's and the two H's of OH + OH). The atom map can assign the reactive atom to one copy while the located TS uses the equivalent atom of the other copy; without that equivalence the check cannot try the correct mapping. The new get_repeated_species_atom_equivalences() supplies it: for a repeated species, the atoms at the same position across copies are equivalent. Validated on the real OH + OH -> H2O + O saddle (imaginary frequency -768 cm-1) from the reproducibility runs, where the check now returns True where it returned False, recovering reaction 05 and the other same-species H_Abstractions. The count mismatch also returned False, and the scheduler calls switch_ts() whenever the verdict is False. A count mismatch means the bond indices cannot be applied to this geometry at all, which is an inability to perform the analysis rather than an observation about the mode; switching to another TS guess cannot change the atom count that made the analysis impossible, so False sent the one signal whose only consumer responds with an action that cannot help, at the cost of a TS that had passed its imaginary-frequency check and had never been examined. A genuine atom imbalance is already rejected loudly by ARCReaction.check_atom_balance(), which raises at reaction construction and in the scheduler. The mismatch now logs a warning naming both counts and returns None, the value the function already returns when no job is given and when the normal mode displacements cannot be parsed. None does not trigger switch_ts, and ts_passed_checks() still treats it as not passed, so the TS is withheld from the checks it must pass without being destroyed. Both returns were audited: the count mismatch was the only one that reported an inability to check as a negative verdict. The final `return False` after every candidate mapping has been tried is a genuine negative and is unchanged. Tests: a genuinely mismatched atom count now yields None rather than False (fails without the fix with "AssertionError: False is not None"); a check_freq_job()-level assertion that the same mismatch does not reach switch_ts (fails without the fix with the same message); and an A + A reaction whose TS survives the count gate. The atom count gate runs ahead of the atom map gate that main added meanwhile. The atom map gate reads ARCReaction.atom_map, which calls check_atom_balance(raise_error=True) and raises ReactionError precisely for a TS that does not span the reactant atoms, so the cheaper and more specific diagnosis has to be made first. Both gates return None, so the order is observable only for a reaction that trips both, where the count message is the accurate one.
calvinp0
force-pushed
the
fix_repeated_reactant_species
branch
from
August 23, 2026 12:14
a999954 to
910eb48
Compare
Member
Author
|
Superseded by #1021, which carries these commits unchanged alongside the rest of the A + A handling. The three layers — per-occurrence participants in a well, the dedup-aware normal mode displacement check, and thermo recovery when Arkane declines to write its library — are one story, and the thermo recovery in particular only reads correctly with the context of why R1 == R2 reaches Arkane at all. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Base:
main. This branch absorbs #975 ("Do not discard a TS when the normal mode displacement check cannot be run"), which was flattened into this PR and is now closed. Two commits, no file touched by more than one of them.Support for reactions in which the same species participates more than once (
A + A), across the reaction object, the combined geometries, the AutoTST adapter, the thermo library, and the TS normal mode displacement check.ARCReaction.remove_dup_species()collapsesr_species/p_speciesso each species is held once; the number of occurrences survives only in the reaction label and is recovered throughget_species_count(). Five consumers were reading the deduplicated list at face value and so saw only one copy of a repeated participant.OH + OH <=> H2O + Ois the running example.The index space
This is the single fact the whole PR turns on:
find_equivalent_atoms()and the TS geometry all live in the per-occurrence expanded space produced byget_reactants_and_products().r_specieslist does not. Anywhere the two are mixed, a repeated-reactant reaction silently disagrees with itself.What this changes
get_reactants_xyz()/get_products_xyz()expand each species by its occurrence count. The combined well geometry was short by the repeated species' atoms and no longer matched the atom map.get_species_count()is1in the common case, which leaves every other reaction untouched.The AutoTST adapter built the reverse reaction from
rxn.products/rxn.reactantsdirectly, yielding the atom-imbalancedP1 + P2 <=> R1instead ofP1 + P2 <=> R1 + R1, and aborting the reverse direction of every same-species H-abstraction. The reverse label lists are now expanded byget_species_count()as well.The project thermo library collected every converged species, and Arkane keys thermo entries by adjacency list plus multiplicity, so identical reactants raised a
DatabaseErrorthat aborted the library for the whole project. Isomorphic, same-multiplicity duplicates are now skipped.New
get_repeated_species_atom_equivalences()(arc/checks/nmd.py) builds cross-copy atom-equivalence groups.identify_equivalent_atoms_in_molecule()only finds equivalence within a molecule, so it missed the equivalence between identical copies — the two O atoms and the two H atoms ofOH + OH. The atom map can assign a reactive atom to one copy while the located TS uses the equivalent atom of the other, and without those groups the check cannot try the correct mapping.find_equivalent_atoms()now includes them for the reactant well.The NMD atom-count gate takes its expected count from
get_reactants_and_products()— the same callget_bonds()uses to build the bond indices — instead ofsum(spc.number_of_atoms for spc in reaction.r_species). ForOH + OHthe TS has 4 atoms while the deduplicated reactants reported 2, so the gate never matched and the check bailed out before looking at the mode.To be clear about what the re-derivation does and does not do: the two derivations are numerically identical today, verified across six reaction shapes —
A + B,A + A,A + A + A, a species appearing in both wells,A + A -> A + A, and unimolecular — each compared against the atom countget_bonds()actually indexes into. The gain is coupling, not behaviour: the counts now agree by construction rather than by a parallel re-derivation that can drift.A count mismatch is an inability to check, not a verdict
The count mismatch also returned
False, and the scheduler callsswitch_ts()whenever the verdict isFalse. It now logs a warning naming both counts and returnsNone— the value the function already returns when no frequency job is given and when the normal mode displacements cannot be parsed.Falseis not merely harsh, it is the wrong signal:False's sole consumer is the scheduler, which responds by trying a different TS guess.TSGuessis validated against the same expanded reactant well byARCReaction.check_atom_balance(), so all guesses for a reaction have the same atom count. The condition is invariant under the remedy — switching guesses cannot change the atom count that made the analysis impossible.Falsesent the one signal whose only consumer responds with an action that cannot help, at the cost of the whole guess list (each guess a fresh optimisation and frequency job) plus a TS that had converged, had passed its imaginary-frequency check, and had never been examined.ReactionErrorfromcheck_atom_balance(), raised at reaction construction and again from the scheduler, so a real imbalance never reaches this gate quietly.Nonedoes not triggerswitch_ts(), andts_passed_checks()still treats it as not passed, so the TS is withheld from the checks it must pass without being destroyed. IRC validation remains an independent gate.Both returns in the function were audited: the count mismatch was the only one that reported an inability to check as a negative verdict. The final
return False, after every candidate mapping has been tried, is a genuine negative and is unchanged.Gate order. The atom-count gate runs ahead of the atom-map gate
mainadded meanwhile. The atom-map gate readsARCReaction.atom_map, which callscheck_atom_balance(raise_error=True)and raisesReactionErrorprecisely for a TS that does not span the reactant atoms, so the cheaper and more specific diagnosis has to be made first. Both gates returnNone, so the order is observable only for a reaction that trips both, where the count message is the accurate one.Validation
Validated on the real
OH + OH -> H2O + Osaddle (imaginary frequency -768 cm⁻¹) from the reproducibility runs: the normal mode displacement check returnsTruewhere it previously returnedFalse, recovering benchmark reaction 05 and the other same-species H-abstractions.Tests
Nonerather thanFalse— fails without the fix withAssertionError: False is not None.check_freq_job()-level assertion that the same mismatch does not reachswitch_ts()— fails without the fix with the same message.A + Areaction whose TS survives the count gate.The scheduler-level test is deliberately built on a genuine count mismatch rather than on an
A + Areaction: once the count fix is in place anA + Areaction clears the gate (4 == 4) and exits at a laterNone, so it never reaches the branch being changed and cannot discriminate this fix.🤖 Generated with Claude Code