Skip to content

CREST-based TS conformer search adapter - #921

Open
calvinp0 wants to merge 4 commits into
mainfrom
crest_adapter_clean
Open

CREST-based TS conformer search adapter#921
calvinp0 wants to merge 4 commits into
mainfrom
crest_adapter_clean

Conversation

@calvinp0

@calvinp0 calvinp0 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Adds CREST as a TS-search adapter. CREST does not search for a saddle itself — it takes the TS
seeds ARC already generates heuristically and runs constrained GFN2-xTB metadynamics around them,
so the reactive core is pinned while the spectator degrees of freedom are conformationally sampled.
The result is a better-converged starting geometry for the subsequent DFT TS optimisation.

Commits

commit scope
CREST: settings, constants, and installer binary discovery, env activation, make install-crest, angstrom_to_bohr
CREST: TS-search adapter (H-abstraction) + seed hub the adapter, the neutral seed/constraint hub, hydrolysis constraints, shared-code changes
CREST: XY-addition four-center seed builder four-centre concerted XY-addition seed geometry
CREST: document the TS-search adapter docs/source/TS_search.rst

Every file is touched by exactly one commit.

Family support and gating

CREST is opt-in. An adapter runs only if it is both registered for the family in
ts_adapters_by_rmg_family and present in the global ts_adapters list; crest is deliberately
absent from the default list, alongside goflow and rits, because it needs its own environment
and an external binary. Enable it per run:

ts_adapters:
  - heuristics
  - crest

Constraints are implemented for:

  • H_Abstraction
  • carbonyl_based_hydrolysis, ether_hydrolysis, nitrile_hydrolysis — ARC-native families, always
    classified, so these are what make CREST reachable in a normal run
  • XY_Addition_MultipleBond — registered for crest only, so the four-centre seed builder is
    reachable through CREST and through no default adapter. This family's group definition also
    requires a halogen at *4 ([F1s,Cl1s,Br1s]), so RMG files it under the halogens family set
    rather than default; in a default run ARCReaction.family is None for these reactions and the
    seed builder is not reached at all. It needs rmg_family_set: 'halogens' or 'all'.

Chemistry decisions

H-abstraction reactive core is element-aware. ARC applies r1_stretch = r2_stretch = 1.2 to
the reference single-bond length, which makes the core element-dependent. For a carbon, nitrogen or
oxygen partner this PR uses absolute targets instead — BREAKING_BOND_LENGTH = 1.29 and
FORMING_BOND_LENGTH = 1.36 Å, the medians over 1670 DFT-optimised H-abstraction transition states,
a set whose donor and acceptor are heavy atoms in all but 62 records. Every other partner element
keeps the multiplicative path, because those medians describe heavy-partner abstraction and are
wrong elsewhere: for an H-atom partner 1.36 Å sits about 0.46 Å out along the reaction coordinate
from a saddle near 0.90 Å, and for S, Si, P, Br and I the absolute target is shorter than the
equilibrium bond. The two bonds are decided independently, so one seed can take the absolute target
on the side facing a carbon and the multiplicative stretch on the side facing a bromine.

ABSOLUTE_BOND_LENGTH_PARTNERS = ('C', 'N', 'O') and DEFAULT_BOND_STRETCH = 1.2 name the rule.
Passing r1_stretch / r2_stretch explicitly still multiplies, for every element.

Breaking / forming bond, in Å:

system partners main this PR
CH₄ + OH C, O 1.308 / 1.152 1.29 / 1.36
CH₄ + H C, H 1.308 / 0.889 1.29 / 0.889
CH₄ + HS C, S 1.308 / 1.608 1.29 / 1.608
HBr + CH₃ Br, C 1.692 / 1.308 1.692 / 1.36

Behaviour is unchanged from main for every partner outside C/N/O. For C/N/O the forming bond is
0.05–0.21 Å longer than main produces; a GFN2-xTB Hessian on the CH₄ + OH seed moves from
−462.8 cm⁻¹ to −775.1 cm⁻¹, one imaginary mode in both cases. This is a change to the default
heuristics adapter and so affects runs that never enable CREST.

Hydrolysis uses all six pairwise distances among the electrophilic centre, leaving group, water
oxygen and transferring water hydrogen. Four atoms have 6 internal degrees of freedom and there are
C(4,2) = 6 pairwise distances, so this is exactly determining; a smaller set leaves the reactive core
free to relax away from the saddle during sampling.

Changes outside the CREST paths

These affect runs that never use CREST:

  • arc/constants.py — removed a duplicate bohr_to_angstrom = 0.529177 that shadowed a0 * 1e10;
    added angstrom_to_bohr as its reciprocal.
  • arc/scheduler.pysuccessful_methods now credits every entry in a guess's method_sources,
    deduplicated case-insensitively, so a geometry found by several adapters credits all of them.
  • arc/species/converter.py — new reorder_xyz_string; empty input now raises ConverterError
    rather than IndexError, and blank interior lines are skipped per the documented contract.
  • arc/job/adapters/ts/heuristics.py — the element-aware reactive-core rule above.

