Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions documentation/source/users/rmg/input.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
57 changes: 57 additions & 0 deletions rmgpy/data/auto_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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."""
Expand Down Expand Up @@ -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 <PAH_libs> 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.'
Comment on lines +297 to +302
)

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 '<PAH_libs>' 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.
Expand Down Expand Up @@ -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(
Expand Down
29 changes: 26 additions & 3 deletions test/database/databaseTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -466,9 +471,27 @@ 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:
Comment on lines +474 to 478
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. 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

if entry.site:
with check:
assert isinstance(entry.site, str)
Expand Down
54 changes: 54 additions & 0 deletions test/rmgpy/data/autoDatabaseTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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('<PAH_libs>', 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()
Loading