Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
69f021c
Add thermodynamic coverage dependent models in surface reactor
12Chao Mar 10, 2024
1ad7c77
add thermo coverage dependence instance while reading the file
12Chao Mar 11, 2024
416082c
add import copy package
12Chao Mar 11, 2024
0945656
add the instances for thermo coverage dependence
12Chao Mar 11, 2024
a42f58f
add polynomial thermodynamic coverage model to Wilhoit, NASA, and the…
12Chao Mar 12, 2024
6d4ab27
declare _thermo_coverage_dependence in the pxd files
12Chao Mar 12, 2024
f23a8d6
fix the typos, and add the missing import command line
12Chao Mar 12, 2024
50402b8
Add thermo_coverage_dependence as new property to NASA
12Chao Mar 12, 2024
669ba4e
Move thermo_coverage_dependence from NASAPolynomial to NASA
12Chao Mar 13, 2024
f887b57
Add unit test for coverage dependent thermodynamics in RMG thermo cla…
12Chao Mar 13, 2024
cdf11f3
check thermo_coverage_dependence is not empty
12Chao Mar 13, 2024
d927e5a
Fix the bug for calculating thermo coverage dependent correct values
12Chao Mar 13, 2024
060f252
add a big matrix for species Gibbs free energy corrections
12Chao Mar 15, 2024
518859e
correct Gibbs free energy for each species based on its 3-parameter p…
12Chao Mar 16, 2024
e292755
use stoichiometric matrix to calculate the correct value for Keq
12Chao Mar 18, 2024
64b6e20
Add unit tests for thermo coverage dependent models in surface solver…
12Chao Mar 20, 2024
e81f947
use isomorphic check to check if thermo coverage dependent species ar…
12Chao Mar 24, 2024
5c9f83a
delete unused instances
12Chao Mar 25, 2024
cd59089
Add solver unit test for thermodynamic coverage dependent models
12Chao Mar 25, 2024
e6fac7b
save thermo coverage dependent models as numpy arrays instead a list …
12Chao Mar 25, 2024
0360c57
create a subclass of numpy array overriding __repr__ method
12Chao Mar 25, 2024
0c77c57
create thermo coverage dep polynomial model in numpy array class with…
12Chao Mar 25, 2024
4f09c50
use adjacency list as the key of thermo cov dep models
12Chao Mar 25, 2024
31de2b1
adjust the method to read data from numpy arrays instead list
12Chao Mar 25, 2024
43dea6b
Write thermo coverage dependent data to Cantera yaml file
12Chao Mar 28, 2024
ed0474c
Use chemkin strings instead of labels when writing to yaml files
12Chao Mar 29, 2024
51e3821
Write the thermo coverage dependent data to yaml files in right format
12Chao Mar 30, 2024
79098ed
Add thermo_coverag_dependence as an instance for class RMG
12Chao Apr 2, 2024
fae856c
fix the bug that the coverage kinetics cannot be written to chemkin
12Chao Jul 18, 2024
b35ee5b
write data as RMG object instead of floats to keep the units consistent
12Chao Oct 19, 2024
2ac9c17
add thermo_coverage_dependence to initialize
bjkreitz Oct 10, 2025
a579b3a
fix type in wilhoit.pyx
bjkreitz Oct 13, 2025
b504d9e
fix coefficients check in nasaTest.py
bjkreitz Oct 13, 2025
91fbefb
fix test_thermo_coverage_dependence
bjkreitz Oct 14, 2025
e5ecc8d
fix thermodataTest.py
bjkreitz Oct 14, 2025
a413a28
fix convertTest.py
bjkreitz Oct 14, 2025
6964658
fix wilhoit test_thermo_coverage_dependence and test_wilhoit_as_dict
bjkreitz Oct 14, 2025
b9e8f12
fix solverSurfaceTest.py by adding units
bjkreitz Oct 14, 2025
a6c1de9
calculate rxns_free_energy_change via np.matmul
bjkreitz Oct 14, 2025
fa1196e
remove Chaos bugfix that the coverage kinetics cannot be written to c…
bjkreitz Oct 14, 2025
261230f
remove cantera dependence
bjkreitz Apr 6, 2026
634aa03
remove np_list function
bjkreitz Apr 6, 2026
c15694c
Optimization: avoiding exponentials and stacks.
rwest Apr 8, 2026
e185c4e
optimization: removing loops, and making cython declarations
rwest Apr 8, 2026
1386801
Don't save empty dictionary when thermo coverage is None
rwest Apr 8, 2026
84b1749
Small optimization and simplification.
rwest Apr 8, 2026
ea374fa
update tests for coverage-dependence data as_dict
rwest Apr 8, 2026
589fe73
Fix YAML dumping style for consistency in output files.
rwest Apr 9, 2026
c9e20b1
Changing coverage-dependent thermo cantera yaml output.
rwest Apr 9, 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
7 changes: 5 additions & 2 deletions rmgpy/rmg/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ def database(
def catalyst_properties(bindingEnergies=None,
surfaceSiteDensity=None,
metal=None,
coverageDependence=False):
coverageDependence=False,
thermoCoverageDependence=False,):
"""
Specify the properties of the catalyst.
Binding energies of C,H,O,N atoms, and the surface site density.
Expand Down Expand Up @@ -167,6 +168,7 @@ def catalyst_properties(bindingEnergies=None,
else:
logging.info("Coverage dependence is turned OFF")
rmg.coverage_dependence = coverageDependence
rmg.thermo_coverage_dependence = thermoCoverageDependence

def convert_binding_energies(binding_energies):
"""
Expand Down Expand Up @@ -1088,7 +1090,8 @@ def surface_reactor(temperature,
sensitive_species=sensitive_species,
sensitivity_threshold=sensitivityThreshold,
sens_conditions=sens_conditions,
coverage_dependence=rmg.coverage_dependence)
coverage_dependence=rmg.coverage_dependence,
thermo_coverage_dependence=rmg.thermo_coverage_dependence)
rmg.reaction_systems.append(system)
system.log_initial_conditions(number=len(rmg.reaction_systems))