Testing

106 new tests against origin/main:

file before after
arc/job/adapters/ts/crest_test.py 44
arc/job/adapters/ts/xy_addition_test.py 14
arc/settings/crest_test.py 14
arc/job/adapters/ts/heuristics_test.py 44 69
arc/species/converter_test.py 77 84
arc/scheduler_test.py 60 62

Verified: 884 passed, 32 skipped, 0 failed, 90 subtests across arc/job/adapters/ts/ and
arc/settings/crest_test.py.

The suite was validated by mutation testing rather than by coverage percentage — each new test was
checked to fail under a plausible corruption of the code it covers (flipped comparisons, swapped
atom indices, ±10% on physical constants, disabled guards, removed unit conversions). Notable
results: the coords.ref Å→Bohr conversion and the TURBOMOLE column order are now pinned (both were
previously mutable with the suite green), as are --chrg/--uhf, the *3/*4 orientation the
asymmetric XY factors depend on, and the properness of the antiparallel rotation branch (returning
−I there is a mirror image, det = −1, i.e. a chirality inversion).

Upgrade note

arc/constants.pxd gains angstrom_to_bohr. A stale compiled arc.constants shadows the source and
lacks the symbol, so re-run make compile after pulling — otherwise the first CREST job fails
with an AttributeError at runtime rather than at import.

Known limitations

  • XY_Addition_MultipleBond is only classified with rmg_family_set: 'halogens' or 'all', per above.
  • The D–H–A angle target (174°) is not applied; the seed is built linear.
  • Each seed becomes its own CREST job; a heuristics sweep can produce dozens, so submission is capped
    at MAX_CREST_SEEDS most-distinct seeds, with the number dropped logged. Distinctness is measured
    on interatomic distance matrices, so a rigidly translated or rotated duplicate is recognised as one.
  • The adapter waits on its jobs synchronously, so a reaction's TS-guess stage lasts as long as its
    CREST jobs do. This is how every TS adapter behaves — heuristics, autotst, gcn, kinbot,
    xtb_gsm and orca_neb all run to completion inside execute_incore — and it follows from the
    stage's contract, since the scheduler asks for guesses and needs them back before it proceeds.
    CREST differs from the others in submitting its compute to the queue rather than running it in the
    driver's own allocation, and in bounding it: a job is given up on after
    MAX_CREST_WALL_TIME_SECONDS of running or MAX_CREST_QUEUE_WAIT_SECONDS without starting.
  • Acceptance after sampling checks the restrained distances only, at 0.15 Å. No angle restraint is
    written and no angle is checked, because restraining A–H, H–B and A–B fixes all three sides of the
    reactive triangle and so determines the angle. Measured on three H-abstraction cases run on a
    cluster (CH₄ + OH, C₂H₆ + HO₂, CH₃OH + CH₃; 21 geometries across crest_best and the ensembles),
    the restrained distances drift at most 0.006 Å from the seed and the A–H–B angle moves at most
    9.2°, usually under 3°. The 0.15 Å tolerance is therefore about 25× the observed drift.
  • submit_job() in arc/job/local.py signals failure as either (None, None) or ('errored', '').
    The adapter handles both.

Copilot AI review requested due to automatic review settings July 23, 2026 11:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/heuristics_test.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/species/converter.py Fixed
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch from 45a03ee to 39d7c01 Compare July 23, 2026 17:55
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.09%. Comparing base (1f1ccb4) to head (8c18690).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #921      +/-   ##
==========================================
+ Coverage   65.67%   66.09%   +0.41%     
==========================================
  Files         121      125       +4     
  Lines       41015    41886     +871     
  Branches    10548    10738     +190     
==========================================
+ Hits        26938    27685     +747     
- Misses      11064    11141      +77     
- Partials     3013     3060      +47     
Flag Coverage Δ
functionaltests 66.09% <ø> (+0.41%) ⬆️
unittests 66.09% <ø> (+0.41%) ⬆️

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 4 times, most recently from e6a47d8 to 174199b Compare August 14, 2026 08:50
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/species/converter_test.py Fixed
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 2 times, most recently from d2b0575 to f4ae037 Compare August 14, 2026 12:13
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 2 times, most recently from b31f635 to b2e2076 Compare August 14, 2026 17:38
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch from b2e2076 to 89ba170 Compare August 14, 2026 18:42
from arc.species.species import ARCSpecies, TSGuess, SpeciesError, colliding_atoms
from arc.species.zmat import get_parameter_from_atom_indices, remove_zmat_atom_0, up_param, xyz_to_zmat
from arc.species.vectors import calculate_angle
from arc.job.adapters.ts.seed_hub import get_ts_seeds
Comment thread arc/job/adapters/ts/seed_hub.py Fixed
@calvinp0 calvinp0 changed the title CREST CREST-based TS conformer search adapter Aug 14, 2026
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 2 times, most recently from 31231d0 to d70196a Compare August 14, 2026 19:14
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 3 times, most recently from c988137 to bba78a3 Compare August 22, 2026 06:40
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 2 times, most recently from 23beab3 to 03321d8 Compare August 23, 2026 12:13

@LeenFahoum LeenFahoum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good overall! I left a few comments.

CREST_REACTIVE_CORE_SIZES = {'H_Abstraction': 3,
'carbonyl_based_hydrolysis': 4,
'ether_hydrolysis': 4,
'nitrile_hydrolysis': 4,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we also add XY_Addition_MultipleBond: 4 here? The CREST constraints for this family use all four *1-*4 atoms, so it looks like its reactive core size should also be 4. If so, the [2, 2] case in test_tiny_system_gate_skips_crest_only_for_whole_molecule_core should also expect True

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@LeenFahoum Yup! it is my intention but right now #978 needs to go in before this PR if we want to have XY_Addition_MultipleBond because right now ARC loads RMG-DB as default so it does not load in that family. The #978 PR is to change default to all and then I can adjust this PR to include it too

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have added XY_Addition_MultipleBond: 4 and flipped [2,2] case to assertTrue. Also added [3, 2] (5 atoms, one spectator, gated) and [6,2] (C2H4 + HCl at 8 atoms, not gated) so the boundary is pinned from both sides.

Also, with #978, t has been merged but the RMG-DB load is still default and not all so XY_Addition_MultipleBond won't be identified unless the user changes in their settings to load all

Comment thread arc/job/adapters/ts/crest.py Outdated
if os.path.isfile(stale_best_path):
try:
os.remove(stale_best_path)
except OSError as e:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If removing the previous crest_best.xyz fails, could we return None here instead of continuing?
Otherwise process_completed_jobs() may later pick up the old geometry and treat it as the result of the new run, since it only checks whether the file exists

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

True, fixed it. process_completed_jobs() accepts a geometry on os.path.exists(crest_best.xyz) alone, so a surviving stale file would be reported as the new run's result. crest_ts_conformer_search() now returns None when the removal fails, before writing any input.

I did change one idee though, the caller was doing break on None return, which would have dropped the remaining seed for the reaction. An undeletable file is a property of that directory, so now it's continue. Also made two tests to cover one being aborted and the other seeds still being prepared and then submitted.

if job_info["status"] not in TERMINAL_JOB_STATUSES:
try:
job_info["status"] = check_job_status(job_id)
except Exception as e:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A temporary scheduler error could mark the job as errored even if it’s still running. I think its better to just log the exception here and keep the current status unchanged

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, the exception is now logged and the status is left as it was, so the job is polled again on the next cycle.

Comment thread arc/job/adapters/ts/seed_hub.py Outdated
selected_hydrogen = None
selected_heavy_atoms = None
for hydrogen_index in hydrogen_indices:
heavy_atoms = sorted(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This filter removes all H atoms, even though a free H can be valid in H-abstraction. I think it would be better to use the same free-vs-bound H check as _is_valid_h_abs_atom_assignment() so bonded H atoms are still excluded

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nice find! _get_h_abs_atoms_from_xyz now admits the free H atoms as candidate partners, using the same _is_free_h_atom check as _is_valid_h_abs_atom_assignment, so R1H + R2 <> R1 + R2H recolves whilst a hydrogen bound to something else is still excluded as a spectator. CH4 + H test for this.

@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch 5 times, most recently from b7c237f to afd7821 Compare September 1, 2026 16:50
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch from afd7821 to 82c91d3 Compare September 5, 2026 07:30
Returns:
List[str]: The families of both hydrolysis parameter sets.
"""
from arc.job.adapters.ts.heuristics import FAMILY_SETS
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch from 82c91d3 to ca6f8c0 Compare September 5, 2026 18:19
@calvinp0
calvinp0 requested a lite review from Copilot September 5, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch from ca6f8c0 to 8c18690 Compare September 5, 2026 19:11
The absolute H-abstraction reactive-core distances BREAKING_BOND_LENGTH = 1.29 A and
FORMING_BOND_LENGTH = 1.36 A are the medians of the breaking (A-H) and forming (H-B)
bond lengths over a dataset of 1670 DFT-optimized H-abstraction transition states. That
dataset is dominated by C, N and O partner atoms, which is why the absolute distances are
applied only when the partner is one of those elements (ABSOLUTE_BOND_LENGTH_PARTNERS);
any other partner keeps the multiplicative 1.2 x equilibrium-bond stretch of the well.
@calvinp0
calvinp0 force-pushed the crest_adapter_clean branch from 8c18690 to f2edae5 Compare September 7, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants