diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1c6ba3e4b..03eee405f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,23 +71,6 @@ jobs: echo "::endgroup::" julia --version # Check that Julia is installed correctly - - name: Set RMG-Py + database in PATH and PYTHONPATH - shell: micromamba-shell {0} - working-directory: ARC - run: | - # bash devtools/install_rmg.sh - echo "RMG_PY_PATH=$(realpath ../RMG-Py)" >> $GITHUB_ENV - echo "RMG_DB_PATH=$(realpath ../RMG-database)" >> $GITHUB_ENV - echo "PATH=$(realpath ../RMG-Py):$PATH" >> $GITHUB_ENV - echo "PYTHONPATH=$(realpath ../RMG-Py):$PYTHONPATH" >> $GITHUB_ENV - - - name: Cache RMG-Py source - uses: actions/cache@v4 - with: - path: RMG-Py - key: ${{ runner.os }}-rmgpy-${{ hashFiles('ARC/devtools/install_rmg.sh') }} - restore-keys: ${{ runner.os }}-rmgpy- - - name: Install all extras - CI shell: micromamba-shell {0} working-directory: ARC diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 1b2b55d583..c6bc80e4f2 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -20,34 +20,17 @@ jobs: path: ARC fetch-depth: 1 - # ── pull RMG sources ──────────────────────────────────────── - - name: Checkout RMG‑Py - uses: actions/checkout@v4 - with: - repository: ReactionMechanismGenerator/RMG-Py - path: RMG-Py - fetch-depth: 1 - - - name: Checkout RMG‑database - uses: actions/checkout@v4 - with: - repository: ReactionMechanismGenerator/RMG-database - path: RMG-database - fetch-depth: 1 - - # ── build‑only env ─────────────────────────────────────────── + # ── rmg_env for Arkane/database availability ─────────────── - name: Set up micromamba (rmg_env) uses: mamba-org/setup-micromamba@v2 with: environment-name: rmg_env - environment-file: RMG-Py/environment.yml + create-args: >- + -c rmg -c conda-forge python=3.9 numpy<2 rmg=3.3.0 connie::symmetry cache-environment: true cache-downloads: true generate-run-shell: false - - name: Build RMG‑Py in rmg_env - run: micromamba run -n rmg_env make -C RMG-Py -j"$(nproc)" - # ── docs env (wrapper shell) ──────────────────────────────── - name: Set up micromamba (arc_env) uses: mamba-org/setup-micromamba@v2 @@ -77,9 +60,8 @@ jobs: # ── build HTML docs ───────────────────────────────────────── - name: Set env vars & Build docs run: | - export RMG_PATH="$GITHUB_WORKSPACE/RMG-Py" - export RMG_DB_PATH="$GITHUB_WORKSPACE/RMG-database" - export PYTHONPATH="$GITHUB_WORKSPACE/RMG-Py:$GITHUB_WORKSPACE/ARC:$PYTHONPATH" + export RMG_DB_PATH="$MAMBA_ROOT_PREFIX/envs/rmg_env/share/RMG-database" + export PYTHONPATH="$GITHUB_WORKSPACE/ARC:$PYTHONPATH" export PATH="$GITHUB_WORKSPACE/ARC:$PATH" make -C ARC/docs html shell: micromamba-shell {0} diff --git a/.github/workflows/update-cache.yml b/.github/workflows/update-cache.yml index 11a8e03477..23e0f40165 100644 --- a/.github/workflows/update-cache.yml +++ b/.github/workflows/update-cache.yml @@ -21,42 +21,6 @@ jobs: - name: Checkout ARC uses: actions/checkout@v4 - # ────────── RMG‑Py ────────── - - name: Cache RMG - id: cache-rmg - uses: actions/cache@v4 - with: - path: RMG-Py - key: ${{ runner.os }}-rmg-main - restore-keys: | - ${{ runner.os }}-rmg- - - name: Checkout RMG-Py - if: steps.cache-rmg.outputs.cache-hit != 'true' - uses: actions/checkout@v4 - with: - repository: ReactionMechanismGenerator/RMG-Py - path: RMG-Py - ref: 55464c54d1fa61b531e865682df598d33718597d - fetch-depth: 1 - - # ────────── RMG‑database ────────── - - name: Cache RMG-database - id: cache-rmg-db - uses: actions/cache@v4 - with: - path: RMG-database - key: ${{ runner.os }}-rmgdb-main - restore-keys: | - ${{ runner.os }}-rmgdb- - - name: Checkout RMG-database - if: steps.cache-rmg-db.outputs.cache-hit != 'true' - uses: actions/checkout@v4 - with: - repository: ReactionMechanismGenerator/RMG-database - path: RMG-database - ref: main - fetch-depth: 1 - # ────────── AutoTST ────────── - name: Cache AutoTST id: cache-autotst diff --git a/.gitignore b/.gitignore index 62485f1faa..9132dc1f86 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ build/* *.log *.xml + +# AI Agent files +AGENTS.md diff --git a/Makefile b/Makefile index 79bfbbd27b..ff5b1e7091 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DEVTOOLS_DIR := devtools .PHONY: all help clean test test-unittests test-functional test-all \ - install-all install-ci install-pyrdl install-rmgdb install-autotst install-gcn \ + install-all install-ci install-pyrdl install-rmg install-rmgdb install-autotst install-gcn \ install-gcn-cpu install-kinbot install-sella install-xtb install-torchani install-ob \ lite check-env compile @@ -28,6 +28,7 @@ help: @echo " install Install all external dependencies" @echo " install-ci Install all external dependencies for CI (no clean)" @echo " install-pyrdl Install PyRDL" + @echo " install-rmg Install RMG (default: packaged, override via RMG_ARGS=...)" @echo " install-rmgdb Install RMG-database" @echo " install-autotst Install AutoTST" @echo " install-gcn Install TS-GCN (GPU)" @@ -75,6 +76,9 @@ install-lite: install-pyrdl: bash $(DEVTOOLS_DIR)/install_pyrdl.sh +install-rmg: + bash $(DEVTOOLS_DIR)/install_rmg.sh $(RMG_ARGS) + install-rmgdb: bash $(DEVTOOLS_DIR)/install_rmgdb.sh diff --git a/arc/family/family.py b/arc/family/family.py index 1b9b1edd46..6683905fbc 100644 --- a/arc/family/family.py +++ b/arc/family/family.py @@ -23,6 +23,23 @@ logger = get_logger() +def get_rmg_db_subpath(*parts: str, must_exist: bool = False) -> str: + """Return a path under the RMG database, handling both source and packaged layouts.""" + if RMG_DB_PATH is None: + if must_exist: + raise FileNotFoundError('RMG_DB_PATH is not set; cannot locate the RMG database.') + return os.path.join('input', *parts) + candidates = [ + os.path.join(RMG_DB_PATH, 'input', *parts), + os.path.join(RMG_DB_PATH, *parts), + ] + if must_exist: + for candidate in candidates: + if os.path.exists(candidate): + return candidate + return candidates[0] + + class ReactionFamily(object): """ A class for representing a reaction family. @@ -72,7 +89,7 @@ def get_groups_file_as_lines(self, consider_arc_families: bool = True) -> List[s Returns: List[str]: The groups file as a list of lines. """ - groups_path = os.path.join(RMG_DB_PATH, 'input', 'kinetics', 'families', self.label, 'groups.py') + groups_path = get_rmg_db_subpath('kinetics', 'families', self.label, 'groups.py', must_exist=True) if not os.path.isfile(groups_path): if consider_arc_families: groups_path = os.path.join(ARC_FAMILIES_PATH, f'{self.label}.py') @@ -605,7 +622,7 @@ def get_rmg_recommended_family_sets() -> Dict[str, str]: Dict[str, str]: The recommended RMG family sets. """ family_sets = dict() - recommended_path = os.path.join(RMG_DB_PATH, 'input', 'kinetics', 'families', 'recommended.py') + recommended_path = get_rmg_db_subpath('kinetics', 'families', 'recommended.py', must_exist=True) if not os.path.isfile(recommended_path): raise FileNotFoundError(f'Could not find the recommended RMG families file at {recommended_path}') with open(recommended_path, 'r') as f: diff --git a/arc/family/family_test.py b/arc/family/family_test.py index c0cf838174..6082d36e48 100644 --- a/arc/family/family_test.py +++ b/arc/family/family_test.py @@ -12,6 +12,7 @@ from arc.family.family import (ReactionFamily, ARC_FAMILIES_PATH, RMG_DB_PATH, + get_rmg_db_subpath, add_labels_to_molecule, check_product_isomorphism, descent_complex_group, @@ -50,8 +51,10 @@ def setUpClass(cls): def test_rmgdb_path(self): """Test finding the RMG-database path""" - self.assertIn('RMG-database', RMG_DB_PATH) self.assertTrue(os.path.isdir(RMG_DB_PATH)) + self.assertTrue(any(token in RMG_DB_PATH for token in ('RMG-database', 'rmgdatabase', 'rmg_database'))) + self.assertTrue(os.path.isdir(os.path.join(RMG_DB_PATH, 'input')) + or os.path.isdir(os.path.join(RMG_DB_PATH, 'kinetics'))) def test_arc_families_path(self): """Test finding the ARC families folder path""" @@ -920,14 +923,14 @@ def test_get_product_num(self): def test_get_reactant_groups_from_template(self): """Test getting reactant groups from a template""" fam_1 = ReactionFamily('6_membered_central_C-C_shift') - groups_path = os.path.join(RMG_DB_PATH, 'input', 'kinetics', 'families', fam_1.label, 'groups.py') + groups_path = get_rmg_db_subpath('kinetics', 'families', fam_1.label, 'groups.py', must_exist=True) with open(groups_path, 'r') as f: groups = f.readlines() reactants_1 = get_reactant_groups_from_template(groups) self.assertEqual(reactants_1, [['1_5_unsaturated_hexane']]) fam_2 = ReactionFamily('H_Abstraction') - groups_path = os.path.join(RMG_DB_PATH, 'input', 'kinetics', 'families', fam_2.label, 'groups.py') + groups_path = get_rmg_db_subpath('kinetics', 'families', fam_2.label, 'groups.py', must_exist=True) with open(groups_path, 'r') as f: groups = f.readlines() reactants_2 = get_reactant_groups_from_template(groups) @@ -936,7 +939,7 @@ def test_get_reactant_groups_from_template(self): self.assertEqual(reactants_2, expected_reactants) fam_3 = ReactionFamily('1,2-Birad_to_alkene') - groups_path = os.path.join(RMG_DB_PATH, 'input', 'kinetics', 'families', fam_3.label, 'groups.py') + groups_path = get_rmg_db_subpath('kinetics', 'families', fam_3.label, 'groups.py', must_exist=True) with open(groups_path, 'r') as f: groups = f.readlines() reactants_3 = get_reactant_groups_from_template(groups) @@ -988,7 +991,7 @@ def test_get_recipe_actions(self): ['LOSE_RADICAL', '*3', '1']]) fam_1 = ReactionFamily('6_membered_central_C-C_shift') - groups_path = os.path.join(RMG_DB_PATH, 'input', 'kinetics', 'families', fam_1.label, 'groups.py') + groups_path = get_rmg_db_subpath('kinetics', 'families', fam_1.label, 'groups.py', must_exist=True) with open(groups_path, 'r') as f: groups = f.readlines() actions = get_recipe_actions(groups) diff --git a/arc/processor.py b/arc/processor.py index e648d73c1d..7231a3ba66 100644 --- a/arc/processor.py +++ b/arc/processor.py @@ -8,6 +8,7 @@ import arc.plotter as plotter from arc.common import ARC_PATH, get_logger, read_yaml_file, save_yaml_file +from arc.imports import settings from arc.level import Level from arc.job.local import execute_command from arc.statmech.factory import statmech_factory @@ -246,11 +247,15 @@ def compare_thermo(species_for_thermo_lib: list, species_thermo_path = os.path.join(output_directory, 'RMG_thermo.yml') save_yaml_file(path=species_thermo_path, content=[{'label': spc.label, 'adjlist': spc.mol.copy(deep=True).to_adjacency_list()} for spc in species_for_thermo_lib]) + env_name = settings.get('RMG_ENV_NAME', 'rmg_env') + rmg_db_path = settings.get('RMG_DB_PATH') or "" commands = ['bash -lc "set -euo pipefail; ' + f'export RMG_DB_PATH=\\"{rmg_db_path}\\"; ' + f'export RMG_DATABASE=\\"{rmg_db_path}\\"; ' 'if command -v micromamba >/dev/null 2>&1; then ' - f' micromamba run -n rmg_env python {THERMO_SCRIPT_PATH} {species_thermo_path}; ' + f' micromamba run -n {env_name} python {THERMO_SCRIPT_PATH} {species_thermo_path}; ' 'elif command -v conda >/dev/null 2>&1 || command -v mamba >/dev/null 2>&1; then ' - f' conda run -n rmg_env python {THERMO_SCRIPT_PATH} {species_thermo_path}; ' + f' conda run -n {env_name} python {THERMO_SCRIPT_PATH} {species_thermo_path}; ' 'else ' ' echo \'❌ Micromamba/Mamba/Conda required\' >&2; exit 1; ' 'fi"', @@ -300,7 +305,8 @@ def compare_rates(rxns_for_kinetics_lib: list, 'family': rxn.family, } for rxn in rxns_for_kinetics_lib], ) - env_name = 'rmg_env' + env_name = settings.get('RMG_ENV_NAME', 'rmg_env') + rmg_db_path = settings.get('RMG_DB_PATH') or "" shell_script = f"""if command -v micromamba &> /dev/null; then eval "$(micromamba shell hook --shell=bash)" micromamba activate {env_name} @@ -313,6 +319,8 @@ def compare_rates(rxns_for_kinetics_lib: list, else exit 1 fi +export RMG_DB_PATH="{rmg_db_path}" +export RMG_DATABASE="{rmg_db_path}" python {KINETICS_SCRIPT_PATH} {reactions_kinetics_path} > >(tee -a stdout.log) 2> >(tee -a stderr.log >&2) """ o, e = execute_command(command=shell_script, diff --git a/arc/settings/settings.py b/arc/settings/settings.py index 8be7b76cd7..ea2c90a9cc 100644 --- a/arc/settings/settings.py +++ b/arc/settings/settings.py @@ -5,6 +5,7 @@ Any definitions made to the local file will take precedence over this file. """ +import glob import os import string import sys @@ -308,8 +309,25 @@ def find_executable(env_name, executable_name='python'): os.path.join(home, 'anaconda3', 'envs', env_name, 'bin', executable_name), os.path.join(home, 'miniconda3', 'envs', env_name, 'bin', executable_name), os.path.join(home, '.conda', 'envs', env_name, 'bin', executable_name), + os.path.join(home, 'micromamba', 'envs', env_name, 'bin', executable_name), + os.path.join(home, '.micromamba', 'envs', env_name, 'bin', executable_name), + os.path.join(home, '.local', 'share', 'mamba', 'envs', env_name, 'bin', executable_name), os.path.join('/Local/ce_dana', 'anaconda3', 'envs', env_name, 'bin', executable_name), ] + mamba_root = os.getenv('MAMBA_ROOT_PREFIX') + if mamba_root: + candidate_paths.append(os.path.join(mamba_root, 'envs', env_name, 'bin', executable_name)) + conda_prefix = os.getenv('CONDA_PREFIX') + if conda_prefix: + candidate_paths.append(os.path.join(os.path.dirname(conda_prefix), 'envs', env_name, 'bin', executable_name)) + conda_exe = os.getenv('CONDA_EXE') + if conda_exe: + conda_base = os.path.dirname(os.path.dirname(conda_exe)) + candidate_paths.append(os.path.join(conda_base, 'envs', env_name, 'bin', executable_name)) + conda_envs_path = os.getenv('CONDA_ENVS_PATH') + if conda_envs_path: + for path in conda_envs_path.split(os.pathsep): + candidate_paths.append(os.path.join(path, env_name, 'bin', executable_name)) for path in candidate_paths: if os.path.isfile(path): return path @@ -320,14 +338,40 @@ def find_executable(env_name, executable_name='python'): TS_GCN_PYTHON = find_executable('ts_gcn') AUTOTST_PYTHON = find_executable('tst_env') ARC_PYTHON = find_executable('arc_env') +RMG_ENV_NAME = 'rmg_env' RMG_PYTHON = find_executable('rmg_env') XTB = find_executable('xtb_env', 'xtb') # Set RMG_DB_PATH with fallback methods rmg_db_candidates, rmg_candidates = list(), list() + +def add_rmg_db_candidates(prefix: str) -> None: + """Add RMG-database candidates relative to a conda/mamba env prefix.""" + if not prefix: + return + rmg_db_candidates.extend([ + os.path.join(prefix, 'share', 'RMG-database'), + os.path.join(prefix, 'share', 'rmg-database'), + os.path.join(prefix, 'share', 'rmg', 'database'), + os.path.join(prefix, 'share', 'rmg_database'), + os.path.join(prefix, 'share', 'RMG_database'), + os.path.join(prefix, 'share', 'rmgdatabase'), + os.path.join(prefix, 'share', 'RMGdatabase'), + ]) + rmg_db_candidates.extend(glob.glob(os.path.join(prefix, 'lib', 'python*', 'site-packages', 'RMG-database'))) + rmg_db_candidates.extend(glob.glob(os.path.join(prefix, 'lib', 'python*', 'site-packages', 'rmg-database'))) + rmg_db_candidates.extend(glob.glob(os.path.join(prefix, 'lib', 'python*', 'site-packages', 'rmg_database'))) + rmg_db_candidates.extend(glob.glob(os.path.join(prefix, 'lib', 'python*', 'site-packages', 'RMG_database'))) + rmg_db_candidates.extend(glob.glob(os.path.join(prefix, 'lib', 'python*', 'site-packages', 'rmgdatabase'))) + rmg_db_candidates.extend(glob.glob(os.path.join(prefix, 'lib', 'python*', 'site-packages', 'RMGdatabase'))) + for candidate in glob.glob(os.path.join(prefix, 'share', '**', 'recommended.py'), recursive=True): + if candidate.endswith(os.path.join('input', 'kinetics', 'families', 'recommended.py')): + rmg_db_candidates.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(candidate))))) + # Use exported RMG_PATH & RMG_DB_PATH if available -exported_rmg_path, exported_rmg_db_path = os.getenv("RMG_PATH"), os.getenv("RMG_DB_PATH") +exported_rmg_path = os.getenv("RMG_PATH") +exported_rmg_db_path = os.getenv("RMG_DB_PATH") or os.getenv("RMG_DATABASE") if exported_rmg_path: rmg_candidates.append(exported_rmg_path) if exported_rmg_db_path: @@ -339,7 +383,7 @@ def find_executable(env_name, executable_name='python'): rmg_db_candidates.append(os.path.join(gw, 'RMG-database')) for python_path in sys.path: - if 'RMG-database' in python_path: + if 'RMG-database' in python_path or 'rmgdatabase' in python_path or 'rmg_database' in python_path: rmg_db_candidates.append(python_path) if 'RMG-Py' in python_path: rmg_db_candidates.append(os.path.join(os.path.dirname(python_path), 'RMG-database')) @@ -348,9 +392,23 @@ def find_executable(env_name, executable_name='python'): if 'RMG-Py' in p: rmg_candidates.append(p) rmg_db_candidates.append(os.path.join(os.path.dirname(p), 'RMG-database')) - if 'RMG-database' in p: + if 'RMG-database' in p or 'rmgdatabase' in p or 'rmg_database' in p: rmg_db_candidates.append(p) +add_rmg_db_candidates(os.path.dirname(os.path.dirname(sys.executable))) +if RMG_PYTHON: + add_rmg_db_candidates(os.path.dirname(os.path.dirname(RMG_PYTHON))) +if os.getenv('MAMBA_ROOT_PREFIX'): + add_rmg_db_candidates(os.path.join(os.getenv('MAMBA_ROOT_PREFIX'), 'envs', 'rmg_env')) +if os.getenv('CONDA_PREFIX'): + add_rmg_db_candidates(os.path.join(os.path.dirname(os.getenv('CONDA_PREFIX')), 'envs', 'rmg_env')) +if os.getenv('CONDA_EXE'): + conda_base = os.path.dirname(os.path.dirname(os.getenv('CONDA_EXE'))) + add_rmg_db_candidates(os.path.join(conda_base, 'envs', 'rmg_env')) +if os.getenv('CONDA_ENVS_PATH'): + for path in os.getenv('CONDA_ENVS_PATH').split(os.pathsep): + add_rmg_db_candidates(os.path.join(path, 'rmg_env')) + rmg_candidates.extend([ os.path.join(home, 'Code', 'RMG-Py'), os.path.join(home, 'runner', 'work', 'ARC', 'ARC', 'RMG-Py') diff --git a/arc/statmech/arkane.py b/arc/statmech/arkane.py index 15b9eeed11..607ad91b61 100644 --- a/arc/statmech/arkane.py +++ b/arc/statmech/arkane.py @@ -24,7 +24,8 @@ from arc.species.species import ARCSpecies -RMG_DB_PATH, RMG_PATH = settings['RMG_DB_PATH'], settings['RMG_PATH'] +RMG_DB_PATH = settings['RMG_DB_PATH'] +RMG_ENV_NAME = settings.get('RMG_ENV_NAME', 'rmg_env') logger = get_logger() @@ -445,12 +446,15 @@ def parse_arkane_thermo_output(self, statmech_dir: str) -> None: clean_output_directory(os.path.join(self.output_directory, 'Species', species.label)) script_path = os.path.join(ARC_PATH, 'arc', 'scripts', 'save_arkane_thermo.py') + rmg_db_path = RMG_DB_PATH or "" commands = [f'cd {statmech_dir}', 'bash -lc "set -euo pipefail; ' + f'export RMG_DB_PATH=\\"{rmg_db_path}\\"; ' + f'export RMG_DATABASE=\\"{rmg_db_path}\\"; ' 'if command -v micromamba >/dev/null 2>&1; then ' - f' micromamba run -n rmg_env python {script_path}; ' + f' micromamba run -n {RMG_ENV_NAME} python {script_path}; ' 'elif command -v conda >/dev/null 2>&1 || command -v mamba >/dev/null 2>&1; then ' - f' conda run -n rmg_env python {script_path}; ' + f' conda run -n {RMG_ENV_NAME} python {script_path}; ' 'else ' ' echo \'❌ Micromamba/Mamba/Conda required\' >&2; exit 1; ' 'fi"', @@ -514,16 +518,18 @@ def run_arkane(statmech_dir: str) -> None: if not os.path.isfile(input_file): logger.error(f'Cannot run Arkane in {statmech_dir} because it does not contain an input.py file.') return - env_name = 'rmg_env' - arkane_cmd = f'python "{RMG_PATH}/Arkane.py" input.py' + rmg_db_path = RMG_DB_PATH or "" + arkane_cmd = 'python -m arkane input.py' arkane_cmd += ' 2> >(tee -a stderr.log >&2) | tee -a stdout.log' shell_script = rf'''bash -lc 'set -euo pipefail cd "{statmech_dir}" +export RMG_DB_PATH="{rmg_db_path}" +export RMG_DATABASE="{rmg_db_path}" if command -v micromamba >/dev/null 2>&1; then - micromamba run -n {env_name} {arkane_cmd} + micromamba run -n {RMG_ENV_NAME} {arkane_cmd} elif command -v conda >/dev/null 2>&1 || command -v mamba >/dev/null 2>&1; then - conda run -n {env_name} {arkane_cmd} + conda run -n {RMG_ENV_NAME} {arkane_cmd} else echo "❌ Micromamba/Mamba/Conda required" >&2 exit 1 @@ -710,6 +716,8 @@ def get_arkane_model_chemistry(sp_level: 'Level', return f"LevelOfTheory(method='{sp_level.method}',software='gaussian')" qm_corr_file = os.path.join(RMG_DB_PATH, 'input', 'quantum_corrections', 'data.py') + if not os.path.isfile(qm_corr_file): + qm_corr_file = os.path.join(RMG_DB_PATH, 'quantum_corrections', 'data.py') atom_energies_start = "atom_energies = {" atom_energies_end = "pbac = {" @@ -768,6 +776,8 @@ def check_arkane_bacs(sp_level: 'Level', bool: True if both AECs and BACs are available, False otherwise. """ qm_corr_file = os.path.join(RMG_DB_PATH, 'input', 'quantum_corrections', 'data.py') + if not os.path.isfile(qm_corr_file): + qm_corr_file = os.path.join(RMG_DB_PATH, 'quantum_corrections', 'data.py') atom_energies_start = "atom_energies = {" atom_energies_end = "pbac = {" diff --git a/arc/statmech/arkane_test.py b/arc/statmech/arkane_test.py index 3e7bac2f22..e28d5516a5 100644 --- a/arc/statmech/arkane_test.py +++ b/arc/statmech/arkane_test.py @@ -16,6 +16,7 @@ from arc.statmech.adapter import StatmechEnum from arc.statmech.arkane import ArkaneAdapter from arc.statmech.arkane import _level_to_str, _section_contains_key, get_arkane_model_chemistry +from arc.imports import settings class TestEnumerationClasses(unittest.TestCase): @@ -108,8 +109,22 @@ def test__str__(self): def test_run_statmech_using_molecular_properties(self): """Test running statmech using molecular properties.""" self.arkane_3.compute_thermo() - self.assertTrue(os.path.isfile(os.path.join(ARC_PATH, 'arc', 'testing', 'arkane_tests_delete', 'calcs_3', - 'statmech', 'thermo', 'plots', 'iC3H7.pdf'))) + plot_path = os.path.join(ARC_PATH, 'arc', 'testing', 'arkane_tests_delete', 'calcs_3', + 'statmech', 'thermo', 'plots', 'iC3H7.pdf') + if not os.path.isfile(plot_path): + log_dir = os.path.dirname(os.path.dirname(plot_path)) + stdout_log = os.path.join(log_dir, 'stdout.log') + stderr_log = os.path.join(log_dir, 'stderr.log') + stdout_text = '' + stderr_text = '' + if os.path.isfile(stdout_log): + with open(stdout_log, 'r') as f: + stdout_text = f.read() + if os.path.isfile(stderr_log): + with open(stderr_log, 'r') as f: + stderr_text = f.read() + self.fail(f'Arkane did not generate {plot_path}.\nstdout.log:\n{stdout_text}\nstderr.log:\n{stderr_text}') + self.assertTrue(os.path.isfile(plot_path)) self.assertAlmostEqual(self.ic3h7.e0, 6.75565e+07) def test_level_to_str(self): @@ -123,7 +138,11 @@ def test_level_to_str(self): def test_section_contains_key(self): """Test the _section_contains_key function""" - file_path = os.path.join(os.path.dirname(ARC_PATH), 'RMG-database', 'input', 'quantum_corrections', 'data.py') + rmg_db_path = settings.get('RMG_DB_PATH') + file_path = os.path.join(rmg_db_path, 'input', 'quantum_corrections', 'data.py') + if not os.path.isfile(file_path): + file_path = os.path.join(rmg_db_path, 'quantum_corrections', 'data.py') + self.assertTrue(os.path.isfile(file_path), f'RMG quantum corrections file not found at {file_path}') self.assertTrue(_section_contains_key(file_path=file_path, section_start="atom_energies = {", section_end="pbac = {", diff --git a/devtools/install_rmg.sh b/devtools/install_rmg.sh index a2e0d421d3..15cec94b86 100644 --- a/devtools/install_rmg.sh +++ b/devtools/install_rmg.sh @@ -6,30 +6,38 @@ echo ">>> Starting RMG-Py installer..." ############################################################################### # CONFIGURATION ############################################################################### -# default values # default values ──────────────────────────────────────────────────────────── -MODE=path # {path|pip|conda} +MODE=package # {package|source} +SOURCE_MODE=path # {path|pip|conda} (only used when MODE=source) INSTALL_RMS=false USE_SSH=false ENV_NAME="rmg_env" +RMG_VERSION="${RMG_VERSION:-3.3.0}" -TEMP=$(getopt -o prsch --long pip,conda,rms,ssh,help -- "$@") +TEMP=$(getopt -o prschS --long pip,conda,rms,ssh,source,help -- "$@") [[ $? -eq 0 ]] || { echo "Flag parsing failed"; exit 1; } eval set -- "$TEMP" while true; do case "$1" in - -p|--pip) MODE=pip; shift ;; - -c|--conda) MODE=conda; shift ;; + -p|--pip) MODE=source; SOURCE_MODE=pip; shift ;; + -c|--conda) MODE=source; SOURCE_MODE=conda; shift ;; -r|--rms) INSTALL_RMS=true; shift ;; -s|--ssh) USE_SSH=true; shift ;; + -S|--source) MODE=source; shift ;; -h|--help) cat <>> Updating existing environment: $ENV_NAME" + $COMMAND_PKG install -n "$ENV_NAME" -y -c rmg -c conda-forge \ + "python=3.9" "conda-forge::numpy>=1.10.0,<2" "blas=*=openblas" "conda-forge::numdifftools" \ + "rmg=${RMG_VERSION}" "connie::symmetry" + else + echo ">>> Creating new environment: $ENV_NAME" + $COMMAND_PKG create -n "$ENV_NAME" -y -c rmg -c conda-forge \ + "python=3.9" "conda-forge::numpy>=1.10.0,<2" "blas=*=openblas" "conda-forge::numdifftools" \ + "rmg=${RMG_VERSION}" "connie::symmetry" + fi +fi + ############################################################################### # Paths and clones ############################################################################### @@ -79,43 +104,44 @@ clone_repo () { git clone --depth 1 "$url" "$repo_name" } -# ---------- actual clones --------------------------------------------------- -clone_repo RMG-Py \ - git@github.com:ReactionMechanismGenerator/RMG-Py.git \ - https://github.com/ReactionMechanismGenerator/RMG-Py.git +if [[ "$MODE" == "source" ]]; then + # ---------- actual clones --------------------------------------------------- + clone_repo RMG-Py \ + git@github.com:ReactionMechanismGenerator/RMG-Py.git \ + https://github.com/ReactionMechanismGenerator/RMG-Py.git -clone_repo RMG-database \ - git@github.com:ReactionMechanismGenerator/RMG-database.git \ - https://github.com/ReactionMechanismGenerator/RMG-database.git + clone_repo RMG-database \ + git@github.com:ReactionMechanismGenerator/RMG-database.git \ + https://github.com/ReactionMechanismGenerator/RMG-database.git -export RMG_PY_PATH=$CLONE_ROOT/RMG-Py -export RMG_DB_PATH=$CLONE_ROOT/RMG-database + export RMG_PY_PATH=$CLONE_ROOT/RMG-Py + export RMG_DB_PATH=$CLONE_ROOT/RMG-database + ############################################################################### + # CREATE OR UPDATE rmg_env + ############################################################################### + cd "$RMG_PY_PATH" -############################################################################### -# CREATE OR UPDATE rmg_env -############################################################################### -cd "$RMG_PY_PATH" + if [[ ! -f environment.yml ]]; then + echo "❌ environment.yml not found in RMG-Py directory." + exit 1 + fi -if [[ ! -f environment.yml ]]; then - echo "❌ environment.yml not found in RMG-Py directory." - exit 1 -fi + if $COMMAND_PKG env list | awk '{print $1}' | grep -qx "$ENV_NAME"; then + echo ">>> Updating existing environment: $ENV_NAME" + $COMMAND_PKG env update -n "$ENV_NAME" -f environment.yml --prune --strict-channel-priority + else + echo ">>> Creating new environment: $ENV_NAME" + $COMMAND_PKG env create -n "$ENV_NAME" -f environment.yml -y --strict-channel-priority + fi -if $COMMAND_PKG env list | awk '{print $1}' | grep -qx "$ENV_NAME"; then - echo ">>> Updating existing environment: $ENV_NAME" - $COMMAND_PKG env update -n "$ENV_NAME" -f environment.yml --prune --strict-channel-priority -else - echo ">>> Creating new environment: $ENV_NAME" - $COMMAND_PKG env create -n "$ENV_NAME" -f environment.yml -y --strict-channel-priority + ############################################################################### + # COMPILE RMG + ############################################################################### + echo "🔧 Compiling RMG-Py..." + $COMMAND_PKG run -n "$ENV_NAME" make -j"$(nproc)" fi -############################################################################### -# COMPILE RMG -############################################################################### -echo "🔧 Compiling RMG-Py..." -$COMMAND_PKG run -n "$ENV_NAME" make -j"$(nproc)" - ############################################################################### # Inject (PY)PATH hooks into any conda-like env @@ -162,51 +188,53 @@ EOF ############################################################################### -# UPDATE SHELL PATH +# UPDATE SHELL PATH (source installs only) ############################################################################### -case "$SHELL" in - */zsh) RC=~/.zshrc ;; - *) RC=~/.bashrc ;; -esac +if [[ "$MODE" == "source" ]]; then + case "$SHELL" in + */zsh) RC=~/.zshrc ;; + *) RC=~/.bashrc ;; + esac -if [[ ! -f "$RC" ]]; then - echo "❌ Shell configuration file not found: $RC" - exit 1 -fi + if [[ ! -f "$RC" ]]; then + echo "❌ Shell configuration file not found: $RC" + exit 1 + fi -ACTIVE_RE="^[[:space:]]*[^#].*${RMG_PY_PATH//\//\\/}" # uncommented, contains path -COMMENT_RE="^[[:space:]]*#.*${RMG_PY_PATH//\//\\/}" # commented-out, contains path -NEW_LINE='export PATH="$PATH:'"$RMG_PY_PATH"'"' + ACTIVE_RE="^[[:space:]]*[^#].*${RMG_PY_PATH//\//\\/}" # uncommented, contains path + COMMENT_RE="^[[:space:]]*#.*${RMG_PY_PATH//\//\\/}" # commented-out, contains path + NEW_LINE='export PATH="$PATH:'"$RMG_PY_PATH"'"' -# If PATH_ADD is true, add RMG-Py to PATH -if [ "$MODE" == path ]; then - if grep -Eq "$ACTIVE_RE" "$RC"; then - printf 'ℹ️ RMG-Py already active in %s\n' "$RC" + # If PATH_ADD is true, add RMG-Py to PATH + if [ "$SOURCE_MODE" == path ]; then + if grep -Eq "$ACTIVE_RE" "$RC"; then + printf 'ℹ️ RMG-Py already active in %s\n' "$RC" - elif grep -Eq "$COMMENT_RE" "$RC"; then - printf '✅ Found commented entry; adding new active line\n' - printf '\n# RMG-Py added on %s\n%s\n' "$(date +%F)" "$NEW_LINE" >> "$RC" + elif grep -Eq "$COMMENT_RE" "$RC"; then + printf '✅ Found commented entry; adding new active line\n' + printf '\n# RMG-Py added on %s\n%s\n' "$(date +%F)" "$NEW_LINE" >> "$RC" + else + printf '✅ No entry found; adding new active line\n' + printf '\n# RMG-Py added on %s\n%s\n' "$(date +%F)" "$NEW_LINE" >> "$RC" + fi + elif [ "$SOURCE_MODE" == conda ]; then + # conda envs already have the RMG_PY_PATH in PATH, so no need to add it + add_rmg_hooks "$ENV_NAME" else - printf '✅ No entry found; adding new active line\n' - printf '\n# RMG-Py added on %s\n%s\n' "$(date +%F)" "$NEW_LINE" >> "$RC" + # pip install + $COMMAND_PKG run -n "$ENV_NAME" pip install -e "$RMG_PY_PATH" + echo "📦 RMG-Py installed via pip in $ENV_NAME" fi -elif [ "$MODE" == conda ]; then - # conda envs already have the RMG_PY_PATH in PATH, so no need to add it - add_rmg_hooks "$ENV_NAME" -else - # pip install - $COMMAND_PKG run -n "$ENV_NAME" pip install -e "$RMG_PY_PATH" - echo "📦 RMG-Py installed via pip in $ENV_NAME" -fi -ARC_ENV=arc_env -if [[ "$MODE" == "conda" ]]; then - echo "📦 Adding RMG hooks ARC to $ARC_ENV" - add_rmg_hooks "$ARC_ENV" -else - echo "ℹ️ Skipping ARC hooks as ARC is not cloned or not in conda mode." + ARC_ENV=arc_env + if [[ "$SOURCE_MODE" == "conda" ]]; then + echo "📦 Adding RMG hooks ARC to $ARC_ENV" + add_rmg_hooks "$ARC_ENV" + else + echo "ℹ️ Skipping ARC hooks as ARC is not cloned or not in conda mode." + fi fi @@ -214,6 +242,16 @@ fi # INSTALL JULIA & RMS ############################################################################### if [ "$INSTALL_RMS" = true ]; then + if [[ -z "${RC:-}" ]]; then + case "$SHELL" in + */zsh) RC=~/.zshrc ;; + *) RC=~/.bashrc ;; + esac + if [[ ! -f "$RC" ]]; then + echo "❌ Shell configuration file not found: $RC" + exit 1 + fi + fi echo "📦 Installing RMS (Reaction Mechanism Simulator) in $ENV_NAME" # Check if juliaup is installed - juliaup &> /dev/null if ! command -v juliaup &> /dev/null; then @@ -288,13 +326,14 @@ EOF )); using ReactionMechanismSimulator; Pkg.instantiate(); - ' || echo "RMS install error – continuing anyway ¯\_(ツ)_/¯" # conda-run executes in env :contentReference[oaicite:1]{index=1} + ' || echo "RMS install error – continuing anyway ¯\_(ツ)_/¯" # conda-run executes in env echo "Checking if RMS is installed..." -$COMMAND_PKG run -n "$ENV_NAME" python - <<'PY' +ENV_NAME="$ENV_NAME" $COMMAND_PKG run -n "$ENV_NAME" python - <<'PY' from juliacall import Main +import os import sys pk_ok = Main.seval('Base.identify_package("ReactionMechanismSimulator") !== nothing') -print("RMS visible to Python in rmg_env" if pk_ok else "RMS NOT found") +print(f"RMS visible to Python in {os.environ.get('ENV_NAME', 'rmg_env')}" if pk_ok else "RMS NOT found") sys.exit(0 if pk_ok else 1) PY fi diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 66d1ef4a04..9dffbefcc0 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -54,11 +54,39 @@ Install dependencies conda activate arc_env -- Install the latest **DEVELOPER** version of RMG (which has Arkane). - It is recommended to follow RMG's `Developer installation by source using Anaconda - `_ instructions. - Make sure to add RMG-Py to your PATH and PYTHONPATH variables as explained in RMG's documentation. +- Install RMG (choose one option): + + - Use the installer script via Make (recommended). Default is the packaged release:: + + make install-rmg + + To install from source instead:: + + make install-rmg RMG_ARGS=--source + + Additional source options (examples):: + + make install-rmg RMG_ARGS="--source --conda" + make install-rmg RMG_ARGS="--source --pip" + + Optional extras (examples):: + + make install-rmg RMG_ARGS="--rms" + make install-rmg RMG_ARGS="--source --rms --ssh" + + See ``devtools/install_rmg.sh --help`` for all flags. + + - Source install RMG-Py + RMG-database manually, then set `RMG_PY_PATH` and `RMG_DB_PATH` in your shell or + `.arc/settings.py`. + + - Install the packaged RMG release (RMG-Py + RMG-database + Arkane) into a dedicated environment:: + + mamba create -n rmg_env -c rmg -c conda-forge python=3.9 conda-forge::numpy>=1.10.0,<2 \ + blas=*=openblas conda-forge::numdifftools rmg=3.3.0 connie::symmetry + + ARC runs Arkane automatically for statmech from this environment. + ARC will locate the RMG database inside `rmg_env` by default. If you keep the database elsewhere, + set `RMG_DB_PATH` in your shell or `.arc/settings.py`. - Type ``make install-all`` under the ARC repository folder to install the following 3rd party repositories: `AutoTST `_ (`West et al. `_), diff --git a/docs/source/running.rst b/docs/source/running.rst index 92c397bb7d..e9e0469600 100644 --- a/docs/source/running.rst +++ b/docs/source/running.rst @@ -109,4 +109,24 @@ folder. Various :ref:`standalone tools ` in an iPython format are also av demonstrating different utilizations of the API. Users are of course directed to read :ref:`ARC's API `. +.. _arkane-standalone: + +Running Arkane independently +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +ARC runs Arkane automatically for statmech, but you can also run Arkane on its own. +Make sure RMG-Py and the RMG database are available, then run Arkane with your input file. + +Conda install (recommended):: + + conda run -n rmg_env python -m arkane input.py + +Source install:: + + export RMG_PY_PATH=/path/to/RMG-Py + export RMG_DB_PATH=/path/to/RMG-database + python -m arkane input.py + +See the `Arkane`_ documentation for input file details. + .. include:: links.txt