Skip to content

CREST Adapter#807

Open
calvinp0 wants to merge 224 commits into
mainfrom
crest_adapter
Open

CREST Adapter#807
calvinp0 wants to merge 224 commits into
mainfrom
crest_adapter

Conversation

@calvinp0

@calvinp0 calvinp0 commented Nov 27, 2025

Copy link
Copy Markdown
Member

Addition of CREST Adapter that complements the heuristic adapter.

This pull request adds support for the CREST conformer and transition state (TS) search method to the ARC project, along with several related improvements and code cleanups. The most important changes include integrating CREST as a TS search adapter, updating configuration and constants, and enhancing the heuristics TS search logic for better provenance tracking and code clarity.

CREST Integration:

  • Added CREST as a supported TS search method: updated JobEnum (arc/job/adapter.py), included CREST in the list of adapters and RMG family mapping, and registered it as a default incore adapter (arc/job/adapters/common.py, arc/job/adapters/ts/__init__.py). [1] [2] [3] [4]
  • Implemented a new test suite for CREST input generation (arc/job/adapters/ts/crest_test.py).
  • Added a Makefile target and installation script for CREST (Makefile). [1] [2]

Constants and Configuration:

  • Added the angstrom_to_bohr conversion constant to both Cython and Python constants modules (arc/constants.pxd, arc/constants.py). [1] [2] [3]

Heuristics TS Search Enhancements and Refactoring:

  • Refactored heuristics TS search logic to track and combine method provenance for TS guesses, allowing for more precise attribution when multiple methods contribute to a guess (arc/job/adapters/ts/heuristics.py). [1] [2] [3] [4]
  • Improved code readability and maintainability by reformatting imports and function calls, and clarifying data structures and comments in heuristics TS search (arc/job/adapters/ts/heuristics.py). [1] [2] [3] [4] [5] [6] [7]
    .

Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/job/adapters/ts/autotst_ts.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed

This comment was marked as resolved.

Comment thread arc/job/adapters/ts/crest.py Fixed

This comment was marked as resolved.

Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
@calvinp0 calvinp0 force-pushed the crest_adapter branch 2 times, most recently from 3e88c36 to 7674f5f Compare February 2, 2026 09:49
@calvinp0 calvinp0 requested a review from Copilot February 2, 2026 12:10

This comment was marked as resolved.

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

This comment was marked as resolved.

@calvinp0 calvinp0 force-pushed the crest_adapter branch 2 times, most recently from 9be935e to eab7647 Compare February 4, 2026 20:14
@calvinp0 calvinp0 requested a review from alongd February 4, 2026 21:30
calvinp0 added 15 commits July 13, 2026 09:23
…by enum)

The backend hoists parameters_json.tckdb_origin.origin_kind into the validated
CalculationWithResultsPayload.origin_kind enum {executed, reused_result, imported,
derived} — proven by reused_result round-tripping while screened_conformer 422s
under identical nesting. So parameters_json is not opaque for this key; the
"free-form" assumption was wrong.

_screened_conformer_origin (alt/screened conformer opt rows: converged geometry +
relative energy from the conformer screen, no independently executed ESS job) emitted
origin_kind='screened_conformer', which is not an enum member, so every computed-species
bundle containing an alt conformer 422'd. Map it to 'derived' (a screened conformer's
geometry is derived from the conformer screen, not independently executed) and preserve
the ARC-specific 'screened_conformer' distinction verbatim under a new origin_detail key
(backend-opaque, like reason/producer). The selected conf0 is unmarked (real executed
opt) and unaffected; reused_result rows unchanged.

Add VALID_TCKDB_ORIGIN_KINDS + schema-conformance guard tests that walk each generated
payload and assert every origin_kind (at any depth) is a valid enum member, so this
class of 422 cannot recur silently.
…s_gcn hook

Unpinned 'pip install torch-geometric' pulled 2.6.x (needs torch>=2.0, imports
torch.utils._pytree absent in the pinned torch 1.7.1) -> TS-GCN ModuleNotFoundError.
Pin to the torch-1.7.1-compatible 1.7.2. Also export LD_LIBRARY_PATH=$CONDA_PREFIX/lib
in the activation hook so scipy finds the conda libstdc++ (GLIBCXX_3.4.29) instead of
an older system /lib64 one (surfaced on zeus once the torch-geometric import was fixed).
…stion

A transient TCKDB server not-ready state during a long ARC run made the in-run
upload fail its one-shot /readyz preflight, dropping the reaction upload (payload
was written; a manual CLI re-run succeeded once the server was up).

_ensure_ready now retries the probe up to PREFLIGHT_MAX_ATTEMPTS (5 = 1 initial +
4 retries) with exponential backoff (PREFLIGHT_BASE_DELAY_SECONDS * 2**i, capped
at PREFLIGHT_MAX_DELAY_SECONDS -> 1,2,4,8s, ~15s worst case), retrying both failure
modes (a TCKDBError request exception and a 200 body that reports not-ready), and
returns on the first ready response. The single-check-per-adapter-instance contract
is preserved (the loop runs once; the final error is cached in _preflight_error).
Backoff sleeps route through _preflight_sleep so tests patch them out.

On genuine exhaustion, _record_failure now calls _log_readiness_recovery for a
TCKDBReadinessError, logging that payloads were written and the exact copy-pasteable
re-run command (python -m arc.tckdb.cli <input.yml> -p <project> --upload-mode <mode>)
using the real project dir / upload mode in scope.
Fills the "TS-specific adapter not yet implemented" gap: converged transition
states are no longer deferred — a new computed_ts upload mode composes a
TransitionStateUploadRequest and POSTs each converged TS to /uploads/transition-states.

- adapter.py: submit_computed_ts_from_output + _compose_transition_state_request +
  _ts_calc_to_standalone + _build_ts_reaction_upload. Reuses _build_ts_block (results
  stay wrapped as opt_result/freq_result/... = the CalculationWithResultsPayload shape),
  the geometry/identity fragments, correction schemes, origin markers, and idempotency.
  primary_opt is the type=opt calc (backend validator); additional_calculations carry
  freq/sp/irc/path_search. Bundle-only keys stripped for the extra=forbid fragments.
- sweep.py: _run_ts_sweep uploads each transition_states entry whose own `converged`
  flag is true (same gate as species), maps TS->reaction via ts_label, skips
  non-converged and TSs with no referencing reaction (never fabricates a reaction).
- config.py: UPLOAD_MODE_COMPUTED_TS; CLI picks it up from VALID_UPLOAD_MODES.

Review (Fable) validated a REAL reaction_05 payload against the REAL backend pydantic
schema on the Pi (construction only, no POST): primary_opt=opt, freq/sp/irc attached,
origin_kind valid, no leaked keys. Sub-critical fixes applied: standalone path never
builds/inlines artifacts (backend has no slot) and warns once if artifacts.upload is set;
applied_energy_corrections omission documented + debug-logged; whitespace-only
reaction_family gated out (mirrors backend normalization) to avoid a 422.

Follow-up (documented): replace the test's reconstructed TransitionStateUploadRequest
with a real tckdb_schemas import once the package publishes it.
The GSM stringfile has no per-frame energies (comment lines are all 0.000000,
verified across every benchmark stringfile), so path points were geometry-only.
Rather than fabricate a flat-zero energy profile, this populates path_coordinate
correctly and adds a guarded energy fallback for GSM builds that DO emit energies.

- parser.py: parse_gsm_stringfile_energies() — reads the per-frame comment energy
  (relative kcal/mol) or None; parse_trajectory untouched (its callers unaffected).
