Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
875acf3
Remove Julia installation from install_rmg
alongd Mar 26, 2026
a193706
Minor: Style mod in mapping driver
alongd Mar 17, 2026
a1726ba
Organize teardown in functional and common tests
alongd Apr 18, 2026
dd24ae7
Added Claude to .gitignore
alongd Apr 18, 2026
7de3d32
Added a debug message if fragment mapping fails
alongd Apr 18, 2026
e2565c0
Modifications to CI
alongd Apr 18, 2026
cc2420f
Added a nightly CI for slower tests
alongd Apr 18, 2026
43cc077
Mark slow unit tests in .toml
alongd Apr 18, 2026
853ac7e
Added Reaction.is_unimolecular()
alongd Aug 21, 2023
f4ea9fe
Tests: Reaction.is_unimolecular()
alongd May 15, 2024
d07cb97
Added an atom_order arg to xyz_to_zmat()
alongd Aug 21, 2023
5e2d0f7
Tests: xyz_to_zmat()
alongd Aug 21, 2023
ec0a171
Added 'linear' to ts_adapters_by_rmg_family
alongd Aug 21, 2023
689438c
Added check_ordered_zmats()
alongd Aug 21, 2023
3ea4f13
Tests: check_ordered_zmats()
alongd Aug 21, 2023
91a00d6
Added linear to JobEnum
alongd Aug 21, 2023
6708e3c
Added the Linear TS search job adapter and related utils
alongd May 15, 2024
0d3bf10
Tests: Linear TS Job Adapter and utils
alongd May 15, 2024
ca09160
Added order_xyz_by_atom_map to converter
alongd May 26, 2024
6639a50
Tests: converter order_xyz_by_atom_map()
alongd May 26, 2024
3cfd9c1
Added update_zmat_by_xyz() to zmat
alongd May 26, 2024
926e3c6
Tests: zmat update_zmat_by_xyz()
alongd May 26, 2024
f4a3184
Use the round_to arg in common get_angle_in_180_range()
alongd Jan 1, 2026
10c469e
Tests: use round_to in get_angle_in_180_range()
alongd Jan 1, 2026
0526845
Added anchors to xyz_to_zmat()
alongd Mar 17, 2026
46e43b3
Tests: zmat anchors
alongd Mar 17, 2026
0db8b53
Added converter order_mol_by_atom_map()
alongd Mar 31, 2026
933044f
Added zmat find_smart_anchors()
alongd Mar 31, 2026
7800117
Updated the ts_adapters_by_rmg_family dict
alongd Mar 31, 2026
82eec25
Tests: call self.job_3.execute() in OB tests
alongd Apr 18, 2026
15caba0
Preserve radical sites when perceiving mol from xyz with an existing mol
alongd Mar 31, 2026
6efe85b
Added an iPY notebook to showcase the linear adapter
alongd Apr 18, 2026
89efd48
Docs: Added descriptions of TS search methods implemented in ARC
alongd Apr 18, 2026
a7a7438
Added split_entries() to family
alongd Apr 18, 2026
e14aa04
Minor: Style modifications
alongd Apr 23, 2026
63cb97a
Tests: Added family tests
alongd Apr 23, 2026
6ff493c
Implement ts_adapters_for_unknown_unimolecular() in Scheduler
alongd Apr 23, 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
26 changes: 5 additions & 21 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 All @@ -55,22 +55,6 @@ jobs:
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"
# 1) Bootstrap juliaup non-interactively (-y)
curl -fsSL https://install.julialang.org | sh -s -- -y
# 2) Make juliaup visible *now* (CI shells won’t re-source rc files)
export PATH="$HOME/.juliaup/bin:$PATH"
# 3) Install & select Julia 1.10
juliaup add 1.10
juliaup default 1.10
# 4) Persist for subsequent steps
echo "PATH=$HOME/.juliaup/bin:$PATH" >> "$GITHUB_ENV"
echo "::endgroup::"
julia --version # Check that Julia is installed correctly

- name: Install all extras - CI
shell: micromamba-shell {0}
working-directory: ARC
Expand Down Expand Up @@ -102,7 +86,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 +97,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
87 changes: 87 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ install-all: install

install:
@echo "Installing all external ARC dependencies..."
bash $(DEVTOOLS_DIR)/install_all.sh --rmg-rms
bash $(DEVTOOLS_DIR)/install_all.sh

install-ci:
@echo "Installing all external ARC dependencies for CI (no clean)..."
bash $(DEVTOOLS_DIR)/install_all.sh --no-clean

install-lite:
@echo "Installing ARC's lite version (no external dependencies)..."
bash $(DEVTOOLS_DIR)/install_all.sh --no-ext --rmg-rms
bash $(DEVTOOLS_DIR)/install_all.sh --no-ext

install-pyrdl:
bash $(DEVTOOLS_DIR)/install_pyrdl.sh
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 about 11 hours ago

The best fix is to keep cleanup non-fatal (so existing test behavior is preserved) while making failures observable.
In arc/common_test.py, replace the empty except OSError: pass with handling that records why cleanup failed. The least invasive approach is to print a clear warning message including the path and exception details. This avoids adding imports or changing control flow while satisfying the “non-empty except” requirement and improving debuggability.

Change region:

  • TestCommon._clean_globalized_restart_artifact, around lines 49–52.

No new dependencies are required, and no additional methods/definitions are needed.

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
@@ -48,8 +48,8 @@
         if os.path.isfile(globalized_restart_path):
             try:
                 os.remove(path=globalized_restart_path)
-            except OSError:
-                pass
+            except OSError as e:
+                print(f'Warning: Could not remove test artifact "{globalized_restart_path}": {e}')
 
     @classmethod
     def setUpClass(cls):
EOF
@@ -48,8 +48,8 @@
if os.path.isfile(globalized_restart_path):
try:
os.remove(path=globalized_restart_path)
except OSError:
pass
except OSError as e:
print(f'Warning: Could not remove test artifact "{globalized_restart_path}": {e}')

@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