diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 75deef0616..7b5a39c162 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -51,7 +51,7 @@ env: jobs: build-osx: - runs-on: macos-latest + runs-on: macos-13 # skip scheduled runs from forks if: ${{ !( github.repository != 'ReactionMechanismGenerator/RMG-Py' && github.event_name == 'schedule' ) }} defaults: @@ -63,7 +63,7 @@ jobs: # configures the mamba environment manager and builds the environment - name: Setup Mambaforge Python 3.7 - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: environment-file: environment.yml miniforge-variant: Mambaforge @@ -113,7 +113,7 @@ jobs: # configures the mamba environment manager and builds the environment - name: Setup Mambaforge Python 3.7 - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: environment-file: environment.yml miniforge-variant: Mambaforge @@ -167,7 +167,7 @@ jobs: id: regression-execution timeout-minutes: 60 run: | - for regr_test in aromatics liquid_oxidation nitrogen oxidation sulfur superminimal RMS_constantVIdealGasReactor_superminimal RMS_CSTR_liquid_oxidation RMS_liquidSurface_ch4o2cat fragment RMS_constantVIdealGasReactor_fragment minimal_surface; + for regr_test in aromatics liquid_oxidation nitrogen oxidation sulfur superminimal RMS_constantVIdealGasReactor_superminimal RMS_CSTR_liquid_oxidation RMS_liquidSurface_ch4o2cat fragment RMS_constantVIdealGasReactor_fragment; do if python-jl rmg.py test/regression/"$regr_test"/input.py; then echo "$regr_test" "Executed Successfully" @@ -185,7 +185,7 @@ jobs: # Upload Regression Results as Failed if above step failed - name: Upload Failed Results if: ${{ failure() && steps.regression-execution.conclusion == 'failure' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: failed_regression_results path: | @@ -195,7 +195,7 @@ jobs: - name: Upload Results as Reference # upload the results for scheduled CI (on main) and pushes to main if: ${{ env.REFERENCE_JOB == 'true' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: stable_regression_results path: | @@ -204,7 +204,7 @@ jobs: # Upload Regression Results as Dynamic if Push to non-main Branch - name: Upload Results as Dynamic if: ${{ env.REFERENCE_JOB == 'false' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dynamic_regression_results path: | @@ -215,23 +215,24 @@ jobs: run: mkdir stable_regression_results # Retrieve Stable Results for reference - # Will need to use this -> https://github.com/dawidd6/action-download-artifact + - name : Find ID of Reference Results + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + run_id=$(gh run list -R ReactionMechanismGenerator/RMG-Py --workflow="Continuous Integration" --branch main --limit 1 --json databaseId --jq '.[0].databaseId') + echo "CI_RUN_ID=$run_id" >> $GITHUB_ENV + - name: Retrieve Stable Regression Results if: ${{ env.REFERENCE_JOB == 'false' }} - uses: dsnopek/action-download-artifact@91dda23aa09c68860977dd0ed11d93c0ed3795e7 # see https://github.com/ReactionMechanismGenerator/RMG-Py/pull/2459#issuecomment-1582850815 + uses: actions/download-artifact@v4 with: # this will search for the last successful execution of CI on main and download # the stable regression results - workflow: CI.yml - workflow_conclusion: success - repo: ReactionMechanismGenerator/RMG-Py - branch: main + run-id: ${{ env.CI_RUN_ID }} + repository: ReactionMechanismGenerator/RMG-Py + github-token: ${{ secrets.GITHUB_TOKEN }} name: stable_regression_results path: stable_regression_results - search_artifacts: true # retrieves the last run result, either scheduled daily or on push to main - ensure_latest: true # ensures that the latest run is retrieved - # should result in a set of folders inside stable_regression_results - # each of which has the stable result for that example/test # Regression Testing - Actual Comparisons - name: Regression Tests - Compare to Baseline @@ -242,7 +243,7 @@ jobs: run: | exec 2> >(tee -a regression.stderr >&2) 1> >(tee -a regression.stdout) mkdir -p "test/regression-diff" - for regr_test in aromatics liquid_oxidation nitrogen oxidation sulfur superminimal RMS_constantVIdealGasReactor_superminimal RMS_CSTR_liquid_oxidation fragment RMS_constantVIdealGasReactor_fragment minimal_surface; + for regr_test in aromatics liquid_oxidation nitrogen oxidation sulfur superminimal RMS_constantVIdealGasReactor_superminimal RMS_CSTR_liquid_oxidation fragment RMS_constantVIdealGasReactor_fragment; do echo "" echo "### Regression test $regr_test:" diff --git a/Makefile b/Makefile index c573fc3b47..a2f6b3fed2 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ # ################################################################################ +CC=gcc +CXX=g++ + .PHONY : all minimal main solver check pycheck arkane clean install decython documentation test q2dtor all: pycheck main solver check diff --git a/arkane/ess/adapter.py b/arkane/ess/adapter.py index 9b25b1e2c1..f0a30c932a 100644 --- a/arkane/ess/adapter.py +++ b/arkane/ess/adapter.py @@ -45,10 +45,11 @@ class ESSAdapter(ABC): An abstract ESS Adapter class """ - def __init__(self, path, check_for_errors=True): + def __init__(self, path, check_for_errors=True, scratch_directory=None): self.path = path if check_for_errors: self.check_for_errors() + self.scratch_directory = scratch_directory if scratch_directory is not None else os.path.join(os.path.abspath('.'), str('scratch')) @abstractmethod def check_for_errors(self): @@ -174,7 +175,7 @@ def get_symmetry_properties(self): coordinates, atom_numbers, _ = self.load_geometry() unique_id = '0' # Just some name that the SYMMETRY code gives to one of its jobs # Scratch directory that the SYMMETRY code writes its files in: - scr_dir = os.path.join(os.path.abspath('.'), str('scratch')) + scr_dir = self.scratch_directory if not os.path.exists(scr_dir): os.makedirs(scr_dir) try: diff --git a/arkane/ess/factory.py b/arkane/ess/factory.py index 8e50390093..ca0baee26c 100644 --- a/arkane/ess/factory.py +++ b/arkane/ess/factory.py @@ -61,6 +61,7 @@ def register_ess_adapter(ess: str, def ess_factory(fullpath: str, check_for_errors: bool = True, + scratch_directory: str = None, ) -> Type[ESSAdapter]: """ A factory generating the ESS adapter corresponding to ``ess_adapter``. @@ -106,4 +107,4 @@ def ess_factory(fullpath: str, raise InputError(f'The file at {fullpath} could not be identified as a ' f'Gaussian, Molpro, Orca, Psi4, QChem, or TeraChem log file.') - return _registered_ess_adapters[ess_name](path=fullpath, check_for_errors=check_for_errors) + return _registered_ess_adapters[ess_name](path=fullpath, check_for_errors=check_for_errors, scratch_directory=scratch_directory) diff --git a/arkane/ess/gaussian.py b/arkane/ess/gaussian.py index 2e84d6f231..156823b987 100644 --- a/arkane/ess/gaussian.py +++ b/arkane/ess/gaussian.py @@ -35,7 +35,7 @@ import logging import math import os.path - +import re import numpy as np import rmgpy.constants as constants @@ -309,9 +309,8 @@ def load_energy(self, zpe_scale_factor=1.): with open(self.path, 'r') as f: line = f.readline() while line != '': - if 'SCF Done:' in line: - e_elect = float(line.split()[4]) * constants.E_h * constants.Na + e_elect = float(re.findall(r"SCF Done: E\(.+\) \=\s+[^\s]+", line)[0].split()[-1]) * constants.E_h * constants.Na elect_energy_source = 'SCF' elif ' E2(' in line and ' E(' in line: e_elect = float(line.split()[-1].replace('D', 'E')) * constants.E_h * constants.Na @@ -351,7 +350,6 @@ def load_energy(self, zpe_scale_factor=1.): # G4MP2 calculation without opt and freq calculation # Keyword in Gaussian G4MP2(SP), No zero-point or thermal energies are included. e_elect = float(line.split()[2]) * constants.E_h * constants.Na - # Read the ZPE from the "E(ZPE)=" line, as this is the scaled version. # Gaussian defines the following as # E (0 K) = Elec + E(ZPE), diff --git a/documentation/source/users/rmg/input.rst b/documentation/source/users/rmg/input.rst index 61a599da4a..fbbff65940 100644 --- a/documentation/source/users/rmg/input.rst +++ b/documentation/source/users/rmg/input.rst @@ -49,7 +49,7 @@ by Benson's method. For example, if you wish to use the GRI-Mech 3.0 mechanism [GRIMech3.0]_ as a ThermoLibrary in your model, the syntax will be:: - thermoLibraries = ['primaryThermoLibrary','GRI-Mech3.0'] + thermoLibraries = ['primaryThermoLibrary', 'GRI-Mech3.0'] .. [GRIMech3.0] Gregory P. Smith, David M. Golden, Michael Frenklach, Nigel W. Moriarty, Boris Eiteneer, Mikhail Goldenberg, C. Thomas Bowman, Ronald K. Hanson, Soonho Song, William C. Gardiner, Jr., Vitali V. Lissianski, and Zhiwei Qin http://combustion.berkeley.edu/gri-mech/ @@ -78,7 +78,7 @@ In the following example, the user has created a reaction library with a few additional reactions specific to n-butane, and these reactions are to be used in addition to the Glarborg C3 library:: - reactionLibraries = [('Glarborg/C3',False)], + reactionLibraries = [('Glarborg/C3', False)], The keyword False/True permits user to append all unused reactions (= kept in the edge) from this library to the chemkin file. True means those reactions will be appended. Using just the string inputs would lead to @@ -104,6 +104,23 @@ given in each mechanism, the different mechanisms can have different units. Library. +.. _externallib: + +External Libraries +------------------ +Users may direct RMG to use thermo and/or kinetic libraries which are not included in the RMG database, +e.g., a library a user created that was intentionally saved to a path different than the conventional +RMG-database location. In such cases, the user can specify the full path to the library in the input file:: + + thermoLibraries = ['path/to/your/thermo/library/file.py'] + +or:: + + reactionLibraries = [(path/to/your/kinetic/library/folder/'] + +Combinations in any order of RMG's legacy libraries and users' external libraries are allowed, +and the order in which the libraries are specified is the order in which they are loaded and given priority. + .. _seedmechanism: Seed Mechanisms diff --git a/environment.yml b/environment.yml index 64df68a1c2..b81c13cd9f 100644 --- a/environment.yml +++ b/environment.yml @@ -16,7 +16,7 @@ # made dependency list more explicit (@JacksonBurns). # - October 16, 2023 Switched RDKit and descripatastorus to conda-forge, # moved diffeqpy to pip and (temporarily) removed chemprop -# +# - August 4, 2024 Restricted pyrms to <2 name: rmg_env channels: - defaults @@ -81,14 +81,14 @@ dependencies: - conda-forge::gprof2dot - conda-forge::numdifftools - conda-forge::quantities + - conda-forge::muq + - conda-forge::lpsolve55 + - conda-forge::ringdecomposerlib-python # packages we maintain - - rmg::lpsolve55 - - rmg::muq2 - rmg::pydas >=1.0.3 - rmg::pydqed >=1.0.3 - - rmg::pyrdl - - rmg::pyrms + - rmg::pyrms <2 - rmg::symmetry # packages we would like to stop maintaining (and why) diff --git a/rmgpy/data/kinetics/database.py b/rmgpy/data/kinetics/database.py index 41af9a6b55..91b13e3972 100644 --- a/rmgpy/data/kinetics/database.py +++ b/rmgpy/data/kinetics/database.py @@ -62,6 +62,7 @@ def __init__(self): self.recommended_families = {} self.families = {} self.libraries = {} + self.external_library_labels = {} self.library_order = [] # a list of tuples in the format ('library_label', LibraryType), # where LibraryType is set to either 'Reaction Library' or 'Seed'. self.local_context = { @@ -227,17 +228,18 @@ def load_libraries(self, path, libraries=None): The `path` points to the folder of kinetics libraries in the database, and the libraries should be in files like :file:`/.py`. """ - + self.external_library_labels = dict() if libraries is not None: for library_name in libraries: library_file = os.path.join(path, library_name, 'reactions.py') if os.path.exists(library_name): library_file = os.path.join(library_name, 'reactions.py') - short_library_name = os.path.split(library_name)[-1] + short_library_name = os.path.basename(library_name.rstrip(os.path.sep)) logging.info(f'Loading kinetics library {short_library_name} from {library_name}...') library = KineticsLibrary(label=short_library_name) library.load(library_file, self.local_context, self.global_context) self.libraries[library.label] = library + self.external_library_labels[library_name] = library.label elif os.path.exists(library_file): logging.info(f'Loading kinetics library {library_name} from {library_file}...') library = KineticsLibrary(label=library_name) diff --git a/rmgpy/molecule/draw.py b/rmgpy/molecule/draw.py index 6dd6c8b46b..e18611b92a 100644 --- a/rmgpy/molecule/draw.py +++ b/rmgpy/molecule/draw.py @@ -1644,6 +1644,7 @@ def __init__(self, options=None): self.options = MoleculeDrawer().options.copy() self.options.update({ 'arrowLength': 36, + 'drawReversibleArrow': True }) if options: self.options.update(options) @@ -1744,11 +1745,30 @@ def draw(self, reaction, file_format, path=None): rxn_cr.save() rxn_cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) rxn_cr.set_line_width(1.0) - rxn_cr.move_to(rxn_x + 8, rxn_top + 0.5 * rxn_height) - rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) - rxn_cr.move_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height - 3.0) - rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) - rxn_cr.line_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height + 3.0) + if self.options['drawReversibleArrow'] and reaction.reversible: # draw double harpoons + TOP_HARPOON_Y = rxn_top + (0.5 * rxn_height - 1.75) + BOTTOM_HARPOON_Y = rxn_top + (0.5 * rxn_height + 1.75) + + # Draw top harpoon + rxn_cr.move_to(rxn_x + 8, TOP_HARPOON_Y) + rxn_cr.line_to(rxn_x + arrow_width - 8, TOP_HARPOON_Y) + rxn_cr.move_to(rxn_x + arrow_width - 14, TOP_HARPOON_Y - 3.0) + rxn_cr.line_to(rxn_x + arrow_width - 8, TOP_HARPOON_Y) + + # Draw bottom harpoon + rxn_cr.move_to(rxn_x + arrow_width - 8, BOTTOM_HARPOON_Y) + rxn_cr.line_to(rxn_x + 8, BOTTOM_HARPOON_Y) + rxn_cr.move_to(rxn_x + 14, BOTTOM_HARPOON_Y + 3.0) + rxn_cr.line_to(rxn_x + 8, BOTTOM_HARPOON_Y) + + + else: # draw forward arrow + rxn_cr.move_to(rxn_x + 8, rxn_top + 0.5 * rxn_height) + rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) + rxn_cr.move_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height - 3.0) + rxn_cr.line_to(rxn_x + arrow_width - 8, rxn_top + 0.5 * rxn_height) + rxn_cr.line_to(rxn_x + arrow_width - 14, rxn_top + 0.5 * rxn_height + 3.0) + rxn_cr.stroke() rxn_cr.restore() rxn_x += arrow_width diff --git a/rmgpy/reaction.py b/rmgpy/reaction.py index 7484f70d43..f10e1dcefa 100644 --- a/rmgpy/reaction.py +++ b/rmgpy/reaction.py @@ -1030,15 +1030,15 @@ def generate_reverse_rate_coefficient(self, network_kinetics=False, Tmin=None, T return self.reverse_arrhenius_rate(kf, kunits) elif isinstance(kf, Chebyshev): - Tlist = 1.0 / np.linspace(1.0 / kf.Tmax.value, 1.0 / kf.Tmin.value, 50) - Plist = np.linspace(kf.Pmin.value, kf.Pmax.value, 20) + Tlist = 1.0 / np.linspace(1.0 / kf.Tmax.value_si, 1.0 / kf.Tmin.value_si, 50) + Plist = np.linspace(kf.Pmin.value_si, kf.Pmax.value_si, 20) K = np.zeros((len(Tlist), len(Plist)), float) for Tindex, T in enumerate(Tlist): for Pindex, P in enumerate(Plist): K[Tindex, Pindex] = kf.get_rate_coefficient(T, P) / self.get_equilibrium_constant(T) kr = Chebyshev() - kr.fit_to_data(Tlist, Plist, K, kunits, kf.degreeT, kf.degreeP, kf.Tmin.value, kf.Tmax.value, kf.Pmin.value, - kf.Pmax.value) + kr.fit_to_data(Tlist, Plist, K, kunits, kf.degreeT, kf.degreeP, kf.Tmin.value, kf.Tmax.value, + kf.Pmin.value_si, kf.Pmax.value_si) return kr elif isinstance(kf, PDepArrhenius): diff --git a/rmgpy/rmg/model.py b/rmgpy/rmg/model.py index 1d8c581cc0..1ab45dc67c 100644 --- a/rmgpy/rmg/model.py +++ b/rmgpy/rmg/model.py @@ -1718,7 +1718,12 @@ def add_reaction_library_to_edge(self, reaction_library): num_old_edge_reactions = len(self.edge.reactions) logging.info("Adding reaction library {0} to model edge...".format(reaction_library)) - reaction_library = database.kinetics.libraries[reaction_library] + if reaction_library in database.kinetics.libraries: + reaction_library = database.kinetics.libraries[reaction_library] + elif reaction_library in database.kinetics.external_library_labels: + reaction_library = database.kinetics.libraries[database.kinetics.external_library_labels[reaction_library]] + else: + raise ValueError(f'Library {reaction_library} not found.') rxns = reaction_library.get_library_reactions() for rxn in rxns: diff --git a/test/rmgpy/data/transportTest.py b/test/rmgpy/data/transportTest.py index 9e3b9f71bf..45d7855596 100644 --- a/test/rmgpy/data/transportTest.py +++ b/test/rmgpy/data/transportTest.py @@ -151,9 +151,9 @@ def test_joback(self): [ "acetone", "CC(=O)C", - Length(5.36421, "angstroms"), + Length(5.32979, "angstroms"), Energy(3.20446, "kJ/mol"), - "Epsilon & sigma estimated with Tc=500.53 K, Pc=47.11 bar (from Joback method)", + "Epsilon & sigma estimated with Tc=500.53 K, Pc=48.02 bar (from Joback method)", ], [ "cyclopenta-1,2-diene",