Skip to content

Commit c9df5b4

Browse files
Nora-Khalilrwestntietje1
committed
Enable Cantera-YAML writing.
Nora did most of the work. Richard did some cleanup. Nick fixed species_to_dict function to use correct parameter as rxn species. Co-authored-by: Nora Khalil <[email protected]> Co-authored-by: Richard West <[email protected]> Co-authored-by: Nicholas Tietje <[email protected]>
1 parent 8b07cca commit c9df5b4

File tree

2 files changed

+380
-1
lines changed

2 files changed

+380
-1
lines changed

rmgpy/rmg/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from rmgpy.tools.plot import plot_sensitivity
8585
from rmgpy.tools.uncertainty import Uncertainty, process_local_results
8686
from rmgpy.yaml_rms import RMSWriter
87+
from rmgpy.yaml_cantera import CanteraWriter
8788

8889
################################################################################
8990

@@ -770,7 +771,7 @@ def register_listeners(self, requires_rms=False):
770771
self.attach(ChemkinWriter(self.output_directory))
771772
if requires_rms:
772773
self.attach(RMSWriter(self.output_directory))
773-
774+
self.attach(CanteraWriter(self.output_directory))
774775
if self.generate_output_html:
775776
self.attach(OutputHTMLWriter(self.output_directory))
776777

rmgpy/yaml_cantera.py

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
#!/usr/bin/env python3
2+
3+
###############################################################################
4+
# #
5+
# RMG - Reaction Mechanism Generator #
6+
# #
7+
# Copyright (c) 2002-2024 Prof. William H. Green ([email protected]), #
8+
# Prof. Richard H. West ([email protected]) and the RMG Team ([email protected]) #
9+
# #
10+
# Permission is hereby granted, free of charge, to any person obtaining a #
11+
# copy of this software and associated documentation files (the 'Software'), #
12+
# to deal in the Software without restriction, including without limitation #
13+
# the rights to use, copy, modify, merge, publish, distribute, sublicense, #
14+
# and/or sell copies of the Software, and to permit persons to whom the #
15+
# Software is furnished to do so, subject to the following conditions: #
16+
# #
17+
# The above copyright notice and this permission notice shall be included in #
18+
# all copies or substantial portions of the Software. #
19+
# #
20+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
21+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #
22+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
23+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
24+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING #
25+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER #
26+
# DEALINGS IN THE SOFTWARE. #
27+
# #
28+
###############################################################################
29+
30+
"""
31+
This file defines functions for outputting the RMG generated mechanism to
32+
a yaml file that can be read by Cantera
33+
"""
34+
35+
36+
import os
37+
import yaml
38+
39+
from rmgpy.species import Species
40+
from rmgpy.kinetics.arrhenius import (
41+
MultiArrhenius,
42+
MultiPDepArrhenius,
43+
)
44+
from rmgpy.util import make_output_subdirectory
45+
from datetime import datetime
46+
from rmgpy.chemkin import get_species_identifier
47+
48+
49+
def write_cantera(
50+
spcs,
51+
rxns,
52+
surface_site_density=None,
53+
solvent=None,
54+
solvent_data=None,
55+
path="chem.yml",
56+
):
57+
"""
58+
Writes yaml file depending on the type of system (gas-phase, catalysis).
59+
Writes beginning lines of yaml file, then uses yaml.dump(result_dict) to write species/reactions info.
60+
"""
61+
62+
# intro to file will change depending on the presence of surface species
63+
is_surface = False
64+
for spc in spcs:
65+
if spc.contains_surface_site():
66+
is_surface = True
67+
if is_surface:
68+
result_dict = get_mech_dict_surface(
69+
spcs, rxns, solvent=solvent, solvent_data=solvent_data
70+
)
71+
phases_block, elements_block = get_phases_elements_with_surface(
72+
spcs, surface_site_density
73+
)
74+
else:
75+
result_dict = get_mech_dict_nonsurface(
76+
spcs, rxns, solvent=solvent, solvent_data=solvent_data
77+
)
78+
phases_block, elements_block = get_phases_elements_gas_only(spcs)
79+
80+
with open(path, "w") as f:
81+
# generator line
82+
f.write("generator: RMG\n")
83+
84+
# datetime object containing current date and time
85+
now = datetime.now()
86+
dt_string = now.strftime("%a, %d %b %Y %H:%M:%S")
87+
f.write(f"date: {dt_string}\n")
88+
89+
# units line
90+
f.write(
91+
"\nunits: {length: cm, time: s, quantity: mol, activation-energy: kcal/mol}\n\n"
92+
)
93+
94+
f.write(phases_block)
95+
f.write(elements_block)
96+
97+
yaml.dump(result_dict, stream=f, sort_keys=False)
98+
99+
100+
def get_phases_elements_gas_only(spcs):
101+
"""
102+
Returns 'phases' and 'elements' sections for a file
103+
with only gas-phase species/reactions.
104+
"""
105+
sorted_species = sorted(spcs, key=lambda spcs: spcs.index)
106+
species_to_write = [get_species_identifier(spec) for spec in sorted_species]
107+
# make sure species with "[" or "]" is in quotes
108+
species_to_write = [
109+
f"'{s}'" if "[" in s or "{" in s or "]" in s or "}" in s else s
110+
for s in species_to_write
111+
]
112+
phases_block = f"""
113+
phases:
114+
- name: gas
115+
thermo: ideal-gas
116+
elements: [H, D, T, C, Ci, O, Oi, N, Ne, Ar, He, Si, S, F, Cl, Br, I]
117+
species: [{', '.join(species_to_write)}]
118+
kinetics: gas
119+
transport: mixture-averaged
120+
state: {{T: 300.0, P: 1 atm}}
121+
"""
122+
123+
elements_block = """
124+
elements:
125+
- symbol: Ci
126+
atomic-weight: 13.003
127+
- symbol: D
128+
atomic-weight: 2.014
129+
- symbol: Oi
130+
atomic-weight: 17.999
131+
- symbol: T
132+
atomic-weight: 3.016
133+
134+
"""
135+
return phases_block, elements_block
136+
137+
138+
def get_phases_elements_with_surface(spcs, surface_site_density):
139+
"""
140+
Yaml files with surface species begin with the following blocks of text,
141+
which includes TWO phases instead of just one.
142+
Returns 'phases' and 'elements' sections.
143+
"""
144+
surface_species = []
145+
gas_species = []
146+
for spc in spcs:
147+
148+
if spc.contains_surface_site():
149+
surface_species.append(spc)
150+
else:
151+
gas_species.append(spc)
152+
153+
sorted_surface_species = sorted(
154+
surface_species, key=lambda surface_species: surface_species.index
155+
)
156+
157+
surface_species_to_write = [
158+
get_species_identifier(s) for s in sorted_surface_species
159+
]
160+
161+
# make sure species with "[" or "]" is in quotes
162+
surface_species_to_write = [
163+
f"'{s}'" if "[" in s or "{" in s or "]" in s or "}" in s else s
164+
for s in surface_species_to_write
165+
]
166+
167+
sorted_gas_species = sorted(gas_species, key=lambda gas_species: gas_species.index)
168+
gas_species_to_write = [get_species_identifier(s) for s in sorted_gas_species]
169+
170+
# make sure species with "[" or "]" is in quotes
171+
gas_species_to_write = [
172+
f"'{s}'" if "[" in s or "{" in s or "]" in s or "}" in s else s
173+
for s in gas_species_to_write
174+
]
175+
176+
phases_block = f"""
177+
phases:
178+
- name: gas
179+
thermo: ideal-gas
180+
elements: [H, D, T, C, Ci, O, Oi, N, Ne, Ar, He, Si, S, F, Cl, Br, I]
181+
species: [{', '.join(gas_species_to_write)}]
182+
kinetics: gas
183+
reactions: [gas_reactions]
184+
transport: mixture-averaged
185+
state: {{T: 300.0, P: 1 atm}}
186+
187+
- name: {surface_species[0].smiles.replace("[","").replace("]","")}_surface
188+
thermo: ideal-surface
189+
adjacent-phases: [gas]
190+
elements: [H, D, T, C, Ci, O, Oi, N, Ne, Ar, He, Si, S, F, Cl, Br, I, X]
191+
species: [{', '.join(surface_species_to_write)}]
192+
kinetics: surface
193+
reactions: [surface_reactions]
194+
site-density: {surface_site_density * 1e-4 }
195+
"""
196+
# surface_site_density * 1e-4 #in units of mol/cm^2
197+
198+
elements_block = """
199+
elements:
200+
- symbol: Ci
201+
atomic-weight: 13.003
202+
- symbol: D
203+
atomic-weight: 2.014
204+
- symbol: Oi
205+
atomic-weight: 17.999
206+
- symbol: T
207+
atomic-weight: 3.016
208+
- symbol: X
209+
atomic-weight: 195.083
210+
211+
"""
212+
return phases_block, elements_block
213+
214+
215+
def get_mech_dict_surface(spcs, rxns, solvent="solvent", solvent_data=None):
216+
"""
217+
For systems with surface species/reactions.
218+
Adds 'species', 'gas-reactions', and 'surface-reactions' to result_dict.
219+
"""
220+
gas_rxns = []
221+
surface_rxns = []
222+
for rxn in rxns:
223+
if rxn.is_surface_reaction():
224+
surface_rxns.append(rxn)
225+
else:
226+
gas_rxns.append(rxn)
227+
228+
names = [x.label for x in spcs]
229+
for i, name in enumerate(names): # fix duplicate names
230+
if names.count(name) > 1:
231+
names[i] += "-" + str(names.count(name))
232+
233+
result_dict = dict()
234+
result_dict["species"] = [species_to_dict(x, spcs, names=names) for x in spcs]
235+
236+
# separate gas and surface reactions
237+
238+
gas_reactions = []
239+
for rmg_rxn in gas_rxns:
240+
gas_reactions.extend(reaction_to_dicts(rmg_rxn, spcs))
241+
result_dict["gas_reactions"] = gas_reactions
242+
243+
surface_reactions = []
244+
for rmg_rxn in surface_rxns:
245+
surface_reactions.extend(reaction_to_dicts(rmg_rxn, spcs))
246+
result_dict["surface_reactions"] = surface_reactions
247+
248+
return result_dict
249+
250+
251+
def get_mech_dict_nonsurface(spcs, rxns, solvent="solvent", solvent_data=None):
252+
"""
253+
For gas-phase systems.
254+
Adds 'species' and 'reactions' to result_dict.
255+
"""
256+
names = [x.label for x in spcs]
257+
for i, name in enumerate(names): # fix duplicate names
258+
if names.count(name) > 1:
259+
names[i] += "-" + str(names.count(name))
260+
261+
result_dict = dict()
262+
result_dict["species"] = [species_to_dict(x, spcs, names=names) for x in spcs]
263+
264+
reactions = []
265+
for rmg_rxn in rxns:
266+
reactions.extend(reaction_to_dicts(rmg_rxn, spcs))
267+
result_dict["reactions"] = reactions
268+
269+
return result_dict
270+
271+
272+
def reaction_to_dicts(obj, spcs):
273+
"""
274+
Takes an RMG reaction object (obj), returns a list of dictionaries
275+
for YAML properties. For most reaction objects the list will be of
276+
length 1, but a MultiArrhenius or MultiPDepArrhenius will be longer.
277+
"""
278+
279+
reaction_list = []
280+
if isinstance(obj.kinetics, MultiArrhenius) or isinstance(
281+
obj.kinetics, MultiPDepArrhenius
282+
):
283+
list_of_cantera_reactions = obj.to_cantera(use_chemkin_identifier=True)
284+
else:
285+
list_of_cantera_reactions = [obj.to_cantera(use_chemkin_identifier=True)]
286+
287+
for reaction in list_of_cantera_reactions:
288+
reaction_data = reaction.input_data
289+
efficiencies = getattr(obj.kinetics, "efficiencies", {})
290+
if efficiencies:
291+
reaction_data["efficiencies"] = {
292+
spcs[i].to_chemkin(): float(val)
293+
for i, val in enumerate(
294+
obj.kinetics.get_effective_collider_efficiencies(spcs)
295+
)
296+
if val != 1
297+
}
298+
reaction_list.append(reaction_data)
299+
300+
return reaction_list
301+
302+
303+
def species_to_dict(obj, spc, names=None, label="solvent"):
304+
"""
305+
Takes an RMG species object (obj), returns a list of dictionaries
306+
for YAML properties. Also adds in the number of surface sites
307+
('sites') to dictionary.
308+
"""
309+
310+
result_dict = dict()
311+
312+
if isinstance(obj, Species):
313+
s = obj.to_cantera(use_chemkin_identifier=True)
314+
species_data = s.input_data
315+
try:
316+
result_dict["note"] = obj.transport_data.comment
317+
except:
318+
pass
319+
if "size" in species_data:
320+
sites = species_data["size"]
321+
species_data.pop("size", None)
322+
species_data["sites"] = sites
323+
species_data.update(result_dict)
324+
return (
325+
species_data # returns composition, name, thermo, and transport, and note
326+
)
327+
else:
328+
raise Exception("Species object must be an RMG Species object")
329+
330+
331+
class CanteraWriter(object):
332+
"""
333+
This class listens to a RMG subject
334+
and writes an YAML file with the current state of the RMG model,
335+
to a yaml subfolder.
336+
337+
338+
A new instance of the class can be appended to a subject as follows:
339+
340+
rmg = ...
341+
listener = CanteraWriter(outputDirectory)
342+
rmg.attach(listener)
343+
344+
Whenever the subject calls the .notify() method, the
345+
.update() method of the listener will be called.
346+
347+
To stop listening to the subject, the class can be detached
348+
from its subject:
349+
350+
rmg.detach(listener)
351+
352+
"""
353+
354+
def __init__(self, output_directory=""):
355+
super(CanteraWriter, self).__init__()
356+
self.output_directory = output_directory
357+
make_output_subdirectory(output_directory, "cantera")
358+
359+
def update(self, rmg):
360+
361+
solvent_data = None
362+
if rmg.solvent:
363+
solvent_data = rmg.database.solvation.get_solvent_data(rmg.solvent)
364+
365+
surface_site_density = None
366+
if rmg.reaction_model.surface_site_density:
367+
surface_site_density = rmg.reaction_model.surface_site_density.value_si
368+
369+
write_cantera(
370+
rmg.reaction_model.core.species,
371+
rmg.reaction_model.core.reactions,
372+
surface_site_density=surface_site_density,
373+
solvent=rmg.solvent,
374+
solvent_data=solvent_data,
375+
path=os.path.join(self.output_directory, "cantera", "chem{}.yaml").format(
376+
len(rmg.reaction_model.core.species)
377+
),
378+
)

0 commit comments

Comments
 (0)