- adapter.py _build_path_search_result_payload:
  * path_coordinate = cumulative Kabsch-ALIGNED inter-node displacement in Angstrom
    (the analog of ORCA NEB's Dist.(Ang.) the backend column stores). GSM does not
    keep every node in a common frame — the reactant endpoint is often translated/
    rotated vs the interior, which would inflate a raw displacement (~1.56 A vs
    ~0.15 A aligned for node 0->1) — so each node is aligned onto its predecessor
    (converter.kabsch) before summing. Node 0 = 0.0; null everywhere if alignment
    fails. This replaces an earlier index/(N-1) fraction, which mislabeled a shared
    column whose ORCA rows hold Angstroms.
  * guarded stringfile relative-energy fallback: only when NO per-node Hartree was
    preserved AND the comment column spans > 1e-6 kcal/mol (not the all-zero
    sentinel), avoiding a fabricated flat profile and source-mixing with partial
    Hartrees; flags the peak is_climbing_image.

The real per-node energy gap is producer-side (GSM stdout / gsm_node_outputs not
preserved on the run) and tracked separately. Parser=stays / adapter=extractable.
The running_jobs.yml status snapshot previously listed only bare job_names
(e.g. 'tsg3'), forcing a cross-reference against arc.log to find the job on
the cluster by its qstat name (e.g. 'a3129'). Add
Scheduler._running_job_snapshot_entry, which resolves each running job_name
back to its Job object in job_dict and emits a self-describing entry
{name, server_name (qstat job name), job_id, adapter, server}. Unresolvable
entries fall back to {'name': job_name}. report_running_jobs_snapshot now
builds running_jobs as {label: [entry, ...]}.
GSM's driver invokes the copied ograd gradient wrapper as ./ograd, which
requires the executable bit. write_input_file (incore) and set_files (queue)
copied ograd without +x (safe_copy_file/copyfile drops mode, tracked template
was 100644), so ograd landed 644 and GSM aborted with permission denied.

- incore: add change_mode('+x') on self.ograd_path
- queue: pass make_x=True on the ograd upload dict
- git: make the tracked ograd template 100755 (defense-in-depth)
- test: assert incore ograd is X_OK; flip set_files ograd expectation to make_x=True
When ARC's TS-guess deduplication (ARCSpecies.cluster_tsgs) merges several
guesses with the same geometry into one, it keeps a single primary `method`
plus a merged `method_sources` list. Path-search provenance (GSM/NEB per-node
energies and points) keyed off the SINGLE primary method, so when a
geometry-only method (e.g. gcn) won dedup over an absorbed path-search source
(xtb-gsm / orca_neb), the GSM/NEB log was dropped: `paths['gsm']` was never
set in the scheduler and the TCKDB `path_search` calc was skipped.

Additive fix (does NOT change which guess/method wins selection):

- TSGuess gains `method_source_paths` (method -> log path). cluster_tsgs seeds
  it from each clustered guess's live method/log_path — the xtb_gsm/orca_neb/
  qst2 adapters assign log_path *after* construction, so the dict is empty at
  clustering time and must be seeded there, not only in __init__. Persisted
  through as_dict/from_dict for restarts.
- scheduler._ts_guess_path_provenance: primary method first, else the first
  path-search method in method_sources with a preserved log. Used at both
  output[label]['paths'] write sites.
- output.py restart-fallback recovers a merged source's log into gsm_log/
  neb_log, single-slot (first source wins) to match the scheduler.
- tckdb adapter._resolve_ts_guess_path_search_for_record gates emission off
  the chosen guess's method_sources, preferring the method whose log field is
  populated so the gate and log lookup agree for multi-source merges.
…ints

The xtb-GSM TS search writes per-node xTB outputs into gsm_node_outputs/
(0000.NN.xtbout), each carrying a real total energy, but those energies
never reached the TCKDB path_search_result.points: the reader globbed
*.energy files, while the patched ograd wrapper reliably preserves only
*.xtbout (the .energy/.gradient copies depend on a tm2orca.py rename that
usually doesn't land). So electronic_energy_hartree / relative_energy_kj_mol
came out null despite the data existing on disk.

- Add _parse_xtb_xtbout_energy: robustly parse the xTB total energy
  (Hartree) from an .xtbout file (accepts either the ':: total energy .. Eh'
  block or the boxed '| TOTAL ENERGY .. Eh |' banner, case-insensitive,
  last-match-wins; UTF-8 with errors='replace' so the non-ASCII 'Eh/α'
  glyph can't raise UnicodeDecodeError and abort the bundle build).
- _read_gsm_node_outputs now prefers the cleaner Turbomole .energy file
  (with gradient), then falls back to .xtbout (energy only) for any node
  still missing.
- Relax the relative-energy rule from strict all-or-none to: reference
  every point that has an absolute energy to min(known); points without
  one stay explicit nulls. Real GSM stringfiles have N+1 frames but only
  N node-energy files (the fixed reactant-anchor frame 0 has no ograd
  energy), so strict all-or-none suppressed the whole profile on all real
  data. Node label NN maps to stringfile frame index NN; frame 0 is the
  orphan. Confirmed on reaction_06: 14 energies parse, the -9.0206 Eh peak
  (node 8) lands exactly on the selected TS-guess frame, relatives
  reference the reactant well.

Tests: .xtbout parsing (formats, garbled/missing -> None, non-UTF-8 bytes,
Eh-unit gating, last-match), reader xtbout fallback + .energy precedence +
mixed sources, and the real N+1-frame orphan-endpoint end-to-end shape.
…ilure

compare_thermo ran the RMG/Arkane thermo script via execute_command, which
returns stderr as a list of lines and does not surface the return code.
RMG/Arkane write their normal INFO:root/WARNING:root startup logging to
stderr even on a fully successful run, so `if len(stderr): logger.error(...)`
emitted a false "Error while running RMG thermo script" on every reaction,
despite output/RMG_thermo.yml being produced and consumed. It was a
logger.error only (never raised), so the impact was cosmetic noise in arc.log.

Fix, kept local to compare_thermo (execute_command's signature is unchanged):
- Filter stderr, dropping benign `^(INFO|WARNING|DEBUG):root` lines and blanks;
  demote that chatter to logger.debug.
- Only logger.error if real (non-log) error content remains OR the deliverable
  wasn't produced (gate on RMG_thermo.yml yielding species with h298/s298,
  since the return code isn't available here). A genuine failure (real
  traceback, or an unpopulated/missing deliverable) still logs an error.
- Early-return when there are no species to compare (avoids a pointless RMG
  database load and a spurious error report).

Adds focused unit tests: benign INFO/WARNING stderr + populated deliverable
logs no error; a real traceback, and separately an unpopulated deliverable,
each still log an error.
The backend StatmechInBundle / BundleStatmechIn now accept
optical_isomers (int, ge=1); ARC already carries the value in
output.yml (species[*].statmech.optical_isomers, from Arkane's
conformer.opticalIsomers) but _build_statmech_block_for_species
dropped it, copying only external_symmetry. Add the field with the
same int>=1 guard so absent/non-int/0 are omitted. The shared builder
covers both computed-species and computed-reaction bundle modes.
PayloadWriter.write atomically overwrote the canonical
<label>.payload.json and its .meta.json sidecar on a re-run for the
same label, destroying the prior payload plus the sidecar that held
the last upload's status/idempotency_key/server IDs. The TCKDB backend
appends on re-upload (old + new bundles resolved in review), so
destroying the local prior bundle broke the trail mapping which local
run produced which DB record.

Before overwriting, move the existing payload+sidecar pair into an
archive/ subdir under a self-describing name keyed off the prior
sidecar's identity: uploaded_at if present (the accepted-record
moment), else idempotency_key (the server's dedupe key), else the
payload mtime, else a monotonic counter. The key is made
filesystem-safe (colons stripped). Colliding archive names are
disambiguated with a numeric suffix. Move-then-write ordering means a
crash can never lose the prior payload (it is either canonical or
archived, never gone). First writes archive nothing.

Default-on via archive_previous=True; existing callers untouched. The
canonical current path is unchanged, so upload/replay/discovery
tooling is unaffected (no in-repo tooling globs the payload dirs;
archive/ is a nested subdir external discovery must skip).
Open-shell/unrestricted single points carry an <S**2> spin-contamination
value that is needed to replicate/validate Arkane thermo for radicals and
biradicals. ARC parsed no such diagnostic and output.yml carried none, so
the TCKDB backend's ready spin_diagnostic slot went unfilled.

Parser: add parse_s_squared to the ESS adapter interface (default None on
the base class) with implementations for Gaussian (converged post-SCF
<S**2>=, plus "S**2 before annihilation ... after" -> s_squared_annihilated,
expected S(S+1) from the log's multiplicity), ORCA ("Expectation value of
<S**2>" + "Ideal value S*(S+1)" -> expected; no annihilation), and QChem
(<S^2> =; expected from the echoed $molecule multiplicity). Registered as
parser.parse_s_squared; s_squared_expected_from_multiplicity helper added.
Restricted/closed-shell logs print no <S**2> -> None.

output.yml: _parse_spin_diagnostic parses the sp log (falling back to
freq/opt when the sp energy reused the opt output), recomputing the ideal
S(S+1) from ARC's authoritative multiplicity; _spc_to_dict writes it as
sp_spin_diagnostic (None for closed-shell).

TCKDB emit: _spin_diagnostic_payload builds SpinDiagnosticPayload and
attaches it as the sp calc's spin_diagnostic in _calculation_payload (the
single funnel for all sp construction sites), only when s_squared was
parsed; closed-shell/absent -> block omitted (never all-null). ge=0 and the
required s_squared field are respected; optional companions omitted when
absent. Coupled-cluster wavefunction_diagnostic (T1/D1) deferred.

Tests: parser unit tests (Gaussian doublet/triplet, ORCA, QChem, closed-
shell None, malformed None) against real repo fixtures; output.yml plumbing
tests; TCKDB emit tests incl. full CalculationWithResultsPayload
model_validate against installed tckdb-schemas.
…ions)

For an A+A reaction (two reactants that are the same species, e.g. OH + OH),
Arkane's own save_thermo_lib raises DatabaseError on the R1==R2 duplicate at the
very end of its run -- after output.py is written but before RMG_libraries/thermo.py
is. ARC's save_arkane_thermo.py then returned early (it guards on that missing
library file), so thermo.yaml was never written and parse_arkane_thermo_output left
spc.thermo null: null thermo in output.yml and an empty project thermo library,
despite Arkane having computed full NASA polynomials.

Two independent fixes:

1. Robust reload (primary): when RMG_libraries/thermo.py is absent,
   save_arkane_thermo.py now reconstructs each species' NASA thermo directly from
   output.py (evaluated with the same rmgpy classes Arkane uses to read these files
   back) and produces thermo.yaml exactly as the library path would. This recovers
   thermo from ANY save_thermo_lib failure, not just A+A. Happy path unchanged: the
   library file is used when present.

2. Dedup at the source (secondary): _dedup_thermo_species_list collapses species
   with identical adjacency list + multiplicity into a single Arkane thermo block
   (full-thermo run only; kinetics and e0-only keep every block), so save_thermo_lib
   never sees a duplicate and never crashes. The collapsed duplicate label's thermo
   (and E0/symmetry) is copied from the representative in parse_arkane_thermo_output.

The computed_reaction TCKDB payload was unaffected (it carries no NASA).

Tests: A+A dedup + propagation and an all-distinct regression (arc_env); output.py
reconstruction and library-precedence (rmg_env, run as a script).
# Conflicts:
#	arc/settings/settings.py
if len(tokens) >= 2:
try:
multiplicity = int(tokens[1])
except ValueError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI 1 day ago

To fix this without changing functionality, replace the empty except ValueError: pass with a non-empty handler that explicitly records intent. The safest minimal change is to assign multiplicity = None (no behavioral change since it is already None) and add a clarifying comment that malformed multiplicity lines are ignored intentionally.

Edit only arc/parser/adapters/qchem.py, in parse_s_squared(), around lines 163–166.

No new imports, methods, or dependencies are required.

Suggested changeset 1
arc/parser/adapters/qchem.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/parser/adapters/qchem.py b/arc/parser/adapters/qchem.py
--- a/arc/parser/adapters/qchem.py
+++ b/arc/parser/adapters/qchem.py
@@ -163,7 +163,9 @@
                         try:
                             multiplicity = int(tokens[1])
                         except ValueError:
-                            pass
+                            # Leave multiplicity as None if the echoed $molecule line is malformed.
+                            # This keeps parsing robust while allowing s_squared parsing to proceed.
+                            multiplicity = None
                     in_molecule = False
         if s_squared is None:
             return None
EOF
@@ -163,7 +163,9 @@
try:
multiplicity = int(tokens[1])
except ValueError:
pass
# Leave multiplicity as None if the echoed $molecule line is malformed.
# This keeps parsing robust while allowing s_squared parsing to proceed.
multiplicity = None
in_molecule = False
if s_squared is None:
return None
Copilot is powered by AI and may make mistakes. Always verify output.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # for 'common' and the module under test

try:
from rmgpy.thermo import NASA # noqa: F401

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'NASA' is not used.

Copilot Autofix

AI 1 day ago

Use a module-level import check instead of importing an unused class.
Best fix: in arc/scripts/save_arkane_thermo_test.py, replace:

  • from rmgpy.thermo import NASA # noqa: F401

with:

  • import rmgpy.thermo

This preserves behavior (HAS_RMG reflects whether rmgpy.thermo is importable), removes the unused symbol, and avoids changing functionality.

Suggested changeset 1
arc/scripts/save_arkane_thermo_test.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/scripts/save_arkane_thermo_test.py b/arc/scripts/save_arkane_thermo_test.py
--- a/arc/scripts/save_arkane_thermo_test.py
+++ b/arc/scripts/save_arkane_thermo_test.py
@@ -23,7 +23,7 @@
 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))  # for 'common' and the module under test
 
 try:
-    from rmgpy.thermo import NASA  # noqa: F401
+    import rmgpy.thermo
     HAS_RMG = True
 except ImportError:
     HAS_RMG = False
EOF
@@ -23,7 +23,7 @@
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # for 'common' and the module under test

try:
from rmgpy.thermo import NASA # noqa: F401
import rmgpy.thermo
HAS_RMG = True
except ImportError:
HAS_RMG = False
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread arc/settings/settings.py
# adapters support (see ts_adapters_by_rmg_family) but that RMG does not list as recommended
# (e.g. Intra_RH_Add_Endocyclic, XY_Addition_MultipleBond). Useful when running specific
# reactions across many families (e.g. a benchmark) rather than generating a mechanism.
rmg_family_set = 'default'

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'rmg_family_set' is not used.

Copilot Autofix

AI 1 day ago

To fix this without changing behavior, explicitly export rmg_family_set via __all__. This tells linters/static analyzers that the variable is intentionally part of the module’s public API.

Best change in arc/settings/settings.py:

  • Add a module-level __all__ definition that includes 'rmg_family_set'.
  • Place it near the rmg_family_set assignment for clarity.
  • Do not rename or delete the variable, since it may be imported elsewhere and used as a documented setting.

No new imports, methods, or dependencies are needed.

