Skip to content

Commit 0f52392

Browse files
committed
Added a PFR reactor to the flux diagram generator
1 parent f3dcc84 commit 0f52392

File tree

1 file changed

+155
-4
lines changed

1 file changed

+155
-4
lines changed

t3/utils/flux.py

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
230304
def 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+
333479
def 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,
521667
def 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

Comments
 (0)