def setup(order): gas1.TPX = 1200, 1e3, 'H:0.002, H2:1, CH4:0.01, CH3:0.0002' gas2.TPX = 900, 101325, 'H2:0.1, OH:1e-7, O2:0.1, AR:1e-5' net = ct.ReactorNet() rA = ct.IdealGasReactor(gas1) rB = ct.IdealGasReactor(gas2) if order % 2 == 0: surfX = ct.ReactorSurface(interface, rA, A=0.1) surfY = ct.ReactorSurface(interface, rA, A=10) else: surfY = ct.ReactorSurface(interface, rA, A=10) surfX = ct.ReactorSurface(interface, rA, A=0.1) C1 = np.zeros(interface.n_species) C2 = np.zeros(interface.n_species) C1[0] = 0.3 C1[4] = 0.7 C2[0] = 0.9 C2[4] = 0.1 surfX.coverages = C1 surfY.coverages = C2 if order // 2 == 0: net.add_reactor(rA) net.add_reactor(rB) else: net.add_reactor(rB) net.add_reactor(rA) return rA, rB, surfX, surfY, net
def create_reactors(self, add_Q=False, add_mdot=False, add_surf=False): self.gas = ct.Solution('gri30.xml') self.gas.TPX = 900, 25*ct.one_atm, 'CO:0.5, H2O:0.2' self.gas1 = ct.Solution('gri30.xml') self.gas1.ID = 'gas' self.gas2 = ct.Solution('gri30.xml') self.gas2.ID = 'gas' resGas = ct.Solution('gri30.xml') solid = ct.Solution('diamond.xml', 'diamond') T0 = 1200 P0 = 25*ct.one_atm X0 = 'CH4:0.5, H2O:0.2, CO:0.3' self.gas1.TPX = T0, P0, X0 self.gas2.TPX = T0, P0, X0 self.r1 = ct.IdealGasReactor(self.gas1) self.r2 = self.reactorClass(self.gas2) self.r1.volume = 0.2 self.r2.volume = 0.2 resGas.TP = T0 - 300, P0 env = ct.Reservoir(resGas) U = 300 if add_Q else 0 self.w1 = ct.Wall(self.r1, env, K=1e3, A=0.1, U=U) self.w2 = ct.Wall(self.r2, env, A=0.1, U=U) if add_mdot: mfc1 = ct.MassFlowController(env, self.r1, mdot=0.05) mfc2 = ct.MassFlowController(env, self.r2, mdot=0.05) if add_surf: self.interface1 = ct.Interface('diamond.xml', 'diamond_100', (self.gas1, solid)) self.interface2 = ct.Interface('diamond.xml', 'diamond_100', (self.gas2, solid)) C = np.zeros(self.interface1.n_species) C[0] = 0.3 C[4] = 0.7 self.surf1 = ct.ReactorSurface(self.interface1, A=0.2) self.surf2 = ct.ReactorSurface(self.interface2, A=0.2) self.surf1.coverages = C self.surf2.coverages = C self.surf1.install(self.r1) self.surf2.install(self.r2) self.net1 = ct.ReactorNet([self.r1]) self.net2 = ct.ReactorNet([self.r2]) self.net1.set_max_time_step(0.05) self.net2.set_max_time_step(0.05) self.net2.max_err_test_fails = 10
def test_coverages_regression1(self): # Test with energy equation disabled self.make_reactors() self.r1.energy_enabled = False self.r2.energy_enabled = False surf1 = ct.ReactorSurface(self.interface, self.r1) C = np.zeros(self.interface.n_species) C[0] = 0.3 C[4] = 0.7 surf1.coverages = C self.assertArrayNear(surf1.coverages, C) data = [] test_file = pjoin(self.test_work_dir, 'test_coverages_regression1.csv') reference_file = pjoin(self.test_data_dir, 'WallKinetics-coverages-regression1.csv') data = [] for t in np.linspace(1e-6, 1e-3): self.net.advance(t) data.append([t, self.r1.T, self.r1.thermo.P, self.r1.mass] + list(self.r1.thermo.X) + list(surf1.coverages)) np.savetxt(test_file, data, delimiter=',') bad = utilities.compareProfiles(reference_file, test_file, rtol=1e-5, atol=1e-9, xtol=1e-12) self.assertFalse(bool(bad), bad)
def get_steady_state_starting_coverages( self, temperature_c, pressure, feed_mole_fractions, gas_volume_per_reactor, cat_area_per_gas_volume, ): """ To find the starting coverages, we run the gas to equilibrium, (i.e mostly burned products) then put that in steady state with the surface. May not be working """ gas, surf = self.gas, self.surf gas.TPX = temperature_c + 273.15, pressure, feed_mole_fractions TPY = gas.TPY # store to restore # gas.equilibrate('TP') r = ct.IdealGasReactor(gas, energy="off") r.volume = gas_volume_per_reactor cat_area_per_reactor = cat_area_per_gas_volume * gas_volume_per_reactor rsurf = ct.ReactorSurface(surf, r, A=cat_area_per_reactor) sim = ct.ReactorNet([r]) sim.advance(1e-3) surf() starting_coverages = surf.coverages gas.TPY = TPY # restore to starting conditions del (r, rsurf) return starting_coverages
def solve(gas, t): # Set the temperature and pressure for gas # t is different from t0 gas.TPX = t, pressure, composition surf.TP = t, pressure TDY = gas.TDY cov = surf.coverages # create a new reactor gas.TDY = TDY r = ct.IdealGasReactor(gas, energy='on') r.volume = rvol upstream = ct.Reservoir(gas, name='upstream') downstream = ct.Reservoir(gas, name='downstream') rsurf = ct.ReactorSurface(surf, r, A=cat_area) m = ct.MassFlowController(upstream, r, mdot=mass_flow_rate) v = ct.PressureController(r, downstream, master=m, K=1e-5) sim = ct.ReactorNet([r]) sim.max_err_test_fails = 20 sim.rtol = 1.0e-9 sim.atol = 1.0e-21 # define time, space, and other information vectors z2 = (np.arange(NReactors)) * rlen * 1e3 t_r2 = np.zeros_like(z2) # residence time in each reactor t2 = np.zeros_like(z2) states2 = ct.SolutionArray(gas) for n in range(NReactors): # Set the state of the reservoir to match that of the previous reactor gas.TDY = r.thermo.TDY upstream.syncState() sim.reinitialize() sim.advance_to_steady_state() dist = n * rlen * 1.0e3 # distance in mm t_r2[n] = r.mass / mass_flow_rate # residence time in this reactor t2[n] = np.sum(t_r2) states2.append(gas.state) print('Temperature of Gas :', t, ' residence time :', t2[-1]) MolFrac_CH4 = states2('CH4').X MolFrac_CO2 = states2('CO2').X MolFrac_CO = states2('CO').X MolFrac_H2 = states2('H2').X MolFrac_H2O = states2('H2O').X MolFrac_O2 = states2('O2').X kq = np.zeros(6) kq[0] = MolFrac_CH4[-1] * 100 kq[1] = MolFrac_CO2[-1] * 100 kq[2] = MolFrac_CO[-1] * 100 kq[3] = MolFrac_H2[-1] * 100 kq[4] = MolFrac_H2O[-1] * 100 kq[5] = MolFrac_O2[-1] * 100 return kq
def test_coverages(self): self.make_reactors() surf1 = ct.ReactorSurface(self.interface, self.r1) surf1.coverages = {'c6HH':0.3, 'c6HM':0.7} self.assertNear(surf1.coverages[0], 0.3) self.assertNear(surf1.coverages[1], 0.0) self.assertNear(surf1.coverages[4], 0.7) self.net.advance(1e-5) C_left = surf1.coverages self.make_reactors() surf2 = ct.ReactorSurface(self.interface, self.r2) surf2.coverages = 'c6HH:0.3, c6HM:0.7' self.assertNear(surf2.coverages[0], 0.3) self.assertNear(surf2.coverages[4], 0.7) self.net.advance(1e-5) C_right = surf2.coverages self.assertNear(sum(C_left), 1.0) self.assertArrayNear(C_left, C_right)
def test_sensitivities2(self): net = ct.ReactorNet() gas1 = ct.Solution('diamond.xml', 'gas') solid = ct.Solution('diamond.xml', 'diamond') interface = ct.Interface('diamond.xml', 'diamond_100', (gas1, solid)) r1 = ct.IdealGasReactor(gas1) net.add_reactor(r1) net.atol_sensitivity = 1e-10 net.rtol_sensitivity = 1e-8 gas2 = ct.Solution('h2o2.xml') gas2.TPX = 900, 101325, 'H2:0.1, OH:1e-7, O2:0.1, AR:1e-5' r2 = ct.IdealGasReactor(gas2) net.add_reactor(r2) surf = ct.ReactorSurface(interface, r1, A=1.5) C = np.zeros(interface.n_species) C[0] = 0.3 C[4] = 0.7 surf.coverages = C surf.add_sensitivity_reaction(2) r2.add_sensitivity_reaction(18) for T in (901, 905, 910, 950, 1500): while r2.T < T: net.step() S = net.sensitivities() # number of non-species variables in each reactor Ns = r1.component_index(gas1.species_name(0)) # Index of first variable corresponding to r2 K2 = Ns + gas1.n_species + interface.n_species # Constant volume should generate zero sensitivity coefficient self.assertArrayNear(S[1,:], np.zeros(2)) self.assertArrayNear(S[K2+1,:], np.zeros(2)) # Sensitivity coefficients for the disjoint reactors should be zero self.assertNear(np.linalg.norm(S[Ns:K2,1]), 0.0, atol=1e-5) self.assertNear(np.linalg.norm(S[K2+Ns:,0]), 0.0, atol=1e-5)
def run_reactor_ss( cti_file, t_array=[528], p_array=[75], v_array=[0.00424], h2_array=[0.75], co2_array=[0.5], rtol=1.0e-11, atol=1.0e-22, reactor_type=0, energy="off", sensitivity=False, sensatol=1e-6, sensrtol=1e-6, reactime=1e5, ): ''' run the reactor to steady state. saves a single CSV output file with one row of data. results saved in new folder marked "steady state" under reactor type ''' import pandas as pd import numpy as np import time import cantera as ct from matplotlib import pyplot as plt import csv import math import os import sys import re import itertools import logging from collections import defaultdict import git try: array_i = int(os.getenv("SLURM_ARRAY_TASK_ID")) except TypeError: array_i = 0 # get git commit hash and message repo = git.Repo("/work/westgroup/lee.ting/cantera/ammonia") git_sha = str(repo.head.commit)[0:6] git_msg = str(repo.head.commit.message)[0:20].replace(" ", "_").replace( "'", "_") # this should probably be outside of function settings = list( itertools.product(t_array, p_array, v_array, h2_array, co2_array)) # constants pi = math.pi # set initial temps, pressures, concentrations temp = settings[array_i][0] # kelvin temp_str = str(temp)[0:3] pressure = settings[array_i][1] * ct.one_atm # Pascals X_h2 = settings[array_i][3] x_h2_str = str(X_h2)[0:3].replace(".", "_") x_CO_CO2_str = str(settings[array_i][4])[0:3].replace(".", "_") if X_h2 == 0.75: X_h2o = 0.05 else: X_h2o = 0 X_co = (1 - (X_h2 + X_h2o)) * (settings[array_i][4]) X_co2 = (1 - (X_h2 + X_h2o)) * (1 - settings[array_i][4]) # normalize mole fractions just in case # X_co = X_co/(X_co+X_co2+X_h2) # X_co2= X_co2/(X_co+X_co2+X_h2) # X_h2 = X_h2/(X_co+X_co2+X_h2) mw_co = 28.01e-3 # [kg/mol] mw_co2 = 44.01e-3 # [kg/mol] mw_h2 = 2.016e-3 # [kg/mol] mw_h2o = 18.01528e-3 # [kg/mol] co2_ratio = X_co2 / (X_co + X_co2) h2_ratio = (X_co2 + X_co) / X_h2 # CO/CO2/H2/H2: typical is concentrations_rmg = {"O2(2)": X_co, "NH3(6)": X_co2, "He": X_h2} # initialize cantera gas and surface gas = ct.Solution(cti_file, "gas") # surf_grab = ct.Interface(cti_file,'surface1_grab', [gas_grab]) surf = ct.Interface(cti_file, "surface1", [gas]) # gas_grab.TPX = gas.TPX = temp, pressure, concentrations_rmg surf.TP = temp, pressure # create gas inlet inlet = ct.Reservoir(gas) # create gas outlet exhaust = ct.Reservoir(gas) # Reactor volume rradius = 35e-3 rlength = 70e-3 rvol = (rradius**2) * pi * rlength # Catalyst Surface Area site_density = (surf.site_density * 1000 ) # [mol/m^2]cantera uses kmol/m^2, convert to mol/m^2 cat_weight = 4.24e-3 # [kg] cat_site_per_wt = (300 * 1e-6) * 1000 # [mol/kg] 1e-6mol/micromole, 1000g/kg cat_area = site_density / (cat_weight * cat_site_per_wt) # [m^3] # reactor initialization if reactor_type == 0: r = ct.Reactor(gas, energy=energy) reactor_type_str = "Reactor" elif reactor_type == 1: r = ct.IdealGasReactor(gas, energy=energy) reactor_type_str = "IdealGasReactor" elif reactor_type == 2: r = ct.ConstPressureReactor(gas, energy=energy) reactor_type_str = "ConstPressureReactor" elif reactor_type == 3: r = ct.IdealGasConstPressureReactor(gas, energy=energy) reactor_type_str = "IdealGasConstPressureReactor" rsurf = ct.ReactorSurface(surf, r, A=cat_area) r.volume = rvol surf.coverages = "X(1):1.0" # flow controllers (Graaf measured flow at 293.15 and 1 atm) one_atm = ct.one_atm FC_temp = 293.15 volume_flow = settings[array_i][2] # [m^3/s] molar_flow = volume_flow * one_atm / (8.3145 * FC_temp) # [mol/s] mass_flow = molar_flow * (X_co * mw_co + X_co2 * mw_co2 + X_h2 * mw_h2 + X_h2o * mw_h2o) # [kg/s] mfc = ct.MassFlowController(inlet, r, mdot=mass_flow) # A PressureController has a baseline mass flow rate matching the 'master' # MassFlowController, with an additional pressure-dependent term. By explicitly # including the upstream mass flow rate, the pressure is kept constant without # needing to use a large value for 'K', which can introduce undesired stiffness. outlet_mfc = ct.PressureController(r, exhaust, master=mfc, K=0.01) # initialize reactor network sim = ct.ReactorNet([r]) # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-11 sim.atol = 1.0e-22 ################################################# # Run single reactor ################################################# # round numbers so they're easier to read # temp_str = '%s' % '%.3g' % tempn cat_area_str = "%s" % "%.3g" % cat_area results_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/steady_state/{temp_str}/results" ) flux_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/steady_state/{temp_str}/flux_diagrams/{x_h2_str}/{x_CO_CO2_str}" ) try: os.makedirs(results_path, exist_ok=True) except OSError as error: print(error) try: os.makedirs(flux_path, exist_ok=True) except OSError as error: print(error) gas_ROP_str = [i + " ROP [kmol/m^3 s]" for i in gas.species_names] # surface ROP reports gas and surface ROP. these values are not redundant. gas_surf_ROP_str = [ i + " surface ROP [kmol/m^2 s]" for i in gas.species_names ] surf_ROP_str = [i + " ROP [kmol/m^2 s]" for i in surf.species_names] gasrxn_ROP_str = [ i + " ROP [kmol/m^3 s]" for i in gas.reaction_equations() ] surfrxn_ROP_str = [ i + " ROP [kmol/m^2 s]" for i in surf.reaction_equations() ] output_filename = ( results_path + f"/Spinning_basket_area_{cat_area_str}_energy_{energy}" + f"_temp_{temp}_h2_{x_h2_str}_COCO2_{x_CO_CO2_str}.csv") outfile = open(output_filename, "w") writer = csv.writer(outfile) writer.writerow([ "T (C)", "P (atm)", "V (M^3/s)", "X_co initial", "X_co2 initial", "X_h2 initial", "X_h2o initial", "CO2/(CO2+CO)", "(CO+CO2/H2)", "T (C) final", "Rtol", "Atol", "reactor type", ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str) # Run Simulation to steady state sim.advance_to_steady_state() # Record steady state data to CSV writer.writerow([ temp, pressure, volume_flow, X_co, X_co2, X_h2, X_h2o, co2_ratio, h2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress)) outfile.close() # save flux diagrams at the end of the run save_flux_diagrams(gas, suffix=flux_path, timepoint="end") save_flux_diagrams(surf, suffix=flux_path, timepoint="end") return
def __init__(self, cti_file, init_X, inlet_X, inlet_F, volume, n_cstr=0, P=ct.one_atm, T=298, area=None, K=1e-5, kin_param_to_set=None): ''' parameters: cti_file: str, the cti file must contain a gas phase named 'gas' and optionally a reactive surface named 'surface' init_X: dict, array or list of them initial composition of the reactors ''' if not HAS_CANTERA: raise SpectroChemPyException( 'Cantera is not available : please install it before continuing: \n' 'conda install -c cantera cantera') if area is None: add_surface = False else: add_surface = True # copy inlet parameters (for copy) self._cti = cti_file self._init_X = init_X self._inlet_X = inlet_X self._inlet_F = inlet_F self._volume = volume self.T = T self.P = P self._area = area self._K = K self._kin_param_to_set = kin_param_to_set self.cstr = [] # list of cstrs self.surface = [] # reactor surfaces of cstr's self._mfc = [] # mass flow self.inlet = [] # reservoirs self.event = None self._pc = [] # pressure controllers if isinstance(self._volume, (float, int)): self._volume = self._volume * np.ones((n_cstr)) / n_cstr if add_surface and isinstance(area, (float, int)): self._area = self.area * np.ones((n_cstr)) / n_cstr self.n_cstr = len(volume) # first cstr initial_gas = ct.Solution(self._cti, 'gas') initial_gas.TPX = self.T, self.P, init_X self.n_gas_species = len(initial_gas.X) self.cstr.append( ct.IdealGasReactor(initial_gas, name="R_0", energy='off')) self.cstr[0].volume = volume[0] if add_surface: surface = ct.Interface(self._cti, phaseid='surface', phases=[initial_gas]) if kin_param_to_set is not None: modify_surface_kinetics(surface, kin_param_to_set) self.n_surface_species = len(surface.X) self.surface.append( ct.ReactorSurface(kin=surface, r=self.cstr[0], A=area[0])) # create and connect inlets to R_0 if not isinstance(inlet_X, Iterable): inlet_X = [inlet_X] inlet_F = [inlet_F] self._inlet_F = inlet_F for i, (X, F) in enumerate(zip(inlet_X, self._inlet_F)): inlet_gas = ct.Solution(self._cti, 'gas') inlet_gas.TPX = self.T, self.P, X self.inlet.append( ct.Reservoir(contents=inlet_gas, name=f'inlet_{i}')) self._mfc.append( ct.MassFlowController(self.inlet[-1], self.cstr[0], name=f'MFC_{i}')) if not callable(F): self._mfc[-1].set_mass_flow_rate(F * inlet_gas.density) else: # it is tricky to pass non explicit lambda functions to MassFlowControllers # the following works while use of self._inlet_F[i](t) generate an error # when using reactorNet.advance() if i == 0: self._mfc[-1].set_mass_flow_rate(lambda t: self._inlet_F[0] (t) * inlet_gas.density) elif i == 1: self._mfc[-1].set_mass_flow_rate(lambda t: self._inlet_F[1] (t) * inlet_gas.density) elif i == 2: self._mfc[-1].set_mass_flow_rate(lambda t: self._inlet_F[2] (t) * inlet_gas.density) elif i == 3: self._mfc[-1].set_mass_flow_rate(lambda t: self._inlet_F[3] (t) * inlet_gas.density) elif i == 4: self._mfc[-1].set_mass_flow_rate(lambda t: self._inlet_F[4] (t) * inlet_gas.density) else: raise ValueError( "variable flow rate(s) must be associated within the first" "five MFC(s)") # create other cstrs and link them with the previous one through a pressure controller for i in range(1, len(volume)): initial_gas = ct.Solution(self._cti, 'gas') initial_gas.TPX = self.T, self.P, init_X self.cstr.append( ct.IdealGasReactor(initial_gas, name="R_0", energy='off')) self.cstr[i].volume = volume[i] if add_surface: surface = ct.Interface(self._cti, phaseid='surface', phases=[initial_gas]) self.n_surface_species = len(surface.X) if kin_param_to_set is not None: modify_surface_kinetics(surface, kin_param_to_set) self.surface.append( ct.ReactorSurface(kin=surface, r=self.cstr[i], A=area[i])) self._pc.append( ct.PressureController(self.cstr[i - 1], self.cstr[i], master=self._mfc[-1], K=K)) # create the event event_gas = ct.Solution(self._cti, 'gas') event_gas.TPX = self.T, self.P, init_X self.event = ct.Reservoir(contents=event_gas, name=f'event') self._pc.append( ct.PressureController(self.cstr[-1], self.event, master=self._mfc[-1], K=K)) self.X = np.ones((self.n_cstr, self.n_gas_species)) self.coverages = np.ones((self.n_cstr, self.n_surface_species)) for i, (r, s) in enumerate(zip(self.cstr, self.surface)): self.X[i, :] = r.thermo.X self.coverages[i, :] = s.coverages self.net = ct.ReactorNet(self.cstr)
products = ct.Solution(cti_file, 'products') products.TP = 300.0, 1.0 * ct.one_atm q4 = ct.Quantity(products, mass=0) K2S = ct.Solution(cti_file, 'potsulf') K2S.TP = 300.0, 1.0 * ct.one_atm q5 = ct.Quantity(K2S, mass=0) inter = ct.Interface(cti_file, 'surface', [q1, q2, q3, q4, q5]) inter.TP = 500.0, 1.0 * ct.one_atm env = ct.Reservoir(air) reactor = ct.Reactor(q1 + q2 + q3 + q4 + q5) reactor.volume = lengthstart * area ct.ReactorSurface(surface, reactor, A=(mgp / (1700)) * (6 / 10 ^ -3)) w = ct.Wall(reactor, env, A=area, K=tstep * area / mpiston) sim = ct.ReactorNet([reactor, env]) print "Beginning simulation" time = 0.0 outfile = open('output.csv', 'w') csvfile = csv.writer(outfile) csvfile.writerow("time [s]", "temperature [K]", "pressure [Pa]", "volume [m^3]", "velocity [m/s]") print "Current simulation time:"
def simulatePFRorTPRwithCantera(model_name, canteraGasPhaseObject, canteraSurfacePhaseObject, simulation_settings_module): #simulation_settings_module_name must be a string if it is provided. The module itself should not be passed as an argument. This is intentional. canteraPhases ={} gas = canteraGasPhaseObject surf = canteraSurfacePhaseObject canteraPhases['gas'] = gas canteraPhases['surf'] = surf '''Now the code that handles *either* isothermal PFR or surface TPR. In future, will allow PFR TPR as well''' #We are going to model things as a flow reactor made of CSTRs, with no flow for the surface TPR case, following this example: https://cantera.org/examples/python/reactors/surf_pfr.py.html #it could probably also have been done as a flow reactor with no flow: https://cantera.org/examples/python/surface_chemistry/catalytic_combustion.py.html '''START OF settings that users should change.''' flow_type = simulation_settings_module.flow_type T_gas_feed = simulation_settings_module.T_gas_feed T_surf = simulation_settings_module.T_surf velocity = simulation_settings_module.velocity reactor_cross_area = simulation_settings_module.reactor_cross_area cat_area_per_vol = simulation_settings_module.cat_area_per_vol porosity = simulation_settings_module.porosity length = simulation_settings_module.length P_gas = simulation_settings_module.P_gas gas_composition = simulation_settings_module.gas_composition t_step_size = simulation_settings_module.t_step_size t_final = simulation_settings_module.t_final NReactors = simulation_settings_module.NReactors print_frequency = simulation_settings_module.print_frequency heating_rate = simulation_settings_module.heating_rate surface_coverages = simulation_settings_module.surface_coverages rtol = simulation_settings_module.rtol atol = simulation_settings_module.atol exportOutputs = simulation_settings_module.exportOutputs '''END OF settings that users should change.''' #Initiate concentrations output file and headers. if exportOutputs == True: concentrations_output_filename = model_name + "_output_concentrations.csv" outfile = open(concentrations_output_filename,'w') writer = csv.writer(outfile) concentrationsArrayHeaderList = ['Distance (m)', 'time(s)', 'T_gas (K)', 'T_surf (K)', 'P (atm)'] + \ gas.species_names + surf.species_names concentrationsArrayHeader = str(concentrationsArrayHeaderList)[1:-1] #The species names were imported when "surf" and "gas" objects were created. if exportOutputs == True: writer.writerow(concentrationsArrayHeaderList) if flow_type == "Static": velocity = 0 NReactors = 2 #In the surf_pfr example. For static, we only need 1 reactor, but the rlen formula below has a minimum value of 2. #FIXME num_t_steps = ceil(t_final / t_step_size) #rounds up. rlen = length/(NReactors-1) #This is each individual CSTR's length. #FIXME: Why is this not just Nreactors? Is it because of reservoirs?... #rvol is the reactor volume. We're modeling this as as a single CSTR with no flow. rvol = reactor_cross_area * rlen * porosity #Each individual CSTR gas volume reduced by porosity. # catalyst area in one reactor cat_area = cat_area_per_vol * rvol #Set the initial conditions for the gas and the surface. gas.TPX = T_gas_feed, P_gas, gas_composition surf.TP = T_surf, P_gas surf.X = surface_coverages mass_flow_rate = velocity * reactor_cross_area * gas.density #linear velocity times area is volume/s, times kg/vol becomes kg/s. I think that for this example we neglect effects of surface adsorption regarding how much mass is in the gas phase? TDY = gas.TDY #Get/Set temperature [K] and density [kg/m^3 or kmol/m^3], and mass fractions. From: https://cantera.org/documentation/docs-2.4/sphinx/html/cython/thermo.html cov = surf.coverages #This is normalized coverages, built in from InerfacePhase class: https://cantera.github.io/docs/sphinx/html/cython/thermo.html#cantera.InterfacePhase #It would also be possible to type surface.site_density, giving [kmol/m^2] for surface phases. Also possible to use surf.set_unnormalized_coverages(self, cov) for cases when don't want to use normalized coverages. # create a new reactor gas.TDY = TDY #<-- If TDY was changing, and if original gas wanted to be kept, this could have been gas = copy.deepcopy(gas)? reactor = ct.IdealGasReactor(gas, energy='off') reactor.volume = rvol # create a reservoir to represent the reactor immediately upstream. Note # that the gas object is set already to the state of the upstream reactor upstream_of_CSTR = ct.Reservoir(gas, name='upstream') #A cantera reservoir never changes no matter what goes in/out: https://cantera.org/documentation/docs-2.4/sphinx/html/cython/zerodim.html#cantera.Reservoir # create a reservoir for the reactor to exhaust into. The composition of # this reservoir is irrelevant. downstream_of_CSTR = ct.Reservoir(gas, name='downstream') #A cantera reservoir never changes no matter what goes in/out: https://cantera.org/documentation/docs-2.4/sphinx/html/cython/zerodim.html#cantera.Reservoir #Note: these are reservoirs for individual CSTRs. It's used in a clever way in this example, as will be seen later. #I would prefer to call these reservoirs "feed" and "exhaust", and would consider to fill the downstream/exhaust with inert to make it clear that it is different. Or maybe make deep copies of gas called "feed_gas" and "exhaust_gas". # Add the reacting surface to the reactor. The area is set to the desired # catalyst area in the reactor. rsurf = ct.ReactorSurface(surf, reactor, A=cat_area) #Here is where the catalyst site density gets used. Note that cat_area is actually in meters now, even though site_density is defined as mol/cm^2 in the cti file. # The mass flow rate into the reactor will be fixed by using a # MassFlowController object. f_mfr = ct.MassFlowController(upstream_of_CSTR, reactor, mdot=mass_flow_rate) #mass flow controller makes flow that goes from the first argument to the second argument with mdot providing "The mass flow rate [kg/s] through this device at time t [s]."s # We need an outlet to the downstream reservoir. This will determine the # pressure in the reactor. The value of K will only affect the transient # pressure difference. e_mfr = ct.PressureController(reactor, downstream_of_CSTR, master=f_mfr, K=1e-5) #This makes flow that goes from first argument to second argument with the mass flow controller "f_mfr" controlling the pressure here. K has units of kg/s/Pa times the pressure difference. this "v" that comes out is in same units as the mass flow rate, it's in kg/s. https://cantera.org/documentation/docs-2.4/sphinx/html/cython/zerodim.html#flowdevice e_mfr is the exhaust mass flow rate. sim = ct.ReactorNet([reactor]) #This is normally a list of reactors that are coupled. sim.max_err_test_fails = 12 #Even after looking at docs, I'm not sure what this does. I think this is how many relative and absolute tolerance failures there can be in a single time step before the resolution of the ODE integrator does that step again with finer resolution? https://cantera.org/documentation/docs-2.4/sphinx/html/cython/zerodim.html#reactor-networks #Static versus PFR flag # set relative and absolute tolerances on the simulation sim.rtol = rtol sim.atol = atol gas_rates = [] #NOTE: These are just the rates from the surface phase. These are *not* including any homogeneous rates. surface_rates = [] #NOTE: These are just the rates from the surface phase. These are *not* including any homogeneous rates. sim_times = [] #NOTE: This is less useful if one uses advance_to_steady_state sim_dist = [] #This is the distance in the reactor. If one is using flow and advance_to_steady_state, then this is representative of the kinetics. concentrationsArray = [] #Print some things out for before the simulation, these are basically headers. if print_frequency != None: print(concentrationsArrayHeader) if flow_type == "PFR": for n in range(NReactors): #iterate across the CSTRs. # Set the state of the reservoir to match that of the previous reactor gas.TDY = reactor.thermo.TDY #<-- setting gas to *most recent* state of the reactor. upstream_of_CSTR.syncState() #<-- this is an interesting trick. Once one CSTR has been simulated, we set the current values (the one from the just simulated CSTR) to be upstream value. This works because we're doing implicit ODE down a reactor length, and probably only gives the right answer for steady state kinetics. It also only works because we don't care what's happening downstream. We have a fixed K for the e_mfr, and also f_mfr. sim.reinitialize() sim.advance_to_steady_state() #<-- we advance the current CSTR to steady state before going to the next one. For a tpr, we'd probably use advance(self, double t) instead. The syntax would be something like sim.advance(10.0) for 10 seconds. https://cantera.github.io/docs/sphinx/html/cython/examples/reactors_reactor1.html#py-example-reactor1-py dist = n * rlen # distance in m <-- this could have been defined above, and I would have... but it's probably good to have it down here to make it clear that only 'n" is being used rather than this value, to simulate the reactor. sim_dist.append(dist) sim_times.append(sim.time) gas_rates.append(surf.get_net_production_rates('gas')) surface_rates.append(surf.get_net_production_rates('surf')) # write the gas mole fractions and surface coverages vs. distance rowListOfStrings = [dist, sim.time, gas.T, surf.T, reactor.thermo.P/ct.one_atm] + \ list(gas.concentrations) + list(surf.coverages) if exportOutputs == True: writer.writerow(rowListOfStrings) concentrationsArray.append(np.array(rowListOfStrings)) if print_frequency != None: if not n % print_frequency: #This only prints every specified number of steps. try: with np.printoptions(precision=3): print(np.array(rowListOfStrings)) except: print(np.array(rowListOfStrings)) if flow_type == "Static": T_surf_0 = T_surf #sim.set_max_time_step(t_step_size) #Cantera's sim.step normally advances in a variable way. for i_step in range(num_t_steps): #iterate across the CSTRs. time = i_step*t_step_size T_surf = T_surf_0 + time*heating_rate surf.TP = T_surf, P_gas if simulation_settings_module.piecewise_coverage_dependence == True: modified_reactions_parameters_array = canteraKineticsParametersParser.calculatePiecewiseCoverageDependentModifiedParametersArray(simulation_settings_module, surf.species_names, surf.coverages) #This feature requires the piecewise coverage dependence settings AND the reactions_parameters_array to already be inside the surf object **in advance** canteraKineticsParametersParser.modifyReactionsInOnePhase(surf, modified_reactions_parameters_array, ArrheniusOnly=True) #TODO: Right now this is constrainted to Arrhenius only because cantera does not yet allow modifyReactionsInOnePhase to do more (but it's planned to change in developers roadmap) surf.advance_coverages(t_step_size) #sim.advance(time) would not work. Changing T with time is not officially supported but happens to work with surf.advance_coverages. Supported way to change temperature during simulation for arbitrary reactors is to use custom integrator: https://cantera.org/examples/python/reactors/custom.py.html dist = 0.0 sim_dist.append(dist) sim_times.append(time) gas_rates.append(surf.get_net_production_rates('gas')) surface_rates.append(surf.get_net_production_rates('surf')) rowListOfStrings = [dist, time, gas.T, surf.T, reactor.thermo.P/ct.one_atm] + \ list(gas.X) + list(surf.coverages) if exportOutputs == True: writer.writerow(rowListOfStrings) concentrationsArray.append(np.array(rowListOfStrings)) if print_frequency != None: if not i_step % print_frequency: #This only prints every specified number of steps. try: with np.printoptions(precision=3): print(np.array(rowListOfStrings)) except: print(np.array(rowListOfStrings)) #Need to get things into a stackable state. Figured out from numpy shape that I needed to do at_least2D and transpose. sim_dist = np.atleast_2d(sim_dist).transpose() sim_times = np.atleast_2d(sim_times).transpose() gas_rates = np.array(gas_rates) surface_rates = np.array(surface_rates) gasRatesArray = np.hstack((sim_dist,sim_times, gas_rates)) surfaceRatesArray = np.hstack((sim_dist,sim_times, surface_rates)) rates_all_array = np.hstack((sim_dist,sim_times, gas_rates, surface_rates)) concentrationsArray = np.array(concentrationsArray) concentrationsArrayHeader = concentrationsArrayHeader gasRatesArrayHeader = 'dist(m), time(s),'+str(gas.species_names).replace("'","")[1:-1] surfaceRatesArrayHeader = 'dist(m),time(s),'+str(surf.species_names).replace("'","")[1:-1] rates_all_array_header = 'dist(m),time(s),'+str(gas.species_names).replace("'","")[1:-1]+"," + str(surf.species_names).replace("'","")[1:-1] canteraSimulationsObject = sim cantera_phase_rates = {"gas":gasRatesArray, "surf":surfaceRatesArray} cantera_phase_rates_headers = {"gas":gasRatesArrayHeader, "surf":surfaceRatesArrayHeader} if exportOutputs == True: np.savetxt(model_name + "_output_rates_all.csv", rates_all_array, delimiter=",", comments = '', header = rates_all_array_header) np.savetxt(model_name + "_output_rates_gas.csv", gasRatesArray, delimiter=",", comments = '', header = gasRatesArrayHeader ) np.savetxt(model_name + "_output_rates_surf.csv", surfaceRatesArray, delimiter=",", comments = '', header = surfaceRatesArrayHeader) if exportOutputs == True: outfile.close() return concentrationsArray, concentrationsArrayHeader, rates_all_array, rates_all_array_header, cantera_phase_rates, canteraPhases, cantera_phase_rates_headers, canteraSimulationsObject
def monolith_simulation(path_to_cti, temp, mol_in, verbose=False, sens=False): """ Set up and solve the monolith reactor simulation. Verbose prints out values as you go along Sens is for sensitivity, in the form [perturbation, reaction #] Args: path_to_cti: full path to the cti file temp (float): The temperature in Kelvin mol_in (3-tuple or iterable): the inlet molar ratios of (CH4, O2, Ar) verbose (Boolean): whether to print intermediate results sens (False or 2-tuple/list): if not False, then should be a 2-tuple or list [dk, rxn] in which dk = relative change (eg. 0.01) and rxn = the index of the surface reaction rate to change Returns: gas_out, # gas molar flow rate in moles/minute surf_out, # surface mole fractions gas_names, # gas species names surf_names, # surface species names dist_array, # distances (in mm) T_array # temperatures (in K) """ sols_dict = setup_ct_solution(path_to_cti) gas, surf, i_ar, n_surf_reactions = sols_dict['gas'], sols_dict[ 'surf'], sols_dict['i_ar'], sols_dict['n_surf_reactions'] print( f"Running monolith simulation with CH4 and O2 concs {mol_in[0], mol_in[1]} on thread {threading.get_ident()}" ) ch4, o2, ar = mol_in ratio = ch4 / (2 * o2) X = f"CH4(2):{ch4}, O2(3):{o2}, Ar:{ar}" gas.TPX = 273.15, ct.one_atm, X # need to initialize mass flow rate at STP # mass_flow_rate = velocity * gas.density_mass * area # kg/s mass_flow_rate = flow_rate * gas.density_mass gas.TPX = temp, ct.one_atm, X temp_cat = temp surf.TP = temp_cat, ct.one_atm surf.coverages = 'X(1):1.0' gas.set_multiplier(1.0) TDY = gas.TDY cov = surf.coverages if verbose is True: print( ' distance(mm) X_CH4 X_O2 X_H2 X_CO X_H2O X_CO2' ) # create a new reactor gas.TDY = TDY r = ct.IdealGasReactor(gas) r.volume = rvol # create a reservoir to represent the reactor immediately upstream. Note # that the gas object is set already to the state of the upstream reactor upstream = ct.Reservoir(gas, name='upstream') # create a reservoir for the reactor to exhaust into. The composition of # this reservoir is irrelevant. downstream = ct.Reservoir(gas, name='downstream') # Add the reacting surface to the reactor. The area is set to the desired # catalyst area in the reactor. rsurf = ct.ReactorSurface(surf, r, A=cat_area) # The mass flow rate into the reactor will be fixed by using a # MassFlowController object. # mass_flow_rate = velocity * gas.density_mass * area # kg/s # mass_flow_rate = flow_rate * gas.density_mass m = ct.MassFlowController(upstream, r, mdot=mass_flow_rate) # We need an outlet to the downstream reservoir. This will determine the # pressure in the reactor. The value of K will only affect the transient # pressure difference. v = ct.PressureController(r, downstream, master=m, K=1e-5) sim = ct.ReactorNet([r]) sim.max_err_test_fails = 12 # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-10 sim.atol = 1.0e-20 gas_names = gas.species_names surf_names = surf.species_names gas_out = [] surf_out = [] dist_array = [] T_array = [] surf.set_multiplier(0.0) # no surface reactions until the gauze for n in range(N_reactors): # Set the state of the reservoir to match that of the previous reactor gas.TDY = r.thermo.TDY upstream.syncState() if n == on_catalyst: surf.set_multiplier(1.0) if sens is not False: surf.set_multiplier(1.0 + sens[0], sens[1]) if n == off_catalyst: surf.set_multiplier(0.0) sim.reinitialize() sim.advance_to_steady_state() dist = n * reactor_len * 1.0e3 # distance in mm dist_array.append(dist) T_array.append(surf.T) kmole_flow_rate = mass_flow_rate / gas.mean_molecular_weight # kmol/s gas_out.append(1000 * 60 * kmole_flow_rate * gas.X.copy()) # molar flow rate in moles/minute surf_out.append(surf.X.copy()) # stop simulation when things are done changing, to avoid getting so many COVDES errors if n >= 1001: if np.max(abs(np.subtract(gas_out[-2], gas_out[-1]))) < 1e-15: break # make reaction diagrams # out_dir = 'rxnpath' # os.path.exists(out_dir) or os.makedirs(out_dir) # elements = ['H', 'O'] # locations_of_interest = [1000, 1200, 1400, 1600, 1800, 1999] # if sens is False: # if n in locations_of_interest: # location = str(int(n / 100)) # diagram = ct.ReactionPathDiagram(surf, 'X') # diagram.title = 'rxn path' # diagram.label_threshold = 1e-9 # dot_file = f"{out_dir}/rxnpath-{ratio:.1f}-x-{location}mm.dot" # img_file = f"{out_dir}/rxnpath-{ratio:.1f}-x-{location}mm.pdf" # diagram.write_dot(dot_file) # os.system('dot {0} -Tpng -o{1} -Gdpi=200'.format(dot_file, img_file)) # # for element in elements: # diagram = ct.ReactionPathDiagram(surf, element) # diagram.title = element + 'rxn path' # diagram.label_threshold = 1e-9 # dot_file = f"{out_dir}/rxnpath-{ratio:.1f}-x-{location}mm-{element}.dot" # img_file = f"{out_dir}/rxnpath-{ratio:.1f}-x-{location}mm-{element}.pdf" # diagram.write_dot(dot_file) # os.system('dot {0} -Tpng -o{1} -Gdpi=200'.format(dot_file, img_file)) # else: # pass if verbose is True: if not n % 100: print( ' {0:10f} {1:10f} {2:10f} {3:10f} {4:10f} {5:10f} {6:10f}' .format( dist, *gas['CH4(2)', 'O2(3)', 'H2(6)', 'CO(7)', 'H2O(5)', 'CO2(4)'].X * 1000 * 60 * kmole_flow_rate)) gas_out = np.array(gas_out) surf_out = np.array(surf_out) gas_names = np.array(gas_names) surf_names = np.array(surf_names) data_out = gas_out, surf_out, gas_names, surf_names, dist_array, T_array, i_ar, n_surf_reactions print( f"Finished monolith simulation for CH4 and O2 concs {mol_in[0], mol_in[1]} on thread {threading.get_ident()}" ) return data_out
def plug_flow_simulation(self, output_filename, energy='off'): """ PLUG FLOW REACTOR SIMULATION The plug flow reactor is represented by a linear chain of zero-dimensional reactors. The gas at the inlet to the first one has the specified inlet composition, and for all others the inlet composition is fixed at the composition of thereactor immediately upstream. Since in a PFR model there is no diffusion, the upstream reactors are not affected by any downstream reactors, and therefore the problem may be solved by simply marching from the first to last reactor, integrating each one to steady state. """ gas, surf = self.gas, self.surf gas.TPX = self.temperature_c + 273.15, self.pressure, self.feed_mole_fractions surf.TP = self.temperature_c + 273.15, self.pressure surf.coverages = self.surface_coverages TDY = gas.TDY # create a new reactor gas.TDY = TDY r = ct.IdealGasReactor(gas, energy=energy) r.volume = self.gas_volume_per_reactor # create a reservoir to represent the reactor immediately upstream. Note # that the gas object is set already to the state of the upstream reactor upstream = ct.Reservoir(gas, name="upstream") # create a reservoir for the reactor to exhaust into. The composition of # this reservoir is irrelevant. downstream = ct.Reservoir(gas, name="downstream") # Add the reacting surface to the reactor. The area is set to the desired # catalyst area in the reactor. rsurf = ct.ReactorSurface(surf, r, A=self.cat_area_per_reactor) # The mass flow rate into the reactor will be fixed by using a # MassFlowController object. m = ct.MassFlowController(upstream, r, mdot=self.mass_flow_rate) # We need an outlet to the downstream reservoir. This will determine the # pressure in the reactor. The value of K will only affect the transient # pressure difference. v = ct.PressureController(r, downstream, master=m, K=1e-5) sim = ct.ReactorNet([r]) sim.max_err_test_fails = 24 # set relative and absolute tolerances on the simulation sim.rtol = self.rtol sim.atol = self.atol sim.verbose = False r.volume = self.gas_volume_per_reactor os.makedirs(self.results_directory, exist_ok=True) outfile = open(os.path.join(self.results_directory, output_filename), "w") writer = csv.writer(outfile) writer.writerow(["Distance (mm)", "T (C)", "P (atm)"] + gas.species_names + surf.species_names + ["gas_heat", "surface_heat", "alpha"]) print( " distance(mm) T(C) NH3(6) O2(2) N2(4) N2O(7) NO(5) H2O(3) alpha" ) for n in range(self.number_of_reactors): # Set the state of the reservoir to match that of the previous reactor gas.TDY = TDY = r.thermo.TDY upstream.syncState() sim.reinitialize() previous_coverages = surf.coverages # in case we want to retry if n > 0: # Add a first row in the CSV with just the feed try: sim.advance_to_steady_state() except ct.CanteraError: t = sim.time sim.set_initial_time(0) gas.TDY = TDY surf.coverages = previous_coverages r.syncState() sim.reinitialize() new_target_time = 0.01 * t logging.warning( f"Couldn't reach {t:.1g} s so going to try {new_target_time:.1g} s" ) # self.save_flux_diagrams(path=f"data/H3NX(29)+{delta}") # self.show_flux_diagrams(embed=True) # self.report_rates() # self.report_rate_constants() try: sim.advance(new_target_time) except ct.CanteraError: outfile.close() raise # dont add fluxes at distance=0 because you just # have an almost infinite flux onto vacant surface self.add_fluxes() # for the integration distance_mm = n * self.reactor_length * 1.0e3 # distance in mm # heat evolved by gas phase reaction: gas_heat = surface_heat = alpha = 1 # heat evolved by surf phase reaction: surface_heat = self.cat_area_per_gas_volume * np.dot( surf.net_rates_of_progress, surf.delta_enthalpy) # fraction of heat release that is on surface: alpha = surface_heat / (surface_heat + gas_heat) if not n % 10: print( " {:10f} {:7.1f} {:10f} {:10f} {:10f} {:10f} {:10f} {:10f} {:5.1e}" .format( distance_mm, r.T - 273.15, *gas["NH3(6)", "O2(2)", "N2(4)", "N2O(7)", "NO(5)", "H2O(3)"].X, alpha, )) print("Highest surface coverages are:") for i in np.argsort(surf.coverages)[::-1][:5]: print(surf.species_name(i), round(surf.coverages[i], 4)) if n in (1, int(self.number_of_reactors / 2), self.number_of_reactors - 1): # Will save at start, midpoint, and end self.save_flux_diagrams( path=self.results_directory, suffix=f"Temp_{r.T-273.15:.0f}C_Dist_{distance_mm:.1f}", ) if (not (n - 1) % 100) or n == (self.number_of_reactors - 1): self.save_flux_data( f"flux_data_Temp_{r.T-273.15:.0f}C_Dist_{distance_mm:.1f}") # write the gas mole fractions and surface coverages vs. distance writer.writerow( [distance_mm, r.T - 273.15, r.thermo.P / ct.one_atm] + list(gas.X) + list(surf.coverages) + [gas_heat, surface_heat, alpha]) outfile.close() print("Results saved to '{0}'".format(output_filename))
# define object surface surf = ct.Interface(fcti, 'Pt_surf', [gas]) surf.TP = Tin, p # define object reactor r = ct.IdealGasReactor(gas) vol = area * dx * porosity r.volume = vol upstream = ct.Reservoir(gas, name='upstream') downstream = ct.Reservoir(gas, name='downstream') m = ct.MassFlowController(upstream, r, mdot=mdot) v = ct.PressureController(r, downstream, master=m, K=1.0e-5) area_cat = area_cat_vol * vol rsurf = ct.ReactorSurface(surf, r, A=area_cat) sim = ct.ReactorNet([r]) # solve outfile = open('catalytic_pfr.csv', 'w', newline='') writer = csv.writer(outfile) writer.writerow(['Distance (m)', 'u(m/s)', 'rtime(s)', 'T(K)', 'P(Pa)'] + gas.species_names + surf.species_names) t_res = 0.0 for n in range(n_reactor): gas.TDY = r.thermo.TDY upstream.syncState() sim.reinitialize() sim.advance_to_steady_state()
def run_reactor( cti_file, t_array=[528], p_array=[75], v_array=[0.00424], h2_array=[0.75], co2_array=[0.5], rtol=1.0e-11, atol=1.0e-22, reactor_type=0, energy="off", sensitivity=False, sensatol=1e-6, sensrtol=1e-6, reactime=1e5, ): import pandas as pd import numpy as np import time import cantera as ct from matplotlib import pyplot as plt import csv import math import os import sys import re import itertools import logging from collections import defaultdict import git try: array_i = int(os.getenv("SLURM_ARRAY_TASK_ID")) except TypeError: array_i = 0 # get git commit hash and message repo = git.Repo("/work/westgroup/lee.ting/cantera/ammonia/") git_sha = str(repo.head.commit)[0:6] git_msg = str(repo.head.commit.message)[0:20].replace(" ", "_").replace( "'", "_") # this should probably be outside of function settings = list( itertools.product(t_array, p_array, v_array, h2_array, co2_array)) # constants pi = math.pi # set initial temps, pressures, concentrations temp = settings[array_i][0] # kelvin temp_str = str(temp)[0:3] pressure = settings[array_i][1] * ct.one_atm # Pascals X_h2 = settings[array_i][3] x_h2_str = str(X_h2)[0:3].replace(".", "_") x_CO_CO2_str = str(settings[array_i][4])[0:3].replace(".", "_") if X_h2 == 0.75: X_h2o = 0.05 else: X_h2o = 0 X_co = (1 - (X_h2 + X_h2o)) * (settings[array_i][4]) X_co2 = (1 - (X_h2 + X_h2o)) * (1 - settings[array_i][4]) # normalize mole fractions just in case # X_co = X_co/(X_co+X_co2+X_h2) # X_co2= X_co2/(X_co+X_co2+X_h2) # X_h2 = X_h2/(X_co+X_co2+X_h2) mw_co = 28.01e-3 # [kg/mol] mw_co2 = 44.01e-3 # [kg/mol] mw_h2 = 2.016e-3 # [kg/mol] mw_h2o = 18.01528e-3 # [kg/mol] co2_ratio = X_co2 / (X_co + X_co2) h2_ratio = (X_co2 + X_co) / X_h2 # CO/CO2/H2/H2: typical is concentrations_rmg = {"O2(2)": X_co, "NH3(6)": X_co2, "He": X_h2} # initialize cantera gas and surface gas = ct.Solution(cti_file, "gas") # surf_grab = ct.Interface(cti_file,'surface1_grab', [gas_grab]) surf = ct.Interface(cti_file, "surface1", [gas]) # gas_grab.TPX = gas.TPX = temp, pressure, concentrations_rmg surf.TP = temp, pressure # create gas inlet inlet = ct.Reservoir(gas) # create gas outlet exhaust = ct.Reservoir(gas) # Reactor volume rradius = 35e-3 rlength = 70e-3 rvol = (rradius**2) * pi * rlength # Catalyst Surface Area site_density = (surf.site_density * 1000 ) # [mol/m^2]cantera uses kmol/m^2, convert to mol/m^2 cat_weight = 4.24e-3 # [kg] cat_site_per_wt = (300 * 1e-6) * 1000 # [mol/kg] 1e-6mol/micromole, 1000g/kg cat_area = site_density / (cat_weight * cat_site_per_wt) # [m^3] # reactor initialization if reactor_type == 0: r = ct.Reactor(gas, energy=energy) reactor_type_str = "Reactor" elif reactor_type == 1: r = ct.IdealGasReactor(gas, energy=energy) reactor_type_str = "IdealGasReactor" elif reactor_type == 2: r = ct.ConstPressureReactor(gas, energy=energy) reactor_type_str = "ConstPressureReactor" elif reactor_type == 3: r = ct.IdealGasConstPressureReactor(gas, energy=energy) reactor_type_str = "IdealGasConstPressureReactor" rsurf = ct.ReactorSurface(surf, r, A=cat_area) r.volume = rvol surf.coverages = "X(1):1.0" # flow controllers (Graaf measured flow at 293.15 and 1 atm) one_atm = ct.one_atm FC_temp = 293.15 volume_flow = settings[array_i][2] # [m^3/s] molar_flow = volume_flow * one_atm / (8.3145 * FC_temp) # [mol/s] mass_flow = molar_flow * (X_co * mw_co + X_co2 * mw_co2 + X_h2 * mw_h2 + X_h2o * mw_h2o) # [kg/s] mfc = ct.MassFlowController(inlet, r, mdot=mass_flow) # A PressureController has a baseline mass flow rate matching the 'master' # MassFlowController, with an additional pressure-dependent term. By explicitly # including the upstream mass flow rate, the pressure is kept constant without # needing to use a large value for 'K', which can introduce undesired stiffness. outlet_mfc = ct.PressureController(r, exhaust, master=mfc, K=0.01) # initialize reactor network sim = ct.ReactorNet([r]) # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-11 sim.atol = 1.0e-22 ################################################# # Run single reactor ################################################# # round numbers so they're easier to read # temp_str = '%s' % '%.3g' % tempn cat_area_str = "%s" % "%.3g" % cat_area results_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/transient/{temp_str}/results" ) # results_path_csp = ( # os.path.dirname(os.path.abspath(__file__)) # + f"/{git_sha}_{git_msg}/{reactor_type_str}/transient/{temp_str}/results/csp" # ) flux_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/transient/{temp_str}/flux_diagrams/{x_h2_str}/{x_CO_CO2_str}" ) try: os.makedirs(results_path, exist_ok=True) except OSError as error: print(error) # try: # os.makedirs(results_path_csp, exist_ok=True) # except OSError as error: # print(error) try: os.makedirs(flux_path, exist_ok=True) except OSError as error: print(error) gas_ROP_str = [i + " ROP [kmol/m^3 s]" for i in gas.species_names] # surface ROP reports gas and surface ROP. these values might be redundant, not sure. gas_surf_ROP_str = [ i + " surface ROP [kmol/m^2 s]" for i in gas.species_names ] surf_ROP_str = [i + " ROP [kmol/m^2 s]" for i in surf.species_names] gasrxn_ROP_str = [ i + " ROP [kmol/m^3 s]" for i in gas.reaction_equations() ] surfrxn_ROP_str = [ i + " ROP [kmol/m^2 s]" for i in surf.reaction_equations() ] output_filename = ( results_path + f"/Spinning_basket_area_{cat_area_str}_energy_{energy}" + f"_temp_{temp}_h2_{x_h2_str}_COCO2_{x_CO_CO2_str}.csv") # output_filename_csp = ( # results_path_csp # + f"/Spinning_basket_area_{cat_area_str}_energy_{energy}" # + f"_temp_{temp}_h2_{x_h2_str}_COCO2_{x_CO_CO2_str}.csv" # ) outfile = open(output_filename, "w") # outfile_csp = open(output_filename_csp, "w") writer = csv.writer(outfile) # writer_csp = csv.writer(outfile_csp) # Sensitivity atol, rtol, and strings for gas and surface reactions if selected # slows down script by a lot if sensitivity: sim.rtol_sensitivity = sensrtol sim.atol_sensitivity = sensatol sens_species = [ "NH3(6)" ] #change THIS to your species, can add "," and other species # turn on sensitive reactions/species for i in range(gas.n_reactions): r.add_sensitivity_reaction(i) for i in range(surf.n_reactions): rsurf.add_sensitivity_reaction(i) # for i in range(gas.n_species): # r.add_sensitivity_species_enthalpy(i) # for i in range(surf.n_species): # rsurf.add_sensitivity_species_enthalpy(i) for j in sens_species: gasrxn_sens_str = [ j + " sensitivity to " + i for i in gas.reaction_equations() ] surfrxn_sens_str = [ j + " sensitivity to " + i for i in surf.reaction_equations() ] # gastherm_sens_str = [j + " thermo sensitivity to " + i for i in gas.species_names] # surftherm_sens_str = [j + " thermo sensitivity to " + i for i in surf.species_names] sens_list = gasrxn_sens_str + surfrxn_sens_str # + gastherm_sens_str writer.writerow([ "T (C)", "P (atm)", "V (M^3/s)", "X_co initial", "X_co2initial", "X_h2 initial", "X_h2o initial", "CO2/(CO2+CO)", "(CO+CO2/H2)", "T (C) final", "Rtol", "Atol", "reactor type", ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str + sens_list) else: writer.writerow([ "T (C)", "P (atm)", "V (M^3/s)", "X_co initial", "X_co2 initial", "X_h2 initial", "X_h2o initial", "CO2/(CO2+CO)", "(CO+CO2/H2)", "T (C) final", "Rtol", "Atol", "reactor type", ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str) # writer_csp.writerow( # ["iter", "t", "dt", "Density[kg/m3]", "Pressure[Pascal]", "Temperature[K]",] # + gas.species_names # + surf.species_names # ) t = 0.0 dt = 0.1 iter_ct = 0 # run the simulation first_run = True while t < reactime: # save flux diagrams at beginning of run if first_run == True: save_flux_diagrams(gas, suffix=flux_path, timepoint="beginning") save_flux_diagrams(surf, suffix=flux_path, timepoint="beginning") first_run = False t += dt sim.advance(t) # if t % 10 < 0.01: if sensitivity: # get sensitivity for sensitive species i (e.g. methanol) in reaction j for i in sens_species: g_nrxn = gas.n_reactions s_nrxn = surf.n_reactions # g_nspec = gas.n_species # s_nspec = surf.n_species gas_sensitivities = [ sim.sensitivity(i, j) for j in range(g_nrxn) ] surf_sensitivities = [ sim.sensitivity(i, j) for j in range(g_nrxn, g_nrxn + s_nrxn) ] # gas_therm_sensitivities = [sim.sensitivity(i,j) # for j in range(g_nrxn+s_nrxn,g_nrxn+s_nrxn+g_nspec)] # surf_therm_sensitivities = [sim.sensitivity(i,j) # for j in range(g_nrxn+s_nrxn+g_nspec,g_nrxn+s_nrxn+g_nspec+s_nspec)] sensitivities_all = ( gas_sensitivities + surf_sensitivities # + gas_therm_sensitivities ) writer.writerow([ temp, pressure, volume_flow, X_co, X_co2, X_h2, X_h2o, co2_ratio, h2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress) + sensitivities_all) else: writer.writerow([ temp, pressure, volume_flow, X_co, X_co2, X_h2, X_h2o, co2_ratio, h2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress)) # writer_csp.writerow( # [ # iter_ct, # sim.time, # dt, # gas.density, # gas.P, # gas.T, # ] # + list(gas.X) # + list(surf.X) # ) iter_ct += 1 outfile.close() # outfile_csp.close() # save flux diagrams at the end of the run save_flux_diagrams(gas, suffix=flux_path, timepoint="end") save_flux_diagrams(surf, suffix=flux_path, timepoint="end") return
def monolithFull(gas, surf, temp, mol_in, verbose=False, sens=False): """ Verbose prints out values as you go along Sens is for sensitivity, in the form [perturbation, reaction #] """ ch4, o2, ar = mol_in ratio = ch4 / (2 * o2) ratio = round(ratio, 1) ch4 = str(ch4) o2 = str(o2) ar = str(ar) X = str('CH4(2):' + ch4 + ', O2(3):' + o2 + ', Ar:' + ar) gas.TPX = 273.15, ct.one_atm, X # need to initialize mass flow rate at STP # mass_flow_rate = velocity * gas.density_mass * area # kg/s mass_flow_rate = flow_rate * gas.density_mass gas.TPX = temp, ct.one_atm, X temp_cat = temp surf.TP = temp_cat, ct.one_atm surf.coverages = 'X(1):1.0' gas.set_multiplier(1.0) TDY = gas.TDY cov = surf.coverages if verbose is True: print( ' distance(mm) X_CH4 X_O2 X_H2 X_CO X_H2O X_CO2' ) # create a new reactor gas.TDY = TDY r = ct.IdealGasReactor(gas) r.volume = rvol # create a reservoir to represent the reactor immediately upstream. Note # that the gas object is set already to the state of the upstream reactor upstream = ct.Reservoir(gas, name='upstream') # create a reservoir for the reactor to exhaust into. The composition of # this reservoir is irrelevant. downstream = ct.Reservoir(gas, name='downstream') # Add the reacting surface to the reactor. The area is set to the desired # catalyst area in the reactor. rsurf = ct.ReactorSurface(surf, r, A=cat_area) # The mass flow rate into the reactor will be fixed by using a # MassFlowController object. # mass_flow_rate = velocity * gas.density_mass * area # kg/s # mass_flow_rate = flow_rate * gas.density_mass m = ct.MassFlowController(upstream, r, mdot=mass_flow_rate) # We need an outlet to the downstream reservoir. This will determine the # pressure in the reactor. The value of K will only affect the transient # pressure difference. v = ct.PressureController(r, downstream, master=m, K=1e-5) sim = ct.ReactorNet([r]) sim.max_err_test_fails = 12 # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-10 sim.atol = 1.0e-20 gas_names = gas.species_names surf_names = surf.species_names gas_out = [] # in surf_out = [] dist_array = [] T_array = [] surf.set_multiplier(0.0) # no surface reactions until the gauze for n in range(NReactors): # Set the state of the reservoir to match that of the previous reactor gas.TDY = r.thermo.TDY upstream.syncState() if n == on_catalyst: surf.set_multiplier(1.0) if sens is not False: surf.set_multiplier(1.0 + sens[0], sens[1]) if n == off_catalyst: surf.set_multiplier(0.0) sim.reinitialize() sim.advance_to_steady_state() dist = n * reactor_len * 1.0e3 # distance in mm dist_array.append(dist) T_array.append(surf.T) kmole_flow_rate = mass_flow_rate / gas.mean_molecular_weight # kmol/s gas_out.append(1000 * 60 * kmole_flow_rate * gas.X.copy()) # molar flow rate in moles/minute surf_out.append(surf.X.copy()) # make reaction diagrams out_dir = 'rxnpath' os.path.exists(out_dir) or os.makedirs(out_dir) elements = ['H', 'O'] locations_of_interest = [1000, 1200, 1400, 1600, 1800, 1999] if sens is False: for l in locations_of_interest: if n == l: location = str(int(n / 100)) diagram = ct.ReactionPathDiagram(surf, 'X') diagram.title = 'rxn path' diagram.label_threshold = 1e-9 dot_file = out_dir + '/rxnpath-' + str( ratio) + '-x-' + location + 'mm.dot' img_file = out_dir + '/rxnpath-' + str( ratio) + '-x-' + location + 'mm.pdf' img_path = os.path.join(out_dir, img_file) diagram.write_dot(dot_file) os.system('dot {0} -Tpng -o{1} -Gdpi=200'.format( dot_file, img_file)) for element in elements: diagram = ct.ReactionPathDiagram(surf, element) diagram.title = element + 'rxn path' diagram.label_threshold = 1e-9 dot_file = out_dir + '/rxnpath-' + str( ratio ) + '-surf-' + location + 'mm-' + element + '.dot' img_file = out_dir + '/rxnpath-' + str( ratio ) + '-surf-' + location + 'mm-' + element + '.pdf' img_path = os.path.join(out_dir, img_file) diagram.write_dot(dot_file) os.system('dot {0} -Tpng -o{1} -Gdpi=200'.format( dot_file, img_file)) else: pass if verbose is True: if not n % 100: print( ' {0:10f} {1:10f} {2:10f} {3:10f} {4:10f} {5:10f} {6:10f}' .format( dist, *gas['CH4(2)', 'O2(3)', 'H2(6)', 'CO(7)', 'H2O(5)', 'CO2(4)'].X * 1000 * 60 * kmole_flow_rate)) gas_out = np.array(gas_out) surf_out = np.array(surf_out) gas_names = np.array(gas_names) surf_names = np.array(surf_names) data_out = gas_out, surf_out, gas_names, surf_names, dist_array, T_array return data_out
def define_reactor(feed_temp, feed_pressure, feed_velocity, feed_composition, cat_area_by_rctr_vol, cat_composition, wall_temp, wall_area_by_rctr_vol, length, rctr_type, nodes=None): """ Define Continous Stirred Tank Reactor with a catalyst reacting surface by adding appropriate components to IdealGasReactor class defined in cantera. Due to the nature of underlying class definitions in cantera, CSTR is defined as a collection of reactor, catlayst surface, inlets, outlets, and the corresponding reservoirs. Plug Flow Reactor is defined as a concatenation of CSTRs. :param feed_temp: Temperature of the reactant feed :param feed_pressure: Pressure of the reactant feed :param feed_velocity: Velocity of the reactant feed :param feed_composition: composition of the reactant feed. Use gas phase :param cat_area_by_rctr_vol: Area of catalyst given in inverse length :param cat_composition: Composition of catalyst. Use surface phase :param wall_temp: External temperature outside outer wall of reactor :param wall_area_by_rctr_vol: Area of outer wall given in inverse length :param length: Length of the reactor along which feed flows :param rctr_type: Type of the reactor. Options are 'CSTR' or 'PFR' :param nodes: If reactor type is 'PFR', it is simulated as N CSTRS, where N is given by nodes :return: IdealGasRector or ReactorNet with appropriate components """ gas = feed_composition gas.TP = feed_temp, feed_pressure surf = cat_composition surf.TP = gas.TP if rctr_type == 'CSTR': rctr_len = length else: rctr_len = length / (nodes - 1) width = 1.0 # Arbitrary value surf_area = rctr_len * width rctr_vol = surf_area / cat_area_by_rctr_vol feed_mass_flow_rate = feed_velocity * gas.density * width / \ cat_area_by_rctr_vol """ In the python code, the reactor is input to the defintions of reactor components. Despite the weird way of specifying reactor components in python, they get added to the reactor object in the C++ code """ upstream = ct.Reservoir(gas, name='upstream') downstream = ct.Reservoir(gas, name='downstream') if rctr_type == 'CSTR': reactor = ct.IdealGasReactor(gas, energy='off') reactor.volume = rctr_vol rctr_surf = ct.ReactorSurface(surf, reactor, A=surf_area) inlet = ct.MassFlowController(upstream, reactor, mdot=feed_mass_flow_rate) outlet = ct.PressureController(reactor, downstream, master=inlet, K=1e-5) #K is small number return reactor, upstream, gas elif rctr_type == 'PFR': reactors = [] for i in range(nodes): r = ct.IdealGasReactor(gas, energy='off') r.volume = rctr_vol r_srf = ct.ReactorSurface(surf, r, A=surf_area) if not i: inlet = ct.MassFlowController(upstream, r, mdot=feed_mass_flow_rate) else: inlet = ct.MassFlowController(reactors[i - 1], r, mdot=feed_mass_flow_rate) outlet = ct.PressureController(r, downstream, master=inlet, K=1e-5) reactors.append(r) reactor_net = ct.ReactorNet(reactors) return reactor_net, upstream, gas
def run_reactor( cti_file, t_array=[548], surf_t_array=[ 548 ], # not used, but will be for different starting temperatures p_array=[1], v_array=[2.771e-10 ], # 14*7*(140e-4)^2*π/2*0.9=0.0002771(cm^3)=2.771e-10(m^3) o2_array=[0.88], nh3_array=[0.066], rtol=1.0e-11, atol=1.0e-22, reactor_type=0, energy="off", sensitivity=False, sensatol=1e-6, sensrtol=1e-6, reactime=1e5, ): # 14 aluminum plates, each of them containing seven semi-cylindrical microchannels of 280 µm width # and 140 µm depth, 9 mm long, arranged at equal distances of 280 µm try: array_i = int(os.getenv("SLURM_ARRAY_TASK_ID")) except TypeError: array_i = 0 # get git commit hash and message rmg_model_path = "../ammonia" repo = git.Repo(rmg_model_path) date = time.localtime(repo.head.commit.committed_date) git_date = f"{date[0]}_{date[1]}_{date[2]}_{date[3]}{date[4]}" git_sha = str(repo.head.commit)[0:6] git_msg = str(repo.head.commit.message)[0:50].replace(" ", "_").replace( "'", "_").replace("\n", "") git_file_string = f"{git_date}_{git_sha}_{git_msg}" # set sensitivity string for file path name if sensitivity: sensitivity_str = "on" else: sensitivity_str = "off" # this should probably be outside of function settings = list( itertools.product(t_array, surf_t_array, p_array, v_array, o2_array, nh3_array)) # constants pi = math.pi # set initial temps, pressures, concentrations temp = settings[array_i][1] # kelvin temp_str = str(temp)[0:3] pressure = settings[array_i][2] * ct.one_atm # Pascals surf_temp = temp X_o2 = settings[array_i][4] x_O2_str = str(X_o2)[0].replace(".", "_") X_nh3 = (settings[array_i][5]) x_NH3_str = str(X_nh3)[0:11].replace(".", "_") X_he = 1 - X_o2 - X_nh3 mw_nh3 = 17.0306e-3 # [kg/mol] mw_o2 = 31.999e-3 # [kg/mol] mw_he = 4.002602e-3 # [kg/mol] o2_ratio = X_nh3 / X_o2 # O2/NH3/He: typical is concentrations_rmg = {"O2(2)": X_o2, "NH3(6)": X_nh3, "He": X_he} # initialize cantera gas and surface gas = ct.Solution(cti_file, "gas") surf = ct.Interface(cti_file, "surface1", [gas]) # initialize temperatures gas.TPX = temp, pressure, concentrations_rmg surf.TP = temp, pressure # change this to surf_temp when we want a different starting temperature for the surface # if a mistake is made with the input, # cantera will normalize the mole fractions. # make sure that we are reporting/using # the normalized values X_o2 = float(gas["O2(2)"].X) X_nh3 = float(gas["NH3(6)"].X) X_he = float(gas["He"].X) # create gas inlet inlet = ct.Reservoir(gas) # create gas outlet exhaust = ct.Reservoir(gas) # Reactor volume number_of_reactors = 1001 rradius = 1.4e-4 #140µm to 0.00014m rtotal_length = 9e-3 #9mm to 0.009m rtotal_vol = (rradius**2) * pi * rtotal_length / 2 rlength = rtotal_length / 1001 # divide totareactor total volume rvol = (rtotal_vol) / number_of_reactors # Catalyst Surface Area site_density = (surf.site_density * 1000 ) # [mol/m^2] cantera uses kmol/m^2, convert to mol/m^2 cat_area_total = rradius * 2 / 2 * pi * rtotal_length # [m^3] cat_area = cat_area_total / number_of_reactors # reactor initialization if reactor_type == 0: r = ct.Reactor(gas, energy=energy) reactor_type_str = "Reactor" elif reactor_type == 1: r = ct.IdealGasReactor(gas, energy=energy) reactor_type_str = "IdealGasReactor" elif reactor_type == 2: r = ct.ConstPressureReactor(gas, energy=energy) reactor_type_str = "ConstPressureReactor" elif reactor_type == 3: r = ct.IdealGasConstPressureReactor(gas, energy=energy) reactor_type_str = "IdealGasConstPressureReactor" # calculate the available catalyst area in a differential reactor rsurf = ct.ReactorSurface(surf, r, A=cat_area) r.volume = rvol surf.coverages = "X(1):1.0" # flow controllers one_atm = ct.one_atm FC_temp = 293.15 volume_flow = settings[array_i][3] # [m^3/s] molar_flow = volume_flow * one_atm / (8.3145 * FC_temp) # [mol/s] mass_flow = molar_flow * (X_nh3 * mw_nh3 + X_o2 * mw_o2 + X_he * mw_he ) # [kg/s] mfc = ct.MassFlowController(inlet, r, mdot=mass_flow) # A PressureController has a baseline mass flow rate matching the 'master' # MassFlowController, with an additional pressure-dependent term. By explicitly # including the upstream mass flow rate, the pressure is kept constant without # needing to use a large value for 'K', which can introduce undesired stiffness. outlet_mfc = ct.PressureController(r, exhaust, master=mfc, K=0.01) # initialize reactor network sim = ct.ReactorNet([r]) # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-8 sim.atol = 1.0e-16 ################################################# # Run single reactor ################################################# # round numbers for filepath strings so they're easier to read # temp_str = '%s' % '%.3g' % tempn cat_area_str = "%s" % "%.3g" % cat_area # if it doesn't already exist, g species_path = (os.path.dirname(os.path.abspath(__file__)) + f"/{git_file_string}/species_pictures") results_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_file_string}/{reactor_type_str}/energy_{energy}/sensitivity_{sensitivity_str}/{temp_str}/results" ) flux_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_file_string}/{reactor_type_str}/energy_{energy}/sensitivity_{sensitivity_str}/{temp_str}/flux_diagrams/{x_O2_str}/{x_NH3_str}" ) # create species folder for species pictures if it does not already exist try: os.makedirs(species_path, exist_ok=True) save_pictures(git_path=rmg_model_path, species_path=species_path) except OSError as error: print(error) try: os.makedirs(results_path, exist_ok=True) except OSError as error: print(error) try: os.makedirs(flux_path, exist_ok=True) except OSError as error: print(error) gas_ROP_str = [i + " ROP [kmol/m^3 s]" for i in gas.species_names] # surface ROP reports gas and surface ROP. these values are not redundant gas_surf_ROP_str = [ i + " surface ROP [kmol/m^2 s]" for i in gas.species_names ] surf_ROP_str = [i + " ROP [kmol/m^2 s]" for i in surf.species_names] gasrxn_ROP_str = [ i + " ROP [kmol/m^3 s]" for i in gas.reaction_equations() ] surfrxn_ROP_str = [ i + " ROP [kmol/m^2 s]" for i in surf.reaction_equations() ] output_filename = ( results_path + f"/Spinning_basket_area_{cat_area_str}_energy_{energy}" + f"_temp_{temp}_O2_{x_O2_str}_NH3_{x_NH3_str}.csv") outfile = open(output_filename, "w") writer = csv.writer(outfile) # Sensitivity atol, rtol, and strings for gas and surface reactions if selected # slows down script by a lot if sensitivity: sim.rtol_sensitivity = sensrtol sim.atol_sensitivity = sensatol sens_species = [ "NH3(6)", "O2(2)", "N2(4)", "NO(5)", "N2O(7)" ] #change THIS to your species, can add "," and other species # turn on sensitive reactions for i in range(gas.n_reactions): r.add_sensitivity_reaction(i) for i in range(surf.n_reactions): rsurf.add_sensitivity_reaction(i) # thermo sensitivities. leave off for now as they can cause solver crashes # for i in range(gas.n_species): # r.add_sensitivity_species_enthalpy(i) # for i in range(surf.n_species): # rsurf.add_sensitivity_species_enthalpy(i) for j in sens_species: gasrxn_sens_str = [ j + " sensitivity to " + i for i in gas.reaction_equations() ] surfrxn_sens_str = [ j + " sensitivity to " + i for i in surf.reaction_equations() ] # gastherm_sens_str = [j + " thermo sensitivity to " + i for i in gas.species_names] # surftherm_sens_str = [j + " thermo sensitivity to " + i for i in surf.species_names] sens_list = gasrxn_sens_str + surfrxn_sens_str # + gastherm_sens_str writer.writerow([ "Distance (mm)", "T (C)", "P (Pa)", "V (M^3/s)", "X_nh3 initial", "X_o2 initial", "X_he initial", "(NH3/O2)", "T (C) final", "Rtol", "Atol", "reactor type", "energy on?" ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str + sens_list) else: writer.writerow([ "Distance (mm)", "T (C)", "P (Pa)", "V (M^3/s)", "X_nh3 initial", "X_o2 initial", "X_he initial", "(NH3/O2)", "T (C) final", "Rtol", "Atol", "reactor type", "energy on?" ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str) t = 0.0 dt = 0.1 iter_ct = 0 # run the simulation first_run = True distance_mm = 0 for n in range(number_of_reactors): # Set the state of the reservoir to match that of the previous reactor gas.TDY = TDY = r.thermo.TDY inlet.syncState() sim.reinitialize() previous_coverages = surf.coverages # in case we want to retry if n > 0: # Add a first row in the CSV with just the feed try: sim.advance_to_steady_state() except ct.CanteraError: t = sim.time sim.set_initial_time(0) gas.TDY = TDY surf.coverages = previous_coverages r.syncState() sim.reinitialize() new_target_time = 0.01 * t logging.warning( f"Couldn't reach {t:.1g} s so going to try {new_target_time:.1g} s" ) try: sim.advance(new_target_time) except ct.CanteraError: outfile.close() raise # save flux diagrams at beginning of run if first_run == True: save_flux_diagrams(gas, suffix=flux_path, timepoint="beginning", species_path=species_path) save_flux_diagrams(surf, suffix=flux_path, timepoint="beginning", species_path=species_path) first_run = False if sensitivity: # get sensitivity for sensitive species i (e.g. methanol) in reaction j for i in sens_species: g_nrxn = gas.n_reactions s_nrxn = surf.n_reactions # g_nspec = gas.n_species # s_nspec = surf.n_species gas_sensitivities = [ sim.sensitivity(i, j) for j in range(g_nrxn) ] surf_sensitivities = [ sim.sensitivity(i, j) for j in range(g_nrxn, g_nrxn + s_nrxn) ] # gas_therm_sensitivities = [sim.sensitivity(i,j) # for j in range(g_nrxn+s_nrxn,g_nrxn+s_nrxn+g_nspec)] # surf_therm_sensitivities = [sim.sensitivity(i,j) # for j in range(g_nrxn+s_nrxn+g_nspec,g_nrxn+s_nrxn+g_nspec+s_nspec)] sensitivities_all = ( gas_sensitivities + surf_sensitivities # + gas_therm_sensitivities ) writer.writerow([ distance_mm, temp, gas.P, volume_flow, X_nh3, X_o2, X_he, o2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, energy, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress) + sensitivities_all, ) else: writer.writerow([ distance_mm, temp, gas.P, volume_flow, X_nh3, X_o2, X_he, o2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, energy, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress)) iter_ct += 1 distance_mm = n * rlength * 1.0e3 # distance in mm outfile.close() # save flux diagrams at the end of the run save_flux_diagrams(gas, suffix=flux_path, timepoint="end", species_path=species_path) save_flux_diagrams(surf, suffix=flux_path, timepoint="end", species_path=species_path) return
N = 1000 # Number of step gas1 = ct.Solution(file, 'gas') gas1.TPX = t0, ct.one_atm, composition # initial conditions for calculate the mass flow rate gas1() m_dot = Q * gas1.density_mass print('mass flow rate', m_dot) gas1.TPX = T, P, composition sur1 = ct.Interface(file, phase_cat, [gas1]) sur1.TP = T, P r1 = ct.IdealGasConstPressureReactor(gas1, energy='on') #r1.volume = A_eq * l # reacting volume #print(r1.volume) rsur1 = ct.ReactorSurface(sur1, r1, A=l * A_eq * Spv) sim1 = ct.ReactorNet([r1]) TDY = gas1.TDY cov = rsur1.coverages gas1.TDY = TDY gas1() print(sur1.report()) #t_total1 = l * A_eq / Q # Estimate the residence time t_total1 = r1.mass / m_dot print(t_total1) dt = t_total1 / N t1 = (np.arange(N) + 1) * dt z1 = np.zeros_like(t1) u1 = np.zeros_like(t1) state1 = ct.SolutionArray(r1.thermo)
# create a new reactor gas.TDY = TDY r = ct.IdealGasReactor(gas, energy='off') r.volume = rvol # create a reservoir to represent the reactor immediately upstream. Note # that the gas object is set already to the state of the upstream reactor upstream = ct.Reservoir(gas, name='upstream') # create a reservoir for the reactor to exhaust into. The composition of # this reservoir is irrelevant. downstream = ct.Reservoir(gas, name='downstream') # Add the reacting surface to the reactor. The area is set to the desired # catalyst area in the reactor. rsurf = ct.ReactorSurface(surf, r, A=cat_area) # The mass flow rate into the reactor will be fixed by using a # MassFlowController object. m = ct.MassFlowController(upstream, r, mdot=mass_flow_rate) # We need an outlet to the downstream reservoir. This will determine the # pressure in the reactor. The value of K will only affect the transient # pressure difference. v = ct.PressureController(r, downstream, master=m, K=1e-5) sim = ct.ReactorNet([r]) sim.max_err_test_fails = 12 # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-9
def run_reactor( cti_file, t_array=[548], p_array=[1], v_array=[2.7155e-8], #14*7*(140e-4)^2*π/2*0.9=0.02715467 (cm3) o2_array=[0.88], nh3_array=[0.066], rtol=1.0e-11, atol=1.0e-22, reactor_type=0, energy="off", sensitivity=False, sensatol=1e-6, sensrtol=1e-6, reactime=1e5, ): #14 aluminum plates, each of them containing seven semi-cylindrical mi-crochannels of 280 µm width # and 140 µm depth, 9 mm long, arranged at equal distances of 280 µm try: array_i = int(os.getenv("SLURM_ARRAY_TASK_ID")) except TypeError: array_i = 0 # get git commit hash and message repo = git.Repo("/work/westgroup/lee.ting/cantera/ammonia/") git_sha = str(repo.head.commit)[0:6] git_msg = str(repo.head.commit.message)[0:20].replace(" ", "_").replace("'", "_") # this should probably be outside of function settings = list(itertools.product(t_array, p_array, v_array, o2_array, nh3_array)) # constants pi = math.pi # set initial temps, pressures, concentrations temp = settings[array_i][0] # kelvin temp_str = str(temp)[0:3] pressure = settings[array_i][1] * ct.one_atm # Pascals X_o2 = settings[array_i][3] x_O2_str = str(X_o2)[0:3].replace(".", "_") if X_o2 == 0.88: X_he = 0.054 else: X_he = 0 X_nh3 = (1 - (X_o2 + X_he)) * (settings[array_i][4]) x_NH3_str = str(X_nh3)[0:8].replace(".", "_") mw_nh3 = 17.0306e-3 # [kg/mol] mw_o2 = 31.999e-3 # [kg/mol] mw_he = 4.002602e-3 # [kg/mol] o2_ratio = X_nh3 / X_o2 # O2/NH3/He: typical is concentrations_rmg = {"O2(2)": X_o2, "NH3(6)": X_nh3, "He": X_he} # initialize cantera gas and surface gas = ct.Solution(cti_file, "gas") # surf_grab = ct.Interface(cti_file,'surface1_grab', [gas_grab]) surf = ct.Interface(cti_file, "surface1", [gas]) # gas_grab.TPX = gas.TPX = temp, pressure, concentrations_rmg surf.TP = temp, pressure # create gas inlet inlet = ct.Reservoir(gas) # create gas outlet exhaust = ct.Reservoir(gas) # Reactor volume rradius = 1.4e-4 #140µm to 0.00014m rlength = 9e-3 #9mm to 0.009m rvol = (rradius ** 2) * pi * rlength / 2 # Catalyst Surface Area site_density = (surf.site_density * 1000) # [mol/m^2]cantera uses kmol/m^2, convert to mol/m^2 cat_area = rradius * 2 / 2 * pi * rlength # [m^3] #suface site density = 1.86e-9 mol/cm2 = 1.96e-5 mol/m2; molecular weight for Pt = 195.084 g/mol # per kg has 5.125997 moles Pt = 5.125997*6.022e23/1.12e15(cm-2) = 2.756138744e9 cm2/kg = 2.756e5m2/kg # reactor initialization if reactor_type == 0: r = ct.Reactor(gas, energy=energy) reactor_type_str = "Reactor" elif reactor_type == 1: r = ct.IdealGasReactor(gas, energy=energy) reactor_type_str = "IdealGasReactor" elif reactor_type == 2: r = ct.ConstPressureReactor(gas, energy=energy) reactor_type_str = "ConstPressureReactor" elif reactor_type == 3: r = ct.IdealGasConstPressureReactor(gas, energy=energy) reactor_type_str = "IdealGasConstPressureReactor" rsurf = ct.ReactorSurface(surf, r, A=cat_area) r.volume = rvol surf.coverages = "X(1):1.0" # flow controllers (Graaf measured flow at 293.15 and 1 atm) one_atm = ct.one_atm FC_temp = 293.15 volume_flow = settings[array_i][2] # [m^3/s] molar_flow = volume_flow * one_atm / (8.3145 * FC_temp) # [mol/s] mass_flow = molar_flow * (X_nh3 * mw_nh3 + X_o2 * mw_o2 + X_he * mw_he) # [kg/s] mfc = ct.MassFlowController(inlet, r, mdot=mass_flow) # A PressureController has a baseline mass flow rate matching the 'master' # MassFlowController, with an additional pressure-dependent term. By explicitly # including the upstream mass flow rate, the pressure is kept constant without # needing to use a large value for 'K', which can introduce undesired stiffness. outlet_mfc = ct.PressureController(r, exhaust, master=mfc, K=0.01) # initialize reactor network sim = ct.ReactorNet([r]) # set relative and absolute tolerances on the simulation sim.rtol = 1.0e-11 sim.atol = 1.0e-22 ################################################# # Run single reactor ################################################# # round numbers so they're easier to read # temp_str = '%s' % '%.3g' % tempn cat_area_str = "%s" % "%.3g" % cat_area results_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/transient/{temp_str}/results" ) results_path_csp = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/transient/{temp_str}/results/csp" ) flux_path = ( os.path.dirname(os.path.abspath(__file__)) + f"/{git_sha}_{git_msg}/{reactor_type_str}/transient/{temp_str}/flux_diagrams/{x_O2_str}/{x_NH3_str}" ) try: os.makedirs(results_path, exist_ok=True) except OSError as error: print(error) try: os.makedirs(results_path_csp, exist_ok=True) except OSError as error: print(error) try: os.makedirs(flux_path, exist_ok=True) except OSError as error: print(error) gas_ROP_str = [i + " ROP [kmol/m^3 s]" for i in gas.species_names] # surface ROP reports gas and surface ROP. these values might be redundant, not sure. gas_surf_ROP_str = [i + " surface ROP [kmol/m^2 s]" for i in gas.species_names] surf_ROP_str = [i + " ROP [kmol/m^2 s]" for i in surf.species_names] gasrxn_ROP_str = [i + " ROP [kmol/m^3 s]" for i in gas.reaction_equations()] surfrxn_ROP_str = [i + " ROP [kmol/m^2 s]" for i in surf.reaction_equations()] output_filename = ( results_path + f"/Spinning_basket_area_{cat_area_str}_energy_{energy}" + f"_temp_{temp}_O2_{x_O2_str}_NH3_{x_NH3_str}.csv" ) output_filename_csp = ( results_path_csp + f"/Spinning_basket_area_{cat_area_str}_energy_{energy}" + f"_temp_{temp}_h2_{x_O2_str}_NH3_{x_NH3_str}.csv" ) outfile = open(output_filename, "w") outfile_csp = open(output_filename_csp, "w") writer = csv.writer(outfile) writer_csp = csv.writer(outfile_csp) # Sensitivity atol, rtol, and strings for gas and surface reactions if selected # slows down script by a lot if sensitivity: sim.rtol_sensitivity = sensrtol sim.atol_sensitivity = sensatol sens_species = ["NH3(6)"] #change THIS to your species, can add "," and other species # turn on sensitive reactions/species for i in range(gas.n_reactions): r.add_sensitivity_reaction(i) for i in range(surf.n_reactions): rsurf.add_sensitivity_reaction(i) # for i in range(gas.n_species): # r.add_sensitivity_species_enthalpy(i) # for i in range(surf.n_species): # rsurf.add_sensitivity_species_enthalpy(i) for j in sens_species: gasrxn_sens_str = [ j + " sensitivity to " + i for i in gas.reaction_equations() ] surfrxn_sens_str = [ j + " sensitivity to " + i for i in surf.reaction_equations() ] # gastherm_sens_str = [j + " thermo sensitivity to " + i for i in gas.species_names] # surftherm_sens_str = [j + " thermo sensitivity to " + i for i in surf.species_names] sens_list = gasrxn_sens_str + surfrxn_sens_str # + gastherm_sens_str writer.writerow( [ "T (C)", "P (atm)", "V (M^3/s)", "X_nh3 initial", "X_o2 initial", "X_he initial", "(NH3/O2)", "T (C) final", "Rtol", "Atol", "reactor type", ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str + sens_list ) else: writer.writerow( [ "T (C)", "P (atm)", "V (M^3/s)", "X_nh3 initial", "X_o2 initial", "X_he initial", "(NH3/O2)", "T (C) final", "Rtol", "Atol", "reactor type", ] + gas.species_names + surf.species_names + gas_ROP_str + gas_surf_ROP_str + surf_ROP_str + gasrxn_ROP_str + surfrxn_ROP_str ) writer_csp.writerow( ["iter", "t", "dt", "Density[kg/m3]", "Pressure[Pascal]", "Temperature[K]",] + gas.species_names + surf.species_names ) t = 0.0 dt = 0.1 iter_ct = 0 # run the simulation first_run = True while t < reactime: # save flux diagrams at beginning of run if first_run == True: save_flux_diagrams(gas, suffix=flux_path, timepoint="beginning") save_flux_diagrams(surf, suffix=flux_path, timepoint="beginning") first_run = False t += dt sim.advance(t) # if t % 10 < 0.01: if sensitivity: # get sensitivity for sensitive species i (e.g. methanol) in reaction j for i in sens_species: g_nrxn = gas.n_reactions s_nrxn = surf.n_reactions # g_nspec = gas.n_species # s_nspec = surf.n_species gas_sensitivities = [sim.sensitivity(i, j) for j in range(g_nrxn)] surf_sensitivities = [ sim.sensitivity(i, j) for j in range(g_nrxn, g_nrxn + s_nrxn) ] # gas_therm_sensitivities = [sim.sensitivity(i,j) # for j in range(g_nrxn+s_nrxn,g_nrxn+s_nrxn+g_nspec)] # surf_therm_sensitivities = [sim.sensitivity(i,j) # for j in range(g_nrxn+s_nrxn+g_nspec,g_nrxn+s_nrxn+g_nspec+s_nspec)] sensitivities_all = ( gas_sensitivities + surf_sensitivities # + gas_therm_sensitivities ) writer.writerow( [ temp, pressure, volume_flow, X_nh3, X_o2, X_he, o2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress) + sensitivities_all ) else: writer.writerow( [ temp, pressure, volume_flow, X_nh3, X_o2, X_he, o2_ratio, gas.T, sim.rtol, sim.atol, reactor_type_str, ] + list(gas.X) + list(surf.X) + list(gas.net_production_rates) + list(surf.net_production_rates) + list(gas.net_rates_of_progress) + list(surf.net_rates_of_progress) ) writer_csp.writerow( [ iter_ct, sim.time, dt, gas.density, gas.P, gas.T, ] + list(gas.X) + list(surf.X) ) iter_ct += 1 outfile.close() outfile_csp.close() # save flux diagrams at the end of the run save_flux_diagrams(gas, suffix=flux_path, timepoint="end") save_flux_diagrams(surf, suffix=flux_path, timepoint="end") return