Suggested changeset 1
arc/settings/settings.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/settings/settings.py b/arc/settings/settings.py
--- a/arc/settings/settings.py
+++ b/arc/settings/settings.py
@@ -378,6 +378,7 @@
 # (e.g. Intra_RH_Add_Endocyclic, XY_Addition_MultipleBond). Useful when running specific
 # reactions across many families (e.g. a benchmark) rather than generating a mechanism.
 rmg_family_set = 'default'
+__all__ = ['rmg_family_set']
 
 # Default environment names for sister repos
 TS_GCN_PYTHON, TANI_PYTHON, UMA_PYTHON, AUTOTST_PYTHON, KINBOT_PYTHON, ARC_PYTHON, XTB, XTB_PYTHON, OB_PYTHON, RMG_PYTHON, RMG_PATH, RMG_DB_PATH = \
EOF
@@ -378,6 +378,7 @@
# (e.g. Intra_RH_Add_Endocyclic, XY_Addition_MultipleBond). Useful when running specific
# reactions across many families (e.g. a benchmark) rather than generating a mechanism.
rmg_family_set = 'default'
__all__ = ['rmg_family_set']

# Default environment names for sister repos
TS_GCN_PYTHON, TANI_PYTHON, UMA_PYTHON, AUTOTST_PYTHON, KINBOT_PYTHON, ARC_PYTHON, XTB, XTB_PYTHON, OB_PYTHON, RMG_PYTHON, RMG_PATH, RMG_DB_PATH = \
Copilot is powered by AI and may make mistakes. Always verify output.
calvinp0 added 3 commits July 14, 2026 20:20
…ings)

