2828###############################################################################
2929
3030import os .path
31+ import logging
3132
3233import cantera as ct
3334import numpy as np
@@ -63,18 +64,45 @@ class CanteraCondition(object):
6364
6465 """
6566
66- def __init__ (self , reactor_type , reaction_time , mol_frac , T0 = None , P0 = None , V0 = None ):
67+ def __init__ (self , reactor_type , reaction_time , mol_frac , surface_mol_frac = None , T0 = None , P0 = None , V0 = None ):
6768 self .reactor_type = reactor_type
6869 self .reaction_time = Quantity (reaction_time )
6970
7071 # Normalize initialMolFrac if not already done:
7172 if sum (mol_frac .values ()) != 1.00 :
73+ logging .warning ('Initial mole fractions do not sum to one; normalizing.' )
74+ logging .info ('' )
75+ logging .info ('Original composition:' )
76+ for spec , molfrac in mol_frac .items ():
77+ logging .info ('{0} = {1}' .format (spec , molfrac ))
7278 total = sum (mol_frac .values ())
79+ logging .info ('' )
80+ logging .info ('Normalized mole fractions:' )
7381 for species , value in mol_frac .items ():
7482 mol_frac [species ] = value / total
83+ logging .info ('{0} = {1}' .format (spec , mol_frac [species ]))
84+ logging .info ('' )
7585
7686 self .mol_frac = mol_frac
7787
88+ if surface_mol_frac :
89+ # normalize surface mol fractions
90+ if sum (surface_mol_frac .values ()) != 1.00 :
91+ logging .warning ('Initial surface mole fractions do not sum to one; normalizing.' )
92+ logging .info ('' )
93+ logging .info ('Original composition:' )
94+ for spec , molfrac in surface_mol_frac .items ():
95+ logging .info ('{0} = {1}' .format (spec , molfrac ))
96+
97+ logging .info ('' )
98+ logging .info ('Normalized mole fractions:' )
99+ total = sum (surface_mol_frac .values ())
100+ for species , value in surface_mol_frac .items ():
101+ surface_mol_frac [species ] = value / total
102+ logging .info ('{0} = {1}' .format (species , surface_mol_frac [species ]))
103+ logging .info ('' )
104+ self .surface_mol_frac = surface_mol_frac
105+
78106 # Check to see that one of the three attributes T0, P0, and V0 is less unspecified
79107 props = [T0 , P0 , V0 ]
80108 total = 0
@@ -121,7 +149,7 @@ def __str__(self):
121149 return string
122150
123151
124- def generate_cantera_conditions (reactor_type_list , reaction_time_list , mol_frac_list , Tlist = None , Plist = None , Vlist = None ):
152+ def generate_cantera_conditions (reactor_type_list , reaction_time_list , mol_frac_list , surface_mol_frac_list = None , Tlist = None , Plist = None , Vlist = None ):
125153 """
126154 Creates a list of cantera conditions from from the arguments provided.
127155
@@ -137,6 +165,8 @@ def generate_cantera_conditions(reactor_type_list, reaction_time_list, mol_frac_
137165 `reaction_time_list` A tuple object giving the ([list of reaction times], units)
138166 `mol_frac_list` A list of molfrac dictionaries with species object keys
139167 and mole fraction values
168+ `surface_mol_frac_list` A list of molfrac dictionaries with surface species object keys
169+ and mole fraction values
140170 To specify the system for an ideal gas, you must define 2 of the following 3 parameters:
141171 `T0List` A tuple giving the ([list of initial temperatures], units)
142172 'P0List' A tuple giving the ([list of initial pressures], units)
@@ -162,30 +192,35 @@ def generate_cantera_conditions(reactor_type_list, reaction_time_list, mol_frac_
162192 for i in range (len (reaction_time_list .value ))]
163193
164194 conditions = []
195+ if surface_mol_frac_list is None :
196+ surface_mol_frac_list = [] # initialize here to avoid mutable default argument
165197
166198 if Tlist is None :
167199 for reactor_type in reactor_type_list :
168200 for reaction_time in reaction_time_list :
169201 for mol_frac in mol_frac_list :
170- for P in Plist :
171- for V in Vlist :
172- conditions .append (CanteraCondition (reactor_type , reaction_time , mol_frac , P0 = P , V0 = V ))
202+ for surface_mol_frac in surface_mol_frac_list :
203+ for P in Plist :
204+ for V in Vlist :
205+ conditions .append (CanteraCondition (reactor_type , reaction_time , mol_frac , surface_mol_frac = surface_mol_frac , P0 = P , V0 = V ))
173206
174207 elif Plist is None :
175208 for reactor_type in reactor_type_list :
176209 for reaction_time in reaction_time_list :
177210 for mol_frac in mol_frac_list :
178- for T in Tlist :
179- for V in Vlist :
180- conditions .append (CanteraCondition (reactor_type , reaction_time , mol_frac , T0 = T , V0 = V ))
211+ for surface_mol_frac in surface_mol_frac_list :
212+ for T in Tlist :
213+ for V in Vlist :
214+ conditions .append (CanteraCondition (reactor_type , reaction_time , mol_frac , surface_mol_frac = surface_mol_frac , T0 = T , V0 = V ))
181215
182216 elif Vlist is None :
183217 for reactor_type in reactor_type_list :
184218 for reaction_time in reaction_time_list :
185219 for mol_frac in mol_frac_list :
186- for T in Tlist :
187- for P in Plist :
188- conditions .append (CanteraCondition (reactor_type , reaction_time , mol_frac , T0 = T , P0 = P ))
220+ for surface_mol_frac in surface_mol_frac_list :
221+ for T in Tlist :
222+ for P in Plist :
223+ conditions .append (CanteraCondition (reactor_type , reaction_time , mol_frac , surface_mol_frac = surface_mol_frac , T0 = T , P0 = P ))
189224
190225 else :
191226 raise Exception ("Cantera conditions must leave one of T0, P0, and V0 state variables unspecified" )
@@ -198,7 +233,7 @@ class Cantera(object):
198233 """
199234
200235 def __init__ (self , species_list = None , reaction_list = None , canteraFile = '' , output_directory = '' , conditions = None ,
201- sensitive_species = None , thermo_SA = False ):
236+ sensitive_species = None , thermo_SA = False , surface_species_list = None , surface_reaction_list = None ):
202237 """
203238 `species_list`: list of RMG species objects
204239 `reaction_list`: list of RMG reaction objects
@@ -208,24 +243,36 @@ def __init__(self, species_list=None, reaction_list=None, canteraFile='', output
208243 `sensitive_species`: a list of RMG species objects for conductng sensitivity analysis on
209244 `thermo_SA`: a boolean indicating whether or not to run thermo SA. By default, if sensitive_species is given,
210245 only kinetic_SA will be calculated and it must be additionally specified to perform thermo SA.
246+ `surface_species_list`: list of RMG species objects contianing the surface species. This is necessary for all surface mechanisms to initialize the starting mole fractions.
247+ `surface_reaction_list`: list of RMG reaction objects containing the surface reactions.
248+ For surface mechanisms, either canteraFile must be provided, or load_chemkin_model() must be called later on to create the Cantera file with the surface mechanism.
211249 """
212250 self .species_list = species_list
213251 self .reaction_list = reaction_list
214252 self .reaction_map = {}
215253 self .model = ct .Solution (canteraFile ) if canteraFile else None
254+ self .surface = bool (surface_species_list or surface_reaction_list )
255+ self .surface_species_list = surface_species_list
256+ self .surface_reaction_list = surface_reaction_list
216257 self .output_directory = output_directory if output_directory else os .getcwd ()
217258 self .conditions = conditions if conditions else []
218259 self .sensitive_species = sensitive_species if sensitive_species else []
219260 self .thermo_SA = thermo_SA
220261
262+ if self .surface :
263+ assert surface_species_list , "Surface species list must be specified to run a surface simulation"
264+ if canteraFile :
265+ self .surface = ct .Interface (canteraFile , 'surface1' )
266+ self .model = self .surface .adjacent ['gas' ]
267+
221268 # Make output directory if it does not yet exist:
222269 if not os .path .exists (self .output_directory ):
223270 try :
224271 os .makedirs (self .output_directory )
225272 except :
226273 raise Exception ('Cantera output directory could not be created.' )
227274
228- def generate_conditions (self , reactor_type_list , reaction_time_list , mol_frac_list , Tlist = None , Plist = None , Vlist = None ):
275+ def generate_conditions (self , reactor_type_list , reaction_time_list , mol_frac_list , surface_mol_frac_list = None , Tlist = None , Plist = None , Vlist = None ):
229276 """
230277 This saves all the reaction conditions into the Cantera class.
231278 ======================= ====================================================
@@ -240,19 +287,19 @@ def generate_conditions(self, reactor_type_list, reaction_time_list, mol_frac_li
240287 `reaction_time_list` A tuple object giving the ([list of reaction times], units)
241288 `mol_frac_list` A list of molfrac dictionaries with species object keys
242289 and mole fraction values
290+ `surface_mol_frac_list` A list of molfrac dictionaries with surface species object keys and mole fraction values
243291 To specify the system for an ideal gas, you must define 2 of the following 3 parameters:
244292 `T0List` A tuple giving the ([list of initial temperatures], units)
245293 'P0List' A tuple giving the ([list of initial pressures], units)
246294 'V0List' A tuple giving the ([list of initial specific volumes], units)
247295 """
248296
249- self .conditions = generate_cantera_conditions (reactor_type_list , reaction_time_list , mol_frac_list , Tlist , Plist , Vlist )
297+ self .conditions = generate_cantera_conditions (reactor_type_list , reaction_time_list , mol_frac_list , surface_mol_frac_list , Tlist , Plist , Vlist )
250298
251299 def load_model (self ):
252300 """
253301 Load a cantera Solution model from the job's own species_list and reaction_list attributes
254302 """
255-
256303 ct_species = [spec .to_cantera (use_chemkin_identifier = True ) for spec in self .species_list ]
257304
258305 self .reaction_map = {}
@@ -292,6 +339,7 @@ def load_chemkin_model(self, chemkin_file, transport_file=None, **kwargs):
292339 Convert a chemkin mechanism chem.inp file to a cantera mechanism file chem.yaml
293340 and save it in the output_directory
294341 Then load it into self.model
342+ Note that if this is a surface mechanism, the calling function much include surface_file as a keyword argument for the parser
295343 """
296344 from cantera import ck2yaml
297345
@@ -301,9 +349,14 @@ def load_chemkin_model(self, chemkin_file, transport_file=None, **kwargs):
301349 if os .path .exists (out_name ):
302350 os .remove (out_name )
303351 parser = ck2yaml .Parser ()
352+
304353 parser .convert_mech (chemkin_file , transport_file = transport_file , out_name = out_name , ** kwargs )
305354 self .model = ct .Solution (out_name )
306355
356+ if self .surface :
357+ self .surface = ct .Interface (out_name , 'surface1' )
358+ self .model = self .surface .adjacent ['gas' ]
359+
307360 def modify_reaction_kinetics (self , rmg_reaction_index , rmg_reaction ):
308361 """
309362 Modify the corresponding cantera reaction's kinetics to match
@@ -385,14 +438,23 @@ def simulate(self):
385438 species_names_list = [get_species_identifier (species ) for species in self .species_list ]
386439 inert_index_list = [self .species_list .index (species ) for species in self .species_list if species .index == - 1 ]
387440
441+ if self .surface :
442+ surface_species_names_list = [get_species_identifier (species ) for species in self .surface_species_list ]
443+
388444 all_data = []
389445 for condition in self .conditions :
390446
391447 # First translate the mol_frac from species objects to species names
392448 new_mol_frac = {}
393- for key , value in condition .mol_frac .items ():
394- newkey = get_species_identifier (key )
395- new_mol_frac [newkey ] = value
449+ for rmg_species , mol_frac in condition .mol_frac .items ():
450+ species_name = get_species_identifier (rmg_species )
451+ new_mol_frac [species_name ] = mol_frac
452+
453+ if self .surface :
454+ new_surface_mol_frac = {}
455+ for rmg_species , mol_frac in condition .surface_mol_frac .items ():
456+ species_name = get_species_identifier (rmg_species )
457+ new_surface_mol_frac [species_name ] = mol_frac
396458
397459 # Set Cantera simulation conditions
398460 if condition .V0 is None :
@@ -413,6 +475,11 @@ def simulate(self):
413475 else :
414476 raise Exception ('Other types of reactor conditions are currently not supported' )
415477
478+ # add the surface/gas adjacent thing...
479+ if self .surface :
480+ rsurf = ct .ReactorSurface (self .surface , cantera_reactor )
481+ self .surface .TPX = condition .T0 .value_si , condition .P0 .value_si , new_surface_mol_frac
482+
416483 # Run this individual condition as a simulation
417484 cantera_simulation = ct .ReactorNet ([cantera_reactor ])
418485
@@ -451,7 +518,12 @@ def simulate(self):
451518 times .append (cantera_simulation .time )
452519 temperature .append (cantera_reactor .T )
453520 pressure .append (cantera_reactor .thermo .P )
454- species_data .append (cantera_reactor .thermo [species_names_list ].X )
521+
522+ if self .surface :
523+ species_data .append (np .concatenate ((cantera_reactor .thermo [species_names_list ].X , rsurf .kinetics .coverages )))
524+ N_gas = len (cantera_reactor .thermo [species_names_list ].X )
525+ else :
526+ species_data .append (cantera_reactor .thermo [species_names_list ].X )
455527
456528 if self .sensitive_species :
457529 # Cantera returns mass-based sensitivities rather than molar concentration or mole fraction based sensitivities.
@@ -531,6 +603,15 @@ def simulate(self):
531603 index = species .index
532604 )
533605 condition_data .append (species_generic_data )
606+ if self .surface :
607+ for index , species in enumerate (self .surface_species_list ):
608+ # Create generic data object that saves the surface species object into the species object.
609+ species_generic_data = GenericData (label = surface_species_names_list [index ],
610+ species = species ,
611+ data = species_data [:, index + N_gas ],
612+ index = species .index + N_gas
613+ )
614+ condition_data .append (species_generic_data )
534615
535616 # save kinetic data as generic data object
536617 reaction_sensitivity_data = []
0 commit comments