@@ -21,7 +21,7 @@ def generate_flux(model_path: str,
2121 composition : Dict [str , float ],
2222 T : float ,
2323 P : float ,
24- V : Optional [float ] = None ,
24+ V : Optional [float ] = 100 ,
2525 reactor_type : str = 'JSR' ,
2626 energy : bool = False ,
2727 a_tol : float = 1e-16 ,
@@ -52,7 +52,7 @@ def generate_flux(model_path: str,
5252 values are mole fractions.
5353 T (float): The temperature of the mixture, in Kelvin.
5454 P (float): The pressure of the mixture, in bar.
55- V (Optional[float], optional): The reactor volume in cm^3, if relevant.
55+ V (Optional[float], optional): The reactor volume in cm^3, if relevant (fot JSR and PFR, length is computed as V / A) .
5656 reactor_type (str, optional): The reactor type. Supported reactor types are:
5757 'JSR': Jet stirred reactor, which is a CSTR with constant T/P/V
5858 'BatchP': An ideal gas constant pressure and constant volume batch reactor
@@ -109,6 +109,7 @@ def generate_flux(model_path: str,
109109 generate_flux_diagrams (profiles = profiles ,
110110 observables = [observable ],
111111 folder_path = folder_path_observable ,
112+ reactor_type = reactor_type ,
112113 explore_tol = explore_tol ,
113114 dead_end_tol = dead_end_tol ,
114115 display_concentrations = display_concentrations ,
@@ -123,6 +124,7 @@ def generate_flux(model_path: str,
123124 generate_flux_diagrams (profiles = profiles ,
124125 observables = observables ,
125126 folder_path = folder_path ,
127+ reactor_type = reactor_type ,
126128 explore_tol = explore_tol ,
127129 dead_end_tol = dead_end_tol ,
128130 display_concentrations = display_concentrations ,
@@ -161,6 +163,7 @@ def get_profiles_from_simulation(model_path: str,
161163 reactor_type (str, optional): The reactor type. Supported reactor types are:
162164 'JSR': Jet stirred reactor, which is a CSTR with constant T/P/V
163165 'BatchP': An ideal gas constant pressure and constant volume batch reactor
166+ 'PFR': Plug flow reactor, which is a series of CSTRs with constant T/P/V
164167 a_tol (float, optional): The absolute tolerance for the simulation.
165168 r_tol (float, optional): The relative tolerance for the simulation.
166169 energy (bool, optional): Whether to turn energy equations on (False means adiabatic conditions).
@@ -191,6 +194,18 @@ def get_profiles_from_simulation(model_path: str,
191194 a_tol = a_tol ,
192195 r_tol = r_tol ,
193196 )
197+ elif reactor_type == 'PFR' :
198+ profiles = run_pfr (model_path = model_path ,
199+ times = times ,
200+ composition = composition ,
201+ T = T ,
202+ P = P ,
203+ length = V * 1e-6 / 1.0 ,
204+ area = 1.0 , # m^2
205+ n_cells = 10 ,
206+ a_tol = a_tol ,
207+ r_tol = r_tol ,
208+ )
194209 else :
195210 raise NotImplementedError (f'Reactor type { reactor_type } is not yet supported' )
196211 return profiles
@@ -227,6 +242,65 @@ def mark_rxn_duplicity_correctly(gas: ct.Solution):
227242 rxn .duplicate = True
228243
229244
245+ def set_pfr (gas : ct .Solution ,
246+ model_path : str ,
247+ length : float ,
248+ area : float ,
249+ n_cells : int ,
250+ composition : Dict [str , float ],
251+ T : float ,
252+ P : float ,
253+ flow_rate : float ,
254+ a_tol : float = 1e-16 ,
255+ r_tol : float = 1e-10 ,
256+ ) -> Tuple [ct .ReactorNet , List [ct .IdealGasReactor ]]:
257+ """
258+ Set up a plug flow reactor (PFR) using a series of CSTRs.
259+
260+ Args:
261+ gas (ct.Solution): The Cantera Solution object.
262+ model_path (str): The path to the cantera YAML model file.
263+ length (float): Length of the reactor in meters.
264+ area (float): Cross-sectional area in m^2.
265+ n_cells (int): Number of discrete CSTRs to simulate the PFR.
266+ composition (Dict[str, float]): Inlet composition.
267+ T (float): Inlet temperature in K.
268+ P (float): Inlet pressure in Pa.
269+ flow_rate (float): Mass flow rate in kg/s.
270+ a_tol (float): Absolute tolerance.
271+ r_tol (float): Relative tolerance.
272+
273+ Returns:
274+ Tuple[ct.ReactorNet, List[ct.IdealGasReactor]]:
275+ - The reactor network
276+ - List of reactors representing the PFR
277+ """
278+ gas .TPX = T , P , composition
279+ inlet = ct .Reservoir (gas )
280+ outlet = ct .Reservoir (gas )
281+ total_volume = length * area
282+ cell_volume = total_volume / n_cells
283+ reactors = []
284+ upstream = inlet
285+
286+ for _ in range (n_cells ):
287+ gas_cell = ct .Solution (model_path )
288+ gas_cell .TPX = gas .TPX
289+ reactor = ct .IdealGasReactor (gas_cell , energy = "off" , volume = cell_volume )
290+ mfc = ct .MassFlowController (upstream , reactor , mdot = flow_rate )
291+ reactors .append (reactor )
292+ upstream = reactor
293+
294+ # Last reactor connects to outlet reservoir
295+ ct .PressureController (upstream = reactors [- 1 ], downstream = outlet , master = mfc )
296+
297+ network = ct .ReactorNet (reactors )
298+ network .atol = a_tol
299+ network .rtol = r_tol
300+
301+ return network , reactors
302+
303+
230304def set_jsr (gas : ct .Solution ,
231305 time : float ,
232306 composition : Dict [str , float ],
@@ -330,6 +404,78 @@ def closest_bigger_number(array: List[float],
330404 return None , None
331405
332406
407+ def run_pfr (model_path : str ,
408+ times : List [float ],
409+ composition : Dict [str , float ],
410+ T : float ,
411+ P : float ,
412+ length : float ,
413+ area : float ,
414+ n_cells : int = 10 ,
415+ a_tol : float = 1e-16 ,
416+ r_tol : float = 1e-10 ,
417+ ) -> Dict [float , dict ]:
418+ """
419+ Run a PFR simulation using a series of CSTRs with constant T, P.
420+
421+ Args:
422+ model_path (str): The path to the cantera YAML model file.
423+ times (List[float]): List of residence times to run (s).
424+ composition (Dict[str, float]): Inlet composition.
425+ T (float): Temperature in K.
426+ P (float): Pressure in Pa.
427+ length (float): Reactor length in meters.
428+ area (float): Reactor cross-sectional area in m^2.
429+ n_cells (int): Number of CSTRs to discretize the PFR.
430+ a_tol (float): Absolute tolerance.
431+ r_tol (float): Relative tolerance.
432+
433+ Returns:
434+ dict: Outlet profiles (T, P, X, ROPs) for each residence time.
435+ """
436+ gas = ct .Solution (model_path )
437+ profiles = {}
438+ stoichiometry = get_rxn_stoichiometry (gas )
439+ for tau in times :
440+ V_total = length * area
441+ gas .TPX = T , P , composition
442+ rho = gas .density
443+ total_mass = rho * V_total
444+ flow_rate = total_mass / tau # kg/s
445+ network , reactors = set_pfr (
446+ gas = gas ,
447+ model_path = model_path ,
448+ length = length ,
449+ area = area ,
450+ n_cells = n_cells ,
451+ composition = composition ,
452+ T = T ,
453+ P = P ,
454+ flow_rate = flow_rate ,
455+ a_tol = a_tol ,
456+ r_tol = r_tol ,
457+ )
458+
459+ network .advance (tau )
460+ outlet_reactor = reactors [- 1 ]
461+ gas_out = outlet_reactor .thermo
462+ rops = {spc .name : dict () for spc in gas .species ()}
463+ cantera_reaction_rops = gas_out .net_rates_of_progress
464+ for spc in gas .species ():
465+ for i , rxn in enumerate (gas .reactions ()):
466+ if stoichiometry [spc .name ][i ]:
467+ if rxn .equation not in rops [spc .name ]:
468+ rops [spc .name ][rxn .equation ] = 0
469+ rops [spc .name ][rxn .equation ] += cantera_reaction_rops [i ] * stoichiometry [spc .name ][i ]
470+ profile = {'T' : gas_out .T ,
471+ 'P' : gas_out .P ,
472+ 'X' : {s .name : x for s , x in zip (gas_out .species (), gas_out .X )},
473+ 'ROPs' : rops ,
474+ }
475+ profiles [tau ] = profile
476+ return profiles
477+
478+
333479def run_jsr (gas : ct .Solution ,
334480 times : List [float ],
335481 composition : Dict [str , float ],
@@ -521,6 +667,7 @@ def generate_top_rop_bar_figs(profiles: dict,
521667def generate_flux_diagrams (profiles : dict ,
522668 observables : List [str ],
523669 folder_path : str ,
670+ reactor_type : str ,
524671 explore_tol : float = 0.95 ,
525672 dead_end_tol : float = 0.10 ,
526673 display_concentrations : bool = True ,
@@ -538,6 +685,7 @@ def generate_flux_diagrams(profiles: dict,
538685 profiles (dict): The T, P, X, and ROP profiles (values) at a specific time.
539686 observables (List[str]): The species to start the flux diagram with.
540687 folder_path (str): The path to the folder in which to save the flux diagrams and accompanied data.
688+ reactor_type (str): The reactor type.
541689 explore_tol (float, optional): The minimal flux to capture of each species consumption pathway.
542690 dead_end_tol (float, optional): A flux exploration termination criterion.
543691 Don't explore further consumption is lower than this tolerance
@@ -574,6 +722,7 @@ def generate_flux_diagrams(profiles: dict,
574722 min_rop = min_rop ,
575723 max_rop = max_rop ,
576724 folder_path = folder_path ,
725+ reactor_type = reactor_type ,
577726 display_concentrations = display_concentrations ,
578727 report_flux_ratio = report_flux_ratio ,
579728 report_actual_flux = report_actual_flux ,
@@ -591,6 +740,7 @@ def create_digraph(flux_graph: dict,
591740 min_rop : float ,
592741 max_rop : float ,
593742 folder_path : str ,
743+ reactor_type : str ,
594744 display_concentrations : bool = True ,
595745 report_flux_ratio : bool = True ,
596746 report_actual_flux : bool = False ,
@@ -610,6 +760,7 @@ def create_digraph(flux_graph: dict,
610760 min_rop (float): The absolute minimal ROP value.
611761 max_rop (float): The absolute maximal ROP value.
612762 folder_path (str): The path to the folder in which to save the flux diagrams and accompanied data.
763+ reactor_type (str): The reactor type.
613764 display_concentrations (bool, optional): Whether to display the concentrations.
614765 report_flux_ratio (bool, optional): Whether to display the flux ratio.
615766 report_actual_flux (bool, optional): Whether to report the actual flux values rather than the relative flux.
@@ -681,8 +832,8 @@ def create_digraph(flux_graph: dict,
681832 display_r_n_p = display_r_n_p ,
682833 allowed_nodes = allowed_nodes ,
683834 )
684- graph .set (name = 'label' , value = f'Flux diagram at { time } s, ROP range: [{ min_rop :.2e} , { max_rop :.2e} ] ' +
685- 'mol/cm\N{SUPERSCRIPT THREE} /s)' )
835+ graph .set (name = 'label' , value = f'{ reactor_type } Flux diagram at { time } s, ROP range: [{ min_rop :.2e} , { max_rop :.2e} ] ' +
836+ '( mol/cm\N{SUPERSCRIPT THREE} /s)' )
686837 if scaling is not None :
687838 graph .set ('size' , f'{ scaling } ,{ scaling } ' )
688839 if allowed_nodes is not None :
0 commit comments