- arc/statmech/arkane_test.py: TestRunArkaneOutputPySignal asserted a
  'non-cosmetic lines' substring that run_arkane never emits. The advisory
  WARNING it actually logs is 'Arkane emitted errors but still produced
  output.py (proceeding): ...'. The code's output.py-existence success
  signal and its logging are correct; assert the real substring.
- arc/main_test.py: TestARC::test_as_dict expected_dict omitted the qst2
  ESS entry that arcbench's settings enable ('qst2': 'local'). Add
  'qst2': ['local'] in alphabetical slot to match produced ess_settings.
A coordinate-less TS guess (e.g. a failed kinbot/queue guess, or a queue
TS-search job whose .log produced no parseable geometry) reached
almost_equal_coords(None, None) during cluster_tsgs, raising a TypeError
that aborted the entire scheduler at init (benchmark reaction_08).

- almost_equal_tsgs: if either guess's get_xyz() is None, return False
  instead of comparing coordinates. A guess with no geometry can never be
  a geometric duplicate.
- process_completed_tsg_queue_jobs: when a queue .log yields no usable
  geometry, mark the guess failed and log a warning instead of silently
  dropping it, so it is never kept as a clusterable 'successful' guess.

Adds regression tests covering both paths.
Register 'Disproportionation' in both TS-adapter gates for autotst:
the adapter's own supported_families and ts_adapters_by_rmg_family.
intra_H_migration was already enabled and is untouched.

Note: Disproportionation requires the AutoTST env on a branch whose
SUPPORTED_FAMILIES includes it (e.g. fix/disproportionation-support),
not AutoTST main. Adds a focused test asserting both gates admit it.
shutil.rmtree(path, ignore_errors=True)
try:
os.rmdir(self.project_dir)
except OSError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI about 14 hours ago

The best fix is to replace the empty except with explicit handling that preserves current functionality (best-effort cleanup) while making intent clear and avoiding silent swallowing.

In arc/job/adapters/ts/autotst_ts_test.py, update _remove_test_dir so that:

  • os.rmdir(self.project_dir) remains in a try.
  • except OSError becomes except OSError as e.
  • Add an explanatory comment and only suppress expected race/cleanup conditions (directory not empty / not found), re-raising unexpected OS errors.

This keeps tests robust under parallel execution and satisfies the static-analysis concern by avoiding an empty handler.

Suggested changeset 1
arc/job/adapters/ts/autotst_ts_test.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/job/adapters/ts/autotst_ts_test.py b/arc/job/adapters/ts/autotst_ts_test.py
--- a/arc/job/adapters/ts/autotst_ts_test.py
+++ b/arc/job/adapters/ts/autotst_ts_test.py
@@ -44,8 +44,12 @@
         shutil.rmtree(path, ignore_errors=True)
         try:
             os.rmdir(self.project_dir)
-        except OSError:
-            pass
+        except OSError as e:
+            # Best-effort cleanup: under parallel execution the shared parent directory
+            # may already be removed or still contain other tests' subdirectories.
+            if not os.path.isdir(self.project_dir):
+                return
+            raise e
 
     def get_adapter(self, dir_name: str) -> AutoTSTAdapter:
         """A helper function to instantiate an AutoTSTAdapter instance."""
EOF
@@ -44,8 +44,12 @@
shutil.rmtree(path, ignore_errors=True)
try:
os.rmdir(self.project_dir)
except OSError:
pass
except OSError as e:
# Best-effort cleanup: under parallel execution the shared parent directory
# may already be removed or still contain other tests' subdirectories.
if not os.path.isdir(self.project_dir):
return
raise e

def get_adapter(self, dir_name: str) -> AutoTSTAdapter:
"""A helper function to instantiate an AutoTSTAdapter instance."""
Copilot is powered by AI and may make mistakes. Always verify output.
calvinp0 added 7 commits July 15, 2026 12:51
An impossible spin multiplicity (e.g. an even multiplicity for a species
with an even electron count) was forwarded all the way to the ESS, where
it surfaced as a cryptic non-retryable Gaussian GL301 InputError and
aborted the species after wasting a full run.

Enforce the parity relation
    (total_electrons - net_charge) % 2 == (multiplicity - 1) % 2
in ARCSpecies once charge and multiplicity are finalized, raising a clear
SpeciesError that names the label, electron count, net charge, requested
multiplicity, and the nearest valid multiplicities. This is an inviolable
parity relation, so the guard has zero false positives. It is skipped
gracefully when the electron count is not yet known (e.g. a TS or an
xyz-less species with no perceived Molecule).
A fine transition-state optimization that dead-ends on the Gaussian l9999
"Optimization stopped" / "Number of steps exceeded" oscillation (classified
as ['MaxOptCycles', 'GL9999']) was previously escalated only through the
Hessian/step ladder (maxcycle=200 -> recalcfc=5 -> calcall -> RFO) and then
abandoned via 'all_attempted'. opt=(cartesian) - wired until now only to the
GL103/InternalCoordinateError path - was never tried, even though the failure
is a redundant-internal-coordinate oscillation that Cartesian coordinates
sidestep. This lost chemically-sensible TS guesses (e.g. arcbench reaction_16
C=C + HCl, heuristics-xy conformers 0/1).

Insert opt=(cartesian) as an early rung in the MaxOptCycles ladder, right
after the cheap maxcycle bump and before the expensive Hessian/algorithm
escalation, gated tightly to the fine TS opt (is_ts and fine). Ground-state
opts (is_ts=False) and coarse TS opts (fine=False) are unchanged, as is the
GL103 path. Cartesian is recorded once (bare 'cartesian' marker, consistent
with trsh_keyword_cartesian) and folded into the merged opt route so it is
carried through the remaining rungs; the ladder still terminates via
'all_attempted' if Cartesian also fails.

Also fixes a latent cartesian-loss in mixed GL103+MaxOptCycles histories,
where the opt-route rewrite would previously clobber a standalone
opt=(cartesian).
seed = {'xyz': xyz, 'family': rxn.family}
constraints = get_wrapper_constraints(wrapper='crest', reaction=rxn, seed=seed)
self.assertIsInstance(constraints, dict)
self.assertTrue({'A', 'H', 'B', 'atoms', 'distance_pairs', 'angle_atoms'} <= set(constraints))

Check notice

Code scanning / CodeQL

Imprecise assert Note

assertTrue(a <= b) cannot provide an informative message. Using assertLessEqual(a, b) instead will give more informative messages.

Copilot Autofix

AI about 9 hours ago

Replace the imprecise boolean assertion with a specific unittest assertion that compares sets directly.

Best fix in this file/region:

  • In arc/job/adapters/ts/heuristics_test.py, in test_get_wrapper_constraints_crest (around line 2328), change:
    • self.assertTrue(expected_keys <= set(constraints))
      to
    • self.assertLessEqual(expected_keys, set(constraints))

This preserves test intent (expected keys must be a subset of actual keys) while yielding more informative failure messages that show the compared sets.

No new imports, helper methods, or dependencies are needed.

Suggested changeset 1
arc/job/adapters/ts/heuristics_test.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/job/adapters/ts/heuristics_test.py b/arc/job/adapters/ts/heuristics_test.py
--- a/arc/job/adapters/ts/heuristics_test.py
+++ b/arc/job/adapters/ts/heuristics_test.py
@@ -2325,7 +2325,7 @@
         seed = {'xyz': xyz, 'family': rxn.family}
         constraints = get_wrapper_constraints(wrapper='crest', reaction=rxn, seed=seed)
         self.assertIsInstance(constraints, dict)
-        self.assertTrue({'A', 'H', 'B', 'atoms', 'distance_pairs', 'angle_atoms'} <= set(constraints))
+        self.assertLessEqual({'A', 'H', 'B', 'atoms', 'distance_pairs', 'angle_atoms'}, set(constraints))
         self.assertEqual(
             (constraints['A'], constraints['H'], constraints['B']),
             constraints['angle_atoms'],
EOF
@@ -2325,7 +2325,7 @@
seed = {'xyz': xyz, 'family': rxn.family}
constraints = get_wrapper_constraints(wrapper='crest', reaction=rxn, seed=seed)
self.assertIsInstance(constraints, dict)
self.assertTrue({'A', 'H', 'B', 'atoms', 'distance_pairs', 'angle_atoms'} <= set(constraints))
self.assertLessEqual({'A', 'H', 'B', 'atoms', 'distance_pairs', 'angle_atoms'}, set(constraints))
self.assertEqual(
(constraints['A'], constraints['H'], constraints['B']),
constraints['angle_atoms'],
Copilot is powered by AI and may make mistakes. Always verify output.
calvinp0 added 2 commits July 15, 2026 19:41
…ny cores

On small double-radical PESs (e.g. OH + OH -> H2O + O) the CREST H-abstraction
$constrain block pinned only the A-H and H-B distances plus a weak positional
restraint, leaving the A...B heavy-heavy separation and the A-H-B angle free. GFN2-xTB
metadynamics then collapsed the linear A...H...B seed into a bent A-B minimum, which the
post-CREST angle guard correctly rejected.

Part 1: for the three-center H-abstraction path, also emit 'distance: A, B, auto' and
'angle: A, H, B, auto' in the $constrain block (xtb supports the angle keyword; see
xtb_adapter.py). The XY four-center path (no angle_atoms) is left unchanged.

Part 2: skip the CREST guess when the reactive core spans essentially the whole molecule
(H_Abstraction with <=1 spectator atom, i.e. <=4 atoms total), where CREST has no
conformational DOF to sample; defer to the other TS-search methods.
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