From 7a8977bde4fd7f80df7ac39347c6a2e130e8fe83 Mon Sep 17 00:00:00 2001 From: Richard West Date: Mon, 14 Sep 2026 16:11:39 -0400 Subject: [PATCH 1/2] Add warnings for auto-database feature. --- documentation/source/users/rmg/input.rst | 6 +++ rmgpy/data/auto_database.py | 57 ++++++++++++++++++++++++ test/database/databaseTest.py | 27 +++++++++-- test/rmgpy/data/autoDatabaseTest.py | 54 ++++++++++++++++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/documentation/source/users/rmg/input.rst b/documentation/source/users/rmg/input.rst index 16dc4ffce5..faae3ab42e 100644 --- a/documentation/source/users/rmg/input.rst +++ b/documentation/source/users/rmg/input.rst @@ -289,6 +289,12 @@ the full job. .. note:: The ``'auto'`` keyword is opt-in. If you do not use it you must list every library explicitly. +.. warning:: + Auto-selection embeds the RMG team's recommendations, but it is a heuristic keyed on a + small number of coarse features (the elements present, the phase, whether a surface is + present, and the maximum reactor temperature). It should be treated as an **initial + suggestion** to be reviewed and refined for your system, not as a substitute for + inspecting the database. .. _species_list: diff --git a/rmgpy/data/auto_database.py b/rmgpy/data/auto_database.py index c052ca57f6..22ac0358ac 100644 --- a/rmgpy/data/auto_database.py +++ b/rmgpy/data/auto_database.py @@ -48,10 +48,12 @@ from rmgpy.rmg.reactionmechanismsimulator_reactors import ( ConstantTLiquidSurfaceReactor as RMSLiqSurf, ConstantTVLiquidReactor as RMSLiq, + ConstantVIdealGasReactor as RMSConstV, ) except ImportError: RMSLiqSurf = None RMSLiq = None + RMSConstV = None # Values used in input files to request auto-selection AUTO = 'auto' @@ -65,6 +67,10 @@ # Elements that trigger the metal/electrochem chemistry set (currently only Li) ELECTROCHEM_ELEMENTS = {'Li'} +# Elements for which at least one chemistry set exists. Derived from the sets above so it +# cannot drift out of step with determine_chemistry_sets(). +COVERED_ELEMENTS = {'C', 'H', 'O', 'N', 'S', 'X'} | HALOGEN_ELEMENTS | ELECTROCHEM_ELEMENTS + class ChemistrySet(str, Enum): """Named chemistry sets defined in recommended_libraries.yml.""" @@ -261,6 +267,55 @@ def determine_chemistry_sets(profile: ChemistryProfile, return sets +def warn_about_coverage(profile: ChemistryProfile, + reaction_systems: list, + pah_libs_requested: bool = False, + ) -> None: + """ + Warn the user where the detected chemistry falls outside the preset regimes, so that + a silently incomplete selection is visible in the log. + + The selection is a heuristic keyed on a handful of coarse features, so there are + systems it cannot recognise. Each warning below names one such case and says what the + user should do about it. + + Args: + profile: ChemistryProfile instance. + reaction_systems: list of reactor system objects. + pah_libs_requested: bool, True if user included the keyword. + """ + uncovered = sorted(profile.elements_present - COVERED_ELEMENTS) + if uncovered: + logging.warning( + f' Auto-selection found no chemistry set for the element(s) ' + f'{", ".join(uncovered)}. No libraries specific to this chemistry were ' + f'selected; add any relevant libraries to the database() block manually.' + ) + + if RMSConstV is not None and any(isinstance(r, RMSConstV) for r in reaction_systems): + logging.warning( + f' An adiabatic reactor is present, so the detected maximum temperature ' + f'({profile.max_temperature:.0f} K) is the initial temperature and the true ' + f'peak temperature will be higher. Temperature-gated library sets were ' + f'evaluated at the initial temperature; if the peak is expected to exceed ' + f'{CH_PYROLYSIS_T_THRESHOLD:.0f} K, consider requesting the high-temperature ' + f'libraries explicitly.' + ) + + if (profile.has_carbon and profile.max_temperature >= CH_PYROLYSIS_T_THRESHOLD + and profile.has_oxygen and not pah_libs_requested): + logging.info( + " PAH formation libraries were not selected because oxygen is present. Add " + "the '' keyword to request them, which is worth doing for systems " + 'expected to form aromatics, such as fuel-rich partial oxidation.' + ) + + logging.warning( + ' Auto-selection is a heuristic starting point, not a substitute for reviewing ' + 'the database. Inspect the selection above and refine it for your system.' + ) + + def determine_kinetics_families(profile: ChemistryProfile) -> List[FamilySet]: """ Determine which kinetics family sets to activate based on the detected profile. @@ -553,6 +608,8 @@ def auto_select_libraries(rmg): set_names = determine_chemistry_sets(profile, pah_libs_requested) logging.info(f' Chemistry sets triggered: {", ".join(s.value for s in set_names)}') + warn_about_coverage(profile, rmg.reaction_systems, pah_libs_requested) + # Load and expand recommended_libraries.yml recommended_data = load_recommended_yml(rmg.database_directory) auto_thermo, auto_kinetics, auto_transport, auto_seeds = expand_chemistry_sets( diff --git a/test/database/databaseTest.py b/test/database/databaseTest.py index 5e772bc46b..c49d7612f7 100644 --- a/test/database/databaseTest.py +++ b/test/database/databaseTest.py @@ -45,7 +45,7 @@ from rmgpy import settings from rmgpy.data.base import LogicOr from rmgpy.data.rmg import RMGDatabase -from rmgpy.exceptions import ImplicitBenzeneError, UnexpectedChargeError +from rmgpy.exceptions import DatabaseError, ImplicitBenzeneError, UnexpectedChargeError from rmgpy.molecule import Group, Molecule from rmgpy.molecule.atomtype import ATOMTYPES from rmgpy.molecule.pathfinder import find_shortest_path @@ -454,9 +454,14 @@ def kinetics_check_coverage_dependence_units_are_correct(self, family_name): return True def kinetics_check_training_reactions_have_surface_attributes(self, family_name): - """Test that each surface training reaction has surface attributes""" + """Test that each surface training reaction has surface attributes. + + Both a ``metal`` and a ``facet`` are required, and they must exist + in the metal library so that binding energies can be looked up without ambiguity. + """ family = self.database.kinetics.families[family_name] training = family.get_training_depository().entries.values() + metal_db = self.database.thermo.surface["metal"] failed = False for entry in training: if not entry.metal: @@ -466,9 +471,25 @@ def kinetics_check_training_reactions_have_surface_attributes(self, family_name) with check: assert isinstance(entry.metal, str) - if entry.facet: + if not entry.facet: + logging.error(f"Expected a facet attribute for {entry} in {family} family but found {entry.facet!r}") + failed = True + else: with check: assert isinstance(entry.facet, str) + + # A metal+facet must resolve to a real metal library entry so that + # binding energies can be looked up without ambiguity. + if entry.metal and entry.facet: + try: + metal_db.get_binding_energies(entry.metal + entry.facet) + except DatabaseError: + logging.error( + f"Metal/facet {entry.metal + entry.facet!r} for {entry} in {family} family " + f"is not a valid entry in the metal library." + ) + failed = True + if entry.site: with check: assert isinstance(entry.site, str) diff --git a/test/rmgpy/data/autoDatabaseTest.py b/test/rmgpy/data/autoDatabaseTest.py index c262c08a11..3385242b58 100644 --- a/test/rmgpy/data/autoDatabaseTest.py +++ b/test/rmgpy/data/autoDatabaseTest.py @@ -52,6 +52,9 @@ load_recommended_yml, merge_with_user_libraries, resolve_auto_kinetics_families, + warn_about_coverage, + COVERED_ELEMENTS, + CH_PYROLYSIS_T_THRESHOLD, ) from rmgpy.molecule import Molecule from rmgpy.quantity import Quantity @@ -681,5 +684,56 @@ def test_oxyfuel_with_inert_bath_gas(self): self.assertNotIn(ChemistrySet.NITROGEN, sets) +class TestWarnAboutCoverage(unittest.TestCase): + """The selector must say so when the detected chemistry falls outside the presets.""" + + def _warnings(self, profile, reaction_systems=None, pah=False): + with self.assertLogs('root', level='INFO') as captured: + warn_about_coverage(profile, reaction_systems or [], pah) + return '\n'.join(captured.output) + + def test_always_warns_that_selection_is_heuristic(self): + messages = self._warnings(ChemistryProfile(elements_present={'C', 'H'})) + self.assertIn('heuristic starting point', messages) + + def test_uncovered_element(self): + profile = ChemistryProfile(elements_present={'C', 'H', 'O', 'Si'}, + has_carbon=True, has_oxygen=True) + messages = self._warnings(profile) + self.assertIn('no chemistry set for the element(s) Si', messages) + + def test_all_covered_elements_are_silent(self): + profile = ChemistryProfile(elements_present=set(COVERED_ELEMENTS) - {'X'}) + self.assertNotIn('no chemistry set for the element', self._warnings(profile)) + + def test_pah_withheld_note(self): + profile = ChemistryProfile(elements_present={'C', 'H', 'O'}, has_carbon=True, + has_oxygen=True, + max_temperature=CH_PYROLYSIS_T_THRESHOLD + 100) + messages = self._warnings(profile) + self.assertIn('PAH formation libraries were not selected', messages) + self.assertIn('', messages) + + def test_pah_note_suppressed_when_requested(self): + profile = ChemistryProfile(elements_present={'C', 'H', 'O'}, has_carbon=True, + has_oxygen=True, + max_temperature=CH_PYROLYSIS_T_THRESHOLD + 100) + messages = self._warnings(profile, pah=True) + self.assertNotIn('PAH formation libraries were not selected', messages) + + def test_pah_note_suppressed_below_threshold(self): + profile = ChemistryProfile(elements_present={'C', 'H', 'O'}, has_carbon=True, + has_oxygen=True, + max_temperature=CH_PYROLYSIS_T_THRESHOLD - 100) + messages = self._warnings(profile) + self.assertNotIn('PAH formation libraries were not selected', messages) + + def test_isothermal_reactor_does_not_warn_about_adiabatic_temperature(self): + profile = ChemistryProfile(elements_present={'C', 'H'}, has_carbon=True, + max_temperature=1000.0) + messages = self._warnings(profile, [_simple_reactor(1000.0)]) + self.assertNotIn('adiabatic reactor', messages) + + if __name__ == '__main__': unittest.main() From ff2b27fb1eee7617d4f001c5f745b0f5975a0202 Mon Sep 17 00:00:00 2001 From: Richard West Date: Mon, 14 Sep 2026 16:12:18 -0400 Subject: [PATCH 2/2] Clearer error message in database test. --- test/database/databaseTest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/database/databaseTest.py b/test/database/databaseTest.py index c49d7612f7..a95fad1879 100644 --- a/test/database/databaseTest.py +++ b/test/database/databaseTest.py @@ -486,7 +486,9 @@ def kinetics_check_training_reactions_have_surface_attributes(self, family_name) except DatabaseError: logging.error( f"Metal/facet {entry.metal + entry.facet!r} for {entry} in {family} family " - f"is not a valid entry in the metal library." + f"is not a valid entry in the metal library. Either correct the facet or add " + f"{entry.metal + entry.facet!r} (binding energies + surface site density) to " + f"input/surface/libraries/metal.py." ) failed = True