Expand Down
75 changes: 73 additions & 2 deletions rmgpy/rmg/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import logging
import marshal
import os
import re
import resource
import shutil
import sys
Expand Down Expand Up @@ -193,6 +194,7 @@ def clear(self):
self.surface_site_density = None
self.binding_energies = None
self.coverage_dependence = False
self.thermo_coverage_dependence = False
self.forbidden_structures = []

self.reaction_model = None
Expand Down Expand Up @@ -510,7 +512,7 @@ def initialize(self, **kwargs):

# Read input file
self.load_input(self.input_file)

# Check if ReactionMechanismSimulator reactors are being used
# if RMS is not installed but the user attempted to use it, the load_input_file would have failed
# if RMS is not installed and they did not use it, we avoid calling certain functions that would raise an error
Expand All @@ -523,6 +525,7 @@ def initialize(self, **kwargs):
self.reaction_model.core.phase_system.phases["Surface"].site_density = self.surface_site_density.value_si
self.reaction_model.edge.phase_system.phases["Surface"].site_density = self.surface_site_density.value_si
self.reaction_model.coverage_dependence = self.coverage_dependence
self.reaction_model.thermo_coverage_dependence = self.thermo_coverage_dependence

if kwargs.get("restart", ""):
import rmgpy.rmg.input
Expand Down Expand Up @@ -768,7 +771,7 @@ def register_listeners(self, requires_rms=False):
"""

self.attach(ChemkinWriter(self.output_directory))

self.attach(RMSWriter(self.output_directory))

if self.generate_output_html:
Expand Down Expand Up @@ -1221,6 +1224,7 @@ def execute(self, initialize=True, **kwargs):
# generate Cantera files chem.yaml & chem_annotated.yaml in a designated `cantera` output folder
try:
if any([s.contains_surface_site() for s in self.reaction_model.core.species]):
# Surface (catalytic) chemistry
self.generate_cantera_files(
os.path.join(self.output_directory, "chemkin", "chem-gas.inp"),
surface_file=(os.path.join(self.output_directory, "chemkin", "chem-surface.inp")),
Expand All @@ -1229,6 +1233,34 @@ def execute(self, initialize=True, **kwargs):
os.path.join(self.output_directory, "chemkin", "chem_annotated-gas.inp"),
surface_file=(os.path.join(self.output_directory, "chemkin", "chem_annotated-surface.inp")),
)

if self.thermo_coverage_dependence:
# Build coverage_deps: {species_name: string_to_add_to_yaml}
coverage_deps = {}
for s in self.reaction_model.core.species:
if s.contains_surface_site() and s.thermo.thermo_coverage_dependence:
s_name = s.to_chemkin()
for dep_sp_adj, parameters in s.thermo.thermo_coverage_dependence.items():
mol = Molecule().from_adjacency_list(dep_sp_adj)
for sp in self.reaction_model.core.species:
if sp.is_isomorphic(mol, strict=False):
if s_name not in coverage_deps:
coverage_deps[s_name] = ' coverage-dependencies:'
coverage_deps[s_name] += f"""
{sp.to_chemkin()}:
model: {parameters['model']}
enthalpy-coefficients: {[v.value_si for v in parameters['enthalpy-coefficients']]}
entropy-coefficients: {[v.value_si for v in parameters['entropy-coefficients']]}
units: {{energy: J, quantity: mol}}
"""
break

for yaml_path in [
os.path.join(self.output_directory, "cantera", "chem.yaml"),
os.path.join(self.output_directory, "cantera", "chem_annotated.yaml"),
]:
_add_coverage_dependence_to_cantera_yaml(yaml_path, coverage_deps)

else: # gas phase only
self.generate_cantera_files(os.path.join(self.output_directory, "chemkin", "chem.inp"))
self.generate_cantera_files(os.path.join(self.output_directory, "chemkin", "chem_annotated.inp"))
Expand Down Expand Up @@ -2392,6 +2424,45 @@ def obj(y):
self.scaled_condition_list.append(scaled_new_cond)
return

def _add_coverage_dependence_to_cantera_yaml(yaml_path, coverage_deps):
"""Modify a Cantera YAML file in-place to add coverage-dependent surface thermo.

Makes targeted text insertions rather than loading and re-dumping the whole
file, so original formatting is preserved everywhere except the new lines.

Args:
yaml_path: path to the Cantera YAML file to modify
coverage_deps: dict mapping species ChemKin names to their coverage-dependency string.
"""
with open(yaml_path, 'r') as f:
content = f.read()

# --- Modify the surface phase ---
# Replace 'ideal-surface' with 'coverage-dependent-surface' and add reference-state-coverage.
content = content.replace(
' thermo: ideal-surface\n',
' thermo: coverage-dependent-surface\n reference-state-coverage: 0.11\n',
1,
)

# --- Insert coverage-dependencies block after each relevant species entry ---
for species_name, deps in coverage_deps.items():
match = re.search(r'^- name: ' + re.escape(species_name) + r'\n', content, re.MULTILINE)
if not match:
logging.warning(
f"Species {species_name} not found in {yaml_path}; skipping coverage-dependency insertion."
)
continue

after = match.end()
end_match = re.search(r'\n(?=(?:- |\n|\w))', content[after:])
if end_match:
insert_pos = after + end_match.start() + 1
content = content[:insert_pos] + deps + content[insert_pos:]
else:
content = content.rstrip('\n') + '\n' + deps
with open(yaml_path, 'w') as f:
f.write(content)

def log_conditions(rmg_memories, index):
"""
Expand Down
83 changes: 79 additions & 4 deletions rmgpy/solver/surface.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ cimport rmgpy.constants as constants
from rmgpy.quantity import Quantity
from rmgpy.quantity cimport ScalarQuantity
from rmgpy.solver.base cimport ReactionSystem
import copy
from rmgpy.molecule import Molecule

cdef class SurfaceReactor(ReactionSystem):
"""
Expand All @@ -66,9 +68,13 @@ cdef class SurfaceReactor(ReactionSystem):
cdef public ScalarQuantity surface_site_density
cdef public np.ndarray reactions_on_surface # (catalyst surface, not core/edge surface)
cdef public np.ndarray species_on_surface # (catalyst surface, not core/edge surface)
cdef public np.ndarray thermo_coeff_matrix
cdef public np.ndarray stoi_matrix

cdef public bint coverage_dependence
cdef public dict coverage_dependencies
cdef public bint thermo_coverage_dependence



def __init__(self,
Expand All @@ -84,6 +90,7 @@ cdef class SurfaceReactor(ReactionSystem):
sensitivity_threshold=1e-3,
sens_conditions=None,
coverage_dependence=False,
thermo_coverage_dependence=False,
):
ReactionSystem.__init__(self,
termination,
Expand All @@ -103,6 +110,7 @@ cdef class SurfaceReactor(ReactionSystem):
self.surface_volume_ratio = Quantity(surface_volume_ratio)
self.surface_site_density = Quantity(surface_site_density)
self.coverage_dependence = coverage_dependence
self.thermo_coverage_dependence = thermo_coverage_dependence
self.V = 0 # will be set from ideal gas law in initialize_model
self.constant_volume = True
self.sens_conditions = sens_conditions
Expand Down Expand Up @@ -166,6 +174,10 @@ cdef class SurfaceReactor(ReactionSystem):
)
cdef np.ndarray[np.int_t, ndim=1] species_on_surface, reactions_on_surface
cdef Py_ssize_t index
cdef np.ndarray thermo_coeff_matrix = np.zeros((len(self.species_index), len(self.species_index), 6), dtype=np.float64)
cdef np.ndarray stoi_matrix = np.zeros((self.reactant_indices.shape[0], len(self.species_index)), dtype=np.float64)
if self.thermo_coverage_dependence:
self.thermo_coeff_matrix = thermo_coeff_matrix
#: 1 if it's on a surface, 0 if it's in the gas phase
reactions_on_surface = np.zeros((self.num_core_reactions + self.num_edge_reactions), int)
species_on_surface = np.zeros((self.num_core_species), int)
Expand Down Expand Up @@ -195,6 +207,49 @@ cdef class SurfaceReactor(ReactionSystem):
means that Species with index 2 in the current simulation is used in
Reaction 3 with parameters a=0.1, m=-1, E=12 kJ/mol
"""
for sp, sp_index in self.species_index.items():
if sp.contains_surface_site():
if self.thermo_coverage_dependence and sp.thermo.thermo_coverage_dependence:
for spec, parameters in sp.thermo.thermo_coverage_dependence.items():
molecule = Molecule().from_adjacency_list(spec)
for species in self.species_index.keys():
if species.is_isomorphic(molecule, strict=False):
species_index = self.species_index[species]
enthalpy_coeff = np.array([p.value_si for p in parameters['enthalpy-coefficients']])
entropy_coeff = np.array([p.value_si for p in parameters['entropy-coefficients']])
thermo_polynomials = np.concatenate((enthalpy_coeff, entropy_coeff), axis=0)
self.thermo_coeff_matrix[sp_index, species_index] = [x for x in thermo_polynomials]
# create a stoichiometry matrix for reaction enthalpy and entropy correction
# due to thermodynamic coverage dependence
if self.thermo_coverage_dependence:
ir = self.reactant_indices
ip = self.product_indices
for rxn_id, rxn_stoi_num in enumerate(stoi_matrix):
if ir[rxn_id, 0] >= self.num_core_species or ir[rxn_id, 1] >= self.num_core_species or ir[rxn_id, 2] >= self.num_core_species:
continue
elif ip[rxn_id, 0] >= self.num_core_species or ip[rxn_id, 1] >= self.num_core_species or ip[rxn_id, 2] >= self.num_core_species:
continue
else:
if ir[rxn_id, 1] == -1: # only one reactant
rxn_stoi_num[ir[rxn_id, 0]] += -1
elif ir[rxn_id, 2] == -1: # only two reactants
rxn_stoi_num[ir[rxn_id, 0]] += -1
rxn_stoi_num[ir[rxn_id, 1]] += -1
else: # three reactants
rxn_stoi_num[ir[rxn_id, 0]] += -1
rxn_stoi_num[ir[rxn_id, 1]] += -1
rxn_stoi_num[ir[rxn_id, 2]] += -1
if ip[rxn_id, 1] == -1: # only one product
rxn_stoi_num[ip[rxn_id, 0]] += 1
elif ip[rxn_id, 2] == -1: # only two products
rxn_stoi_num[ip[rxn_id, 0]] += 1
rxn_stoi_num[ip[rxn_id, 1]] += 1
else: # three products
rxn_stoi_num[ip[rxn_id, 0]] += 1
rxn_stoi_num[ip[rxn_id, 1]] += 1
rxn_stoi_num[ip[rxn_id, 2]] += 1
self.stoi_matrix = stoi_matrix

self.species_on_surface = species_on_surface
self.reactions_on_surface = reactions_on_surface

Expand Down Expand Up @@ -378,9 +433,10 @@ cdef class SurfaceReactor(ReactionSystem):
cdef np.ndarray[np.float64_t, ndim=2] jacobian, dgdk
cdef list list_of_coverage_deps
cdef double surface_site_fraction, total_sites, a, m, E



cdef np.ndarray[np.float64_t, ndim=1] coverages, coverages_squared, temperature_scaled_coverages
cdef np.ndarray[np.float64_t, ndim=2] thermo_dep_coverage
cdef np.ndarray[np.float64_t, ndim=1] free_energy_coverage_corrections, rxns_free_energy_change, corrected_K_eq
cdef double sp_free_energy_correction
ir = self.reactant_indices
ip = self.product_indices
equilibrium_constants = self.Keq
Expand Down Expand Up @@ -414,14 +470,33 @@ cdef class SurfaceReactor(ReactionSystem):
V = self.V # constant volume reactor
A = self.V * surface_volume_ratio_si # area
total_sites = self.surface_site_density.value_si * A # todo: double check units

for j in range(num_core_species):
if species_on_surface[j]:
C[j] = (N[j] / V) / surface_volume_ratio_si
else:
C[j] = N[j] / V
#: surface species are in mol/m2, gas phase are in mol/m3
core_species_concentrations[j] = C[j]

# Thermodynamic coverage dependence
if self.thermo_coverage_dependence:
coverages = np.where(species_on_surface, N / total_sites, 0.0)
coverages_squared = coverages * coverages
temperature_scaled_coverages = -self.T.value_si * coverages
thermo_dep_coverage = np.empty((6, coverages.shape[0]), dtype=np.float64)
thermo_dep_coverage[0, :] = coverages
thermo_dep_coverage[1, :] = coverages_squared
thermo_dep_coverage[2, :] = coverages_squared * coverages
thermo_dep_coverage[3, :] = temperature_scaled_coverages
thermo_dep_coverage[4, :] = temperature_scaled_coverages * coverages
thermo_dep_coverage[5, :] = temperature_scaled_coverages * coverages_squared
free_energy_coverage_corrections = np.empty(len(self.thermo_coeff_matrix), dtype=np.float64)
for i, matrix in enumerate(self.thermo_coeff_matrix):
free_energy_coverage_corrections[i] = np.diag(np.dot(matrix, thermo_dep_coverage)).sum()
rxns_free_energy_change = np.matmul(self.stoi_matrix,free_energy_coverage_corrections)
corrected_K_eq = copy.deepcopy(self.Keq)
corrected_K_eq *= np.exp(-1 * rxns_free_energy_change / (constants.R * self.T.value_si))
kr = kf / corrected_K_eq

# Coverage dependence
coverage_corrections = np.ones_like(kf, float)
Expand Down
1 change: 1 addition & 0 deletions rmgpy/thermo/nasa.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ cdef class NASAPolynomial(HeatCapacityModel):
cdef class NASA(HeatCapacityModel):

cdef public NASAPolynomial poly1, poly2, poly3
cdef public dict _thermo_coverage_dependence

cpdef NASAPolynomial select_polynomial(self, double T)

Expand Down
Loading
Loading