Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
dfc5623
minor: install_all.sh
alongd Mar 26, 2026
f5dbaa4
Minor: Style mod in mapping driver
alongd Mar 17, 2026
2505590
Organize teardown in functional and common tests
alongd Apr 18, 2026
90bcb98
Added Claude to .gitignore
alongd Apr 18, 2026
ea8d937
Added a debug message if fragment mapping fails
alongd Apr 18, 2026
929be1c
Modifications to CI
alongd Apr 18, 2026
c9583b5
Added a nightly CI for slower tests
alongd Apr 18, 2026
6a347e4
Mark slow unit tests in .toml
alongd Apr 18, 2026
8598a1e
Added Reaction.is_unimolecular()
alongd Aug 21, 2023
dacef4e
Tests: Reaction.is_unimolecular()
alongd May 15, 2024
952a17c
Added an atom_order arg to xyz_to_zmat()
alongd Aug 21, 2023
e38941c
Tests: xyz_to_zmat()
alongd Aug 21, 2023
1f512e5
Added 'linear' to ts_adapters_by_rmg_family
alongd Aug 21, 2023
8833d03
Added check_ordered_zmats()
alongd Aug 21, 2023
c50b03f
Tests: check_ordered_zmats()
alongd Aug 21, 2023
3c9f0d1
Added linear to JobEnum
alongd Aug 21, 2023
598541a
Added the Linear TS search job adapter and related utils
alongd May 15, 2024
f0f0fe4
Tests: Linear TS Job Adapter and utils
alongd May 15, 2024
8e27052
Added order_xyz_by_atom_map to converter
alongd May 26, 2024
810d671
Tests: converter order_xyz_by_atom_map()
alongd May 26, 2024
e01dbb7
Added update_zmat_by_xyz() to zmat
alongd May 26, 2024
b86686f
Tests: zmat update_zmat_by_xyz()
alongd May 26, 2024
c316ef2
Use the round_to arg in common get_angle_in_180_range()
alongd Jan 1, 2026
7d7fb8b
Tests: use round_to in get_angle_in_180_range()
alongd Jan 1, 2026
ff045fb
Added anchors to xyz_to_zmat()
alongd Mar 17, 2026
677a674
Tests: zmat anchors
alongd Mar 17, 2026
1802392
Added converter order_mol_by_atom_map()
alongd Mar 31, 2026
444e320
Added zmat find_smart_anchors()
alongd Mar 31, 2026
aa4f95c
Updated the ts_adapters_by_rmg_family dict
alongd Mar 31, 2026
812c8b2
Tests: call self.job_3.execute() in OB tests
alongd Apr 18, 2026
91c82ca
Preserve radical sites when perceiving mol from xyz with an existing mol
alongd Mar 31, 2026
0a65d5a
Added an iPY notebook to showcase the linear adapter
alongd Apr 18, 2026
b8981e9
Docs: Added descriptions of TS search methods implemented in ARC
alongd Apr 18, 2026
0aa7793
Added split_entries() to family
alongd Apr 18, 2026
64296c2
Tests: family.py
alongd Apr 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ jobs:
path: ARC

- name: Clean Ubuntu Image
uses: jlumbroso/free-disk-space@main
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
swap-storage: false

- name: Set up micromamba (arc_env)
uses: mamba-org/setup-micromamba@v3
with:
environment-name: arc_env
environment-file: ARC/environment.yml
cache-environment: true
cache-environment: false
cache-downloads: true
generate-run-shell: true

Expand Down Expand Up @@ -102,7 +102,7 @@ jobs:
run: |
echo "Running Unit Tests..."
export PYTHONPATH="${{ github.workspace }}/AutoTST:${{ github.workspace }}/KinBot:$PYTHONPATH"
pytest arc/ --cov --cov-report=xml -ra -vv -n auto
pytest arc/ --cov --cov-report=xml -ra -vv -n 4 --dist worksteal -m "not slow"

- name: Run Functional Tests
shell: micromamba-shell {0}
Expand All @@ -113,7 +113,7 @@ jobs:
run: |
echo "Running Functional Tests from $(pwd)..."
export PYTHONPATH="${{ github.workspace }}/AutoTST:${{ github.workspace }}/KinBot:$PYTHONPATH"
pytest functional/ --cov --cov-append --cov-report=xml -ra -vv -n auto
pytest functional/ --cov --cov-append --cov-report=xml -ra -vv -n 4 --dist worksteal

- name: Upload coverage data
uses: codecov/codecov-action@v6
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Nightly (Slow Tests)

on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'

jobs:
nightly-slow-tests:
name: Nightly Unit Tests (including slow)
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}

steps:
- name: Checkout ARC
uses: actions/checkout@v6
with:
ref: main
path: ARC

- name: Clean Ubuntu Image
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: false

- name: Set up micromamba (arc_env)
uses: mamba-org/setup-micromamba@v3
with:
environment-name: arc_env
environment-file: ARC/environment.yml
cache-environment: false
cache-downloads: true
generate-run-shell: true

- name: Install conda in micromamba base
shell: micromamba-shell {0}
run: |
echo "::group::Install conda in micromamba base"
micromamba install -n base -c conda-forge conda
echo "::endgroup::"

- name: Export ARC paths
shell: micromamba-shell {0}
working-directory: ARC
run: |
echo "PATH=$PATH:$PWD" >> "$GITHUB_ENV"
echo "PYTHONPATH=$PYTHONPATH:$PWD" >> "$GITHUB_ENV"

- name: Install Julia 1.10
shell: micromamba-shell {0}
run: |
echo "::group::Install juliaup + Julia 1.10"
curl -fsSL https://install.julialang.org | sh -s -- -y
export PATH="$HOME/.juliaup/bin:$PATH"
juliaup add 1.10
juliaup default 1.10
echo "PATH=$HOME/.juliaup/bin:$PATH" >> "$GITHUB_ENV"
echo "::endgroup::"
julia --version

- name: Install all extras - CI
shell: micromamba-shell {0}
working-directory: ARC
env:
MAMBA_ALWAYS_YES: "true"
CONDA_ALWAYS_YES: "true"
run: make install-ci

- name: Set TS-GCN and AutoTST in PYTHONPATH
shell: micromamba-shell {0}
working-directory: ARC
run: |
echo "PYTHONPATH=$(realpath ../TS-GCN):$(realpath ../AutoTST):$PYTHONPATH" >> $GITHUB_ENV

- name: Compile ARC molecule
shell: micromamba-shell {0}
working-directory: ARC
env:
ARC_COVERAGE: 1
run: |
make compile

- name: Run Unit Tests (including slow)
shell: micromamba-shell {0}
working-directory: ARC
env:
ARC_COVERAGE: 1
CYTHON_TRACE: 1
run: |
echo "Running Unit Tests (including slow)..."
export PYTHONPATH="${{ github.workspace }}/AutoTST:${{ github.workspace }}/KinBot:$PYTHONPATH"
pytest arc/ -ra -vv -n 4 --dist worksteal
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ timer.dat
# .vscode
.vscode

# Claude
CLAUDE.md
.claude/*

# .trunk folder
.trunk

Expand Down
12 changes: 6 additions & 6 deletions arc/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

R = 8.31446261815324 # J/(mol*K)
EA_UNIT_CONVERSION = {'J/mol': 1, 'kJ/mol': 1e+3, 'cal/mol': 4.184, 'kcal/mol': 4.184e+3}
FULL_CIRCLE = 360.0
HALF_CIRCLE = 180.0

default_job_types, servers, supported_ess = settings['default_job_types'], settings['servers'], settings['supported_ess']

Expand Down Expand Up @@ -1507,14 +1509,11 @@ def is_xyz_linear(xyz: Optional[dict]) -> Optional[bool]:
return True


FULL_CIRCLE = 360.0
HALF_CIRCLE = 180.0

def get_angle_in_180_range(angle: float,
round_to: Optional[int] = 2,
round_to: Optional[int] = None,
) -> float:
"""
Get the corresponding angle in the -180 to +180 degree range.
Get the corresponding angle in the -180 to +180 degree range, (-180,180]

Args:
angle (float): An angle in degrees.
Expand All @@ -1524,7 +1523,8 @@ def get_angle_in_180_range(angle: float,
Returns:
float: The corresponding angle in the -180 to +180 degree range.
"""
return (angle + HALF_CIRCLE) % FULL_CIRCLE - HALF_CIRCLE
wrapped = (angle + HALF_CIRCLE) % FULL_CIRCLE - HALF_CIRCLE
return round(wrapped, round_to) if round_to is not None else wrapped


def signed_angular_diff(phi_1: float, phi_2: float) -> float:
Expand Down
28 changes: 24 additions & 4 deletions arc/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,33 @@
"""
Contains unit tests for ARC's common module
"""
@classmethod
def _clean_globalized_restart_artifact(cls):
"""Remove the globalized restart-paths artifact written by
:meth:`test_globalize_paths`.

Called from BOTH ``setUpClass`` (defensive — wipes a stale
artifact left behind by a previously interrupted run) and
``tearDownClass`` (the normal cleanup path). This makes the
cleanup self-healing: a Ctrl+C, ``kill``, or hard error during
a previous run cannot leave the next run inheriting the prior
``restart_paths_globalized.yml``.
"""
globalized_restart_path = os.path.join(
common.ARC_TESTING_PATH, 'restart', '4_globalized_paths',
'restart_paths_globalized.yml')
if os.path.isfile(globalized_restart_path):
try:
os.remove(path=globalized_restart_path)
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 5 days ago

Use an API that encodes the intended “best-effort cleanup” behavior without an empty except.
The best fix is to replace the isfile + try/except os.remove sequence with pathlib.Path.unlink(missing_ok=True).

Why this is best here:

  • It preserves functionality (remove artifact if present; do nothing if already absent).
  • It removes the empty except entirely.
  • It avoids TOCTOU race between existence check and delete.
  • It keeps unexpected errors (e.g., permission denied) visible instead of silently ignored.

Changes needed in arc/common_test.py:

  1. Add from pathlib import Path near existing imports.
  2. In _clean_globalized_restart_artifact, replace the if os.path.isfile(...): try: os.remove... except OSError: pass block with:
    • globalized_restart_path = Path(... ) / ...
    • globalized_restart_path.unlink(missing_ok=True)

No other files or behavior changes are required.

Suggested changeset 1
arc/common_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/common_test.py b/arc/common_test.py
--- a/arc/common_test.py
+++ b/arc/common_test.py
@@ -10,6 +10,7 @@
 import os
 import time
 import unittest
+from pathlib import Path
 
 import numpy as np
 import pandas as pd
@@ -42,14 +43,11 @@
         a previous run cannot leave the next run inheriting the prior
         ``restart_paths_globalized.yml``.
         """
-        globalized_restart_path = os.path.join(
-            common.ARC_TESTING_PATH, 'restart', '4_globalized_paths',
-            'restart_paths_globalized.yml')
-        if os.path.isfile(globalized_restart_path):
-            try:
-                os.remove(path=globalized_restart_path)
-            except OSError:
-                pass
+        globalized_restart_path = (
+            Path(common.ARC_TESTING_PATH) / 'restart' / '4_globalized_paths'
+            / 'restart_paths_globalized.yml'
+        )
+        globalized_restart_path.unlink(missing_ok=True)
 
     @classmethod
     def setUpClass(cls):
EOF
@@ -10,6 +10,7 @@
import os
import time
import unittest
from pathlib import Path

import numpy as np
import pandas as pd
@@ -42,14 +43,11 @@
a previous run cannot leave the next run inheriting the prior
``restart_paths_globalized.yml``.
"""
globalized_restart_path = os.path.join(
common.ARC_TESTING_PATH, 'restart', '4_globalized_paths',
'restart_paths_globalized.yml')
if os.path.isfile(globalized_restart_path):
try:
os.remove(path=globalized_restart_path)
except OSError:
pass
globalized_restart_path = (
Path(common.ARC_TESTING_PATH) / 'restart' / '4_globalized_paths'
/ 'restart_paths_globalized.yml'
)
globalized_restart_path.unlink(missing_ok=True)

@classmethod
def setUpClass(cls):
Copilot is powered by AI and may make mistakes. Always verify output.
pass

@classmethod
def setUpClass(cls):
"""
A method that is run before all unit tests in this class.
"""
cls._clean_globalized_restart_artifact()
cls.maxDiff = None
cls.default_job_types = {'conf_opt': True,
'opt': True,
Expand Down Expand Up @@ -1098,6 +1120,7 @@
self.assertEqual(common.get_angle_in_180_range(-270), 90)
self.assertAlmostEqual(common.get_angle_in_180_range(45.5), 45.5, places=7)
self.assertAlmostEqual(common.get_angle_in_180_range(719.9), -0.1, places=7)
self.assertAlmostEqual(common.get_angle_in_180_range(-5.364589, round_to=2), -5.36)

def test_signed_angular_diff(self):
"""Test the signed angular difference between two angles"""
Expand Down Expand Up @@ -1396,10 +1419,7 @@
"""
A function that is run ONCE after all unit tests in this class.
"""
globalized_restart_path = os.path.join(common.ARC_TESTING_PATH, 'restart', '4_globalized_paths',
'restart_paths_globalized.yml')
if os.path.isfile(globalized_restart_path):
os.remove(path=globalized_restart_path)
cls._clean_globalized_restart_artifact()


if __name__ == '__main__':
Expand Down
93 changes: 90 additions & 3 deletions arc/family/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,17 @@ def check_product_isomorphism(products: List['Molecule'],
return False
prods_a = [generate_resonance_structures_safely(mol) or [mol.copy(deep=True)] for mol in products]
prods_b = [spc.mol_list or [spc.mol] for spc in p_species]
# For singlet biradicals (multiplicity forced below the natural value),
# add a copy with the natural multiplicity so template-generated products
# (which use the high-spin default) can match.
for i, spc in enumerate(p_species):
n_rad = spc.mol.get_radical_count()
natural_mult = n_rad + 1
if n_rad and spc.mol.multiplicity < natural_mult:
aug = [m.copy(deep=True) for m in prods_b[i]]
for m in aug:
m.multiplicity = natural_mult
prods_b[i] = prods_b[i] + aug
isomorphic = [False] * len(products)
for i, prod_a in enumerate(prods_a):
for prod_b in prods_b:
Expand Down Expand Up @@ -869,6 +880,82 @@ def get_recipe_actions(groups_as_lines: List[str]) -> List[List[str]]:
return actions


def split_entries(groups_str: str) -> List[str]:
"""Split a groups.py source string into the bodies of every top-level
``entry(...)`` block.

A naive ``re.findall(r'entry\\((.*?)\\)', ..., re.DOTALL)`` is fooled by
``)`` characters that appear inside the entry's string literals (for
example a ``label = "C1(R)(H)(O(OC3(OH)(R'))C2)"`` for Korcek_step2),
causing the entry to be truncated and downstream parsing to miss the
label and the group adjacency list entirely.

This helper does a small character-by-character scan that tracks
parenthesis depth while skipping over single-quoted, double-quoted,
and triple-quoted string literals. The body returned for each entry
is the text between the opening ``entry(`` and the matching ``)`` —
exactly what the original ``re.findall`` was supposed to return.
"""
bodies: List[str] = []
n = len(groups_str)
pos = 0
while True:
marker = groups_str.find('entry(', pos)
if marker < 0:
break
body_start = marker + len('entry(')
# Walk forward from body_start, tracking paren depth and skipping
# over string literals. Start at depth 1 because we're already
# inside the outer ``entry(`` parentheses.
depth = 1
j = body_start
in_string: Optional[str] = None # None | '"' | "'" | '"""' | "'''"
while j < n and depth > 0:
if in_string is None:
# Check for a triple-quoted string opener first.
triple = groups_str[j:j + 3]
if triple in ('"""', "'''"):
in_string = triple
j += 3
continue
ch = groups_str[j]
if ch in ('"', "'"):
in_string = ch
j += 1
continue
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth == 0:
break
j += 1
else:
if len(in_string) == 3:
if groups_str[j:j + 3] == in_string:
in_string = None
j += 3
continue
j += 1
continue
ch = groups_str[j]
if ch == '\\' and j + 1 < n:
# Skip the escape and the escaped character.
j += 2
continue
if ch == in_string:
in_string = None
j += 1
continue
j += 1
if depth != 0:
# Unmatched ``entry(`` — give up rather than mis-attribute.
break
bodies.append(groups_str[body_start:j])
pos = j + 1
return bodies


def get_entries(groups_as_lines: List[str],
entry_labels: List[str],
) -> Dict[str, str]:
Expand All @@ -883,11 +970,11 @@ def get_entries(groups_as_lines: List[str],
Dict[str, str]: The extracted entries, keys are the labels, values are the groups.
"""
groups_str = ''.join(groups_as_lines)
entries = re.findall(r'entry\((.*?)\)', groups_str, re.DOTALL)
entries = split_entries(groups_str)
specific_entries = dict()
for i, entry in enumerate(entries):
label_match = re.search(r'label = "(.*?)"', entry)
group_match = re.search(r'group =(.*?)(?=\w+ =)', entry, re.DOTALL)
label_match = re.search(r'label\s*=\s*"(.*?)"', entry)
group_match = re.search(r'group\s*=(.*?)(?=\w+\s*=)', entry, re.DOTALL)
if label_match is not None and group_match is not None and label_match.group(1) in entry_labels:
specific_entries[label_match.group(1)] = clean_text(group_match.group(1))
if i > 2000:
Expand Down
Loading
Loading