From 2527869536bec16a9e92a593d9389b632dac1409 Mon Sep 17 00:00:00 2001 From: bangshiuh Date: Sat, 4 Aug 2018 12:31:12 -0400 Subject: [PATCH] [1D/Python] Create BurnerIonFlame and add test Create a base class (IonFlameBase) for both IonFreeFlame and BurnerIonFlame, and use the set_axisymmetric_flow() and set_free_flow() methods to select the flow type. Also combines FreeFlow and AxisymmetricStagnationFlow classes into class IdealGasFlow. --- include/cantera/oneD/StFlow.h | 2 + interfaces/cython/cantera/_cantera.pxd | 8 +- .../examples/onedim/ion_burner_flame.py | 29 +++++ .../{ion_flame.py => ion_free_flame.py} | 2 +- interfaces/cython/cantera/onedim.py | 117 +++++++++++------- interfaces/cython/cantera/onedim.pyx | 101 ++++++++------- interfaces/cython/cantera/test/test_onedim.py | 45 +++++-- 7 files changed, 197 insertions(+), 107 deletions(-) create mode 100644 interfaces/cython/cantera/examples/onedim/ion_burner_flame.py rename interfaces/cython/cantera/examples/onedim/{ion_flame.py => ion_free_flame.py} (96%) diff --git a/include/cantera/oneD/StFlow.h b/include/cantera/oneD/StFlow.h index bb664b41d..691557fb0 100644 --- a/include/cantera/oneD/StFlow.h +++ b/include/cantera/oneD/StFlow.h @@ -149,10 +149,12 @@ public: void setFreeFlow() { m_type = cFreeFlow; + m_dovisc = false; } void setAxisymmetricFlow() { m_type = cAxisymmetricStagnationFlow; + m_dovisc = true; } virtual std::string flowType() { diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 5cf42e50b..1f4138bca 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -687,7 +687,6 @@ cdef extern from "cantera/oneD/StFlow.h": cbool doEnergy(size_t) void enableSoret(cbool) except +translate_exception cbool withSoret() - void setViscosityFlag(bool) void setFreeFlow() void setAxisymmetricFlow() @@ -1041,13 +1040,16 @@ cdef class ReactingSurface1D(Boundary1D): cdef class _FlowBase(Domain1D): cdef CxxStFlow* flow -cdef class FreeFlow(_FlowBase): +cdef class IdealGasFlow(_FlowBase): + pass + +cdef class FreeFlow(IdealGasFlow): pass cdef class IonFlow(_FlowBase): pass -cdef class AxisymmetricStagnationFlow(_FlowBase): +cdef class AxisymmetricStagnationFlow(IdealGasFlow): pass cdef class Sim1D: diff --git a/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py b/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py new file mode 100644 index 000000000..f6982a18f --- /dev/null +++ b/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py @@ -0,0 +1,29 @@ +""" +A burner-stabilized lean premixed hydrogen-oxygen flame at low pressure. +""" + +import cantera as ct +import numpy as np + +p = ct.one_atm +tburner = 600.0 +reactants = 'CH4:1.0, O2:2.0, N2:7.52' # premixed gas composition +width = 0.5 # m +loglevel = 1 # amount of diagnostic output (0 to 5) + +gas = ct.Solution('gri30_ion.cti') +gas.TPX = tburner, p, reactants +mdot = 0.15 * gas.density + +f = ct.IonBurnerFlame(gas, width=width) +f.burner.mdot = mdot +f.set_refine_criteria(ratio=3.0, slope=0.05, curve=0.1) +f.show_solution() + +f.transport_model = 'Ion' +f.solve(loglevel, auto=True) +f.solve(loglevel=loglevel, stage=2, enable_energy=True) +f.save('CH4_burner_flame.xml', 'mix', 'solution with mixture-averaged transport') + +f.write_csv('CH4_burner_flame.csv', quiet=False) + diff --git a/interfaces/cython/cantera/examples/onedim/ion_flame.py b/interfaces/cython/cantera/examples/onedim/ion_free_flame.py similarity index 96% rename from interfaces/cython/cantera/examples/onedim/ion_flame.py rename to interfaces/cython/cantera/examples/onedim/ion_free_flame.py index 7f467ae23..aaa278ed8 100644 --- a/interfaces/cython/cantera/examples/onedim/ion_flame.py +++ b/interfaces/cython/cantera/examples/onedim/ion_free_flame.py @@ -18,7 +18,7 @@ gas = ct.Solution('gri30_ion.xml') gas.TPX = Tin, p, reactants # Set up flame object -f = ct.IonFlame(gas, width=width) +f = ct.IonFreeFlame(gas, width=width) f.set_refine_criteria(ratio=3, slope=0.05, curve=0.1) f.show_solution() diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index f1695d1a1..b8225b947 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -394,9 +394,9 @@ class FreeFlame(FlameBase): def __init__(self, gas, grid=None, width=None): """ - A domain of type FreeFlow named 'flame' will be created to represent - the flame. The three domains comprising the stack are stored as - ``self.inlet``, ``self.flame``, and ``self.outlet``. + A domain of type IdealGasFlow named 'flame' will be created to represent + the flame and set to free flow. The three domains comprising the stack + are stored as ``self.inlet``, ``self.flame``, and ``self.outlet``. :param grid: A list of points to be used as the initial grid. Not recommended @@ -410,7 +410,8 @@ class FreeFlame(FlameBase): self.outlet = Outlet1D(name='products', phase=gas) if not hasattr(self, 'flame'): # Create flame domain if not already instantiated by a child class - self.flame = FreeFlow(gas, name='flame') + self.flame = IdealGasFlow(gas, name='flame') + self.flame.set_free_flow() if width is not None: grid = np.array([0.0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0]) * width @@ -561,29 +562,12 @@ class FreeFlame(FlameBase): return self.solve_adjoint(perturb, self.gas.n_reactions, dgdx) / Su0 -class IonFlame(FreeFlame): - __slots__ = ('inlet', 'outlet', 'flame') - - def __init__(self, gas, grid=None, width=None): - if not hasattr(self, 'flame'): - # Create flame domain if not already instantiated by a child class - self.flame = IonFlow(gas, name='flame') - - super(IonFlame, self).__init__(gas, grid, width) - - def solve(self, loglevel=1, refine_grid=True, auto=False, stage=1, enable_energy=True): - self.flame.set_solvingStage(stage) - if stage == 1: - super(IonFlame, self).solve(loglevel, refine_grid, auto) - if stage == 2: - self.poisson_enabled = True - super(IonFlame, self).solve(loglevel, refine_grid, auto) +class IonFlameBase(FlameBase): def write_csv(self, filename, species='X', quiet=True): """ Write the velocity, temperature, density, electric potential, , electric field stregth, and species profiles to a CSV file. - :param filename: Output file name :param species: @@ -641,6 +625,26 @@ class IonFlame(FreeFlame): Efield.append((phi[np-2] - phi[np-1]) / (z[np-1] - z[np-2])) return Efield + def solve(self, loglevel=1, refine_grid=True, auto=False, stage=1, enable_energy=True): + self.flame.set_solvingStage(stage) + if stage == 1: + super(IonFlameBase, self).solve(loglevel, refine_grid, auto) + if stage == 2: + self.poisson_enabled = True + super(IonFlameBase, self).solve(loglevel, refine_grid, auto) + + +class IonFreeFlame(IonFlameBase, FreeFlame): + __slots__ = ('inlet', 'outlet', 'flame') + + def __init__(self, gas, grid=None, width=None): + if not hasattr(self, 'flame'): + # Create flame domain if not already instantiated by a child class + self.flame = IonFlow(gas, name='flame') + self.flame.set_free_flow() + + super(IonFreeFlame, self).__init__(gas, grid, width) + class BurnerFlame(FlameBase): """A burner-stabilized flat flame.""" @@ -659,14 +663,17 @@ class BurnerFlame(FlameBase): Defines a grid on the interval [0, width] with internal points determined automatically by the solver. - A domain of class `AxisymmetricStagnationFlow` named ``flame`` will - be created to represent the flame. The three domains comprising the - stack are stored as ``self.burner``, ``self.flame``, and - ``self.outlet``. + A domain of class `IdealGasFlow` named ``flame`` will be created to + represent the flame and set to axisymmetric stagnation flow. The three + domains comprising the stack are stored as ``self.burner``, + ``self.flame``, and ``self.outlet``. """ self.burner = Inlet1D(name='burner', phase=gas) self.outlet = Outlet1D(name='outlet', phase=gas) - self.flame = AxisymmetricStagnationFlow(gas, name='flame') + if not hasattr(self, 'flame'): + # Create flame domain if not already instantiated by a child class + self.flame = IdealGasFlow(gas, name='flame') + self.flame.set_axisymmetric_flow() if width is not None: grid = np.array([0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0]) * width @@ -765,6 +772,19 @@ class BurnerFlame(FlameBase): self.set_steady_callback(original_callback) +class IonBurnerFlame(IonFlameBase, BurnerFlame): + """A burner-stabilized flat flame with ionized gas.""" + __slots__ = ('burner', 'flame', 'outlet') + + def __init__(self, gas, grid=None, width=None): + if not hasattr(self, 'flame'): + # Create flame domain if not already instantiated by a child class + self.flame = IonFlow(gas, name='flame') + self.flame.set_axisymmetric_flow() + + super(IonBurnerFlame, self).__init__(gas, grid, width) + + class CounterflowDiffusionFlame(FlameBase): """ A counterflow diffusion flame """ __slots__ = ('fuel_inlet', 'flame', 'oxidizer_inlet') @@ -782,10 +802,10 @@ class CounterflowDiffusionFlame(FlameBase): Defines a grid on the interval [0, width] with internal points determined automatically by the solver. - A domain of class `AxisymmetricStagnationFlow` named ``flame`` will - be created to represent the flame. The three domains comprising the - stack are stored as ``self.fuel_inlet``, ``self.flame``, and - ``self.oxidizer_inlet``. + A domain of class `IdealGasFlow` named ``flame`` will be created to + represent the flame and set to axisymmetric stagnation flow. The three + domains comprising the stack are stored as ``self.fuel_inlet``, + ``self.flame``, and ``self.oxidizer_inlet``. """ self.fuel_inlet = Inlet1D(name='fuel_inlet', phase=gas) self.fuel_inlet.T = gas.T @@ -793,7 +813,8 @@ class CounterflowDiffusionFlame(FlameBase): self.oxidizer_inlet = Inlet1D(name='oxidizer_inlet', phase=gas) self.oxidizer_inlet.T = gas.T - self.flame = AxisymmetricStagnationFlow(gas, name='flame') + self.flame = IdealGasFlow(gas, name='flame') + self.flame.set_axisymmetric_flow() if width is not None: grid = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) * width @@ -1062,12 +1083,14 @@ class ImpingingJet(FlameBase): :param surface: A Kinetics object used to compute any surface reactions. - A domain of class `AxisymmetricStagnationFlow` named ``flame`` will be - created to represent the flow. The three domains comprising the stack - are stored as ``self.inlet``, ``self.flame``, and ``self.surface``. + A domain of class `IdealGasFlow` named ``flame`` will be created to + represent the flame and set to axisymmetric stagnation flow. The three + domains comprising the stack are stored as ``self.inlet``, + ``self.flame``, and ``self.surface``. """ self.inlet = Inlet1D(name='inlet', phase=gas) - self.flame = AxisymmetricStagnationFlow(gas, name='flame') + self.flame = IdealGasFlow(gas, name='flame') + self.flame.set_axisymmetric_flow() if width is not None: grid = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) * width @@ -1138,10 +1161,10 @@ class CounterflowPremixedFlame(FlameBase): Defines a grid on the interval [0, width] with internal points determined automatically by the solver. - A domain of class `AxisymmetricStagnationFlow` named ``flame`` will - be created to represent the flame. The three domains comprising the - stack are stored as ``self.reactants``, ``self.flame``, and - ``self.products``. + A domain of class `IdealGasFlow` named ``flame`` will be created to + represent the flame and set to axisymmetric stagnation flow. The three + domains comprising the stack are stored as ``self.reactants``, + ``self.flame``, and ``self.products``. """ self.reactants = Inlet1D(name='reactants', phase=gas) self.reactants.T = gas.T @@ -1149,7 +1172,8 @@ class CounterflowPremixedFlame(FlameBase): self.products = Inlet1D(name='products', phase=gas) self.products.T = gas.T - self.flame = AxisymmetricStagnationFlow(gas, name='flame') + self.flame = IdealGasFlow(gas, name='flame') + self.flame.set_axisymmetric_flow() if width is not None: # Create grid points aligned with initial guess profile @@ -1231,15 +1255,16 @@ class CounterflowTwinPremixedFlame(FlameBase): Defines a grid on the interval [0, width] with internal points determined automatically by the solver. - A domain of class `AxisymmetricStagnationFlow` named ``flame`` will - be created to represent the flame. The three domains comprising the - stack are stored as ``self.reactants``, ``self.flame``, and - ``self.products``. + A domain of class `IdealGasFlow` named ``flame`` will be created to + represent the flame and set to axisymmetric stagnation flow. The three + domains comprising the stack are stored as ``self.reactants``, + ``self.flame``, and ``self.products``. """ self.reactants = Inlet1D(name='reactants', phase=gas) self.reactants.T = gas.T - self.flame = AxisymmetricStagnationFlow(gas, name='flame') + self.flame = IdealGasFlow(gas, name='flame') + self.flame.set_axisymmetric_flow() #The right boundary is a symmetry plane self.products = SymmetryPlane1D(name='products', phase=gas) diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index 02137e045..00986155e 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -2,6 +2,7 @@ # at http://www.cantera.org/license.txt for license and copyright information. import interrupts +import warnings # Need a pure-python class to store weakrefs to class _WeakrefProxy(object): @@ -467,13 +468,12 @@ cdef class _FlowBase(Domain1D): def __set__(self, do_radiation): self.flow.enableRadiation(do_radiation) - def set_viscosityFlag(self, dovisc): - self.flow.setViscosityFlag(dovisc) - - def set_freeFlow(self): + def set_free_flow(self): + """ Set flow type to free flow.""" self.flow.setFreeFlow() - def set_axisymmetricFlow(self): + def set_axisymmetric_flow(self): + """ Set flow type to axisymmetric stagnation flow.""" self.flow.setAxisymmetricFlow() @@ -483,48 +483,12 @@ cdef CxxIdealGasPhase* getIdealGasPhase(ThermoPhase phase) except *: return (phase.thermo) -cdef class FreeFlow(_FlowBase): - def __cinit__(self, _SolutionBase thermo, *args, **kwargs): - gas = getIdealGasPhase(thermo) - self.flow = new CxxStFlow(gas, thermo.n_species, 2) - self.set_freeFlow() - self.set_viscosityFlag(False) - - -cdef class IonFlow(_FlowBase): +cdef class IdealGasFlow(_FlowBase): """ - An ion flow domain. + An ideal gas flow domain. Functions set_free_flow and set_axisymmetric_flow + can be used to set different type of flow. - In an ion flow dommain, the electric drift is added to the diffusion flux - """ - def __cinit__(self, _SolutionBase thermo, *args, **kwargs): - gas = getIdealGasPhase(thermo) - self.flow = (new CxxIonFlow(gas, thermo.n_species, 2)) - self.set_freeFlow() - self.set_viscosityFlag(False) - - def set_solvingStage(self, stage): - (self.flow).setSolvingStage(stage) - - def set_electricPotential(self, v_inlet, v_outlet): - (self.flow).setElectricPotential(v_inlet, v_outlet) - - property poisson_enabled: - """ Determines whether or not to solve the energy equation.""" - def __get__(self): - return (self.flow).doPoisson(0) - def __set__(self, enable): - if enable: - (self.flow).solvePoissonEqn() - else: - (self.flow).fixElectricPotential() - - -cdef class AxisymmetricStagnationFlow(_FlowBase): - """ - An axisymmetric flow domain. - - In an axisymmetric flow domain, the equations solved are the similarity + For the type of axisymmetric flow, the equations solved are the similarity equations for the flow in a finite-height gap of infinite radial extent. The solution variables are: @@ -552,8 +516,51 @@ cdef class AxisymmetricStagnationFlow(_FlowBase): def __cinit__(self, _SolutionBase thermo, *args, **kwargs): gas = getIdealGasPhase(thermo) self.flow = new CxxStFlow(gas, thermo.n_species, 2) - self.set_axisymmetricFlow() - self.set_viscosityFlag(True) + + +cdef class FreeFlow(IdealGasFlow): + def __init__(self, *args, **kwargs): + warnings.warn("Class FreeFlow is deprecated and will be removed after" + " Cantera 2.4. Use class IdealGasFlow instead and call the" + " set_free_flow() method.") + super().__init__(*args, **kwargs) + self.set_free_flow() + + +cdef class AxisymmetricStagnationFlow(IdealGasFlow): + def __init__(self, *args, **kwargs): + warnings.warn("Class AxisymmetricStagnationFlow is deprecated and will" + " be removed after Cantera 2.4. Use class IdealGasFlow instead and" + " call the set_axisymmetric_flow() method.") + super().__init__(*args, **kwargs) + self.set_free_flow() + + +cdef class IonFlow(_FlowBase): + """ + An ion flow domain. + + In an ion flow dommain, the electric drift is added to the diffusion flux + """ + def __cinit__(self, _SolutionBase thermo, *args, **kwargs): + gas = getIdealGasPhase(thermo) + self.flow = (new CxxIonFlow(gas, thermo.n_species, 2)) + + def set_solvingStage(self, stage): + (self.flow).setSolvingStage(stage) + + def set_electricPotential(self, v_inlet, v_outlet): + (self.flow).setElectricPotential(v_inlet, v_outlet) + + property poisson_enabled: + """ Determines whether or not to solve the energy equation.""" + def __get__(self): + return (self.flow).doPoisson(0) + def __set__(self, enable): + if enable: + (self.flow).solvePoissonEqn() + else: + (self.flow).fixElectricPotential() cdef class Sim1D: diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 77886f25a..0f9945dbd 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -9,12 +9,12 @@ class TestOnedim(utilities.CanteraTest): def test_instantiate(self): gas = ct.Solution('h2o2.xml') - flame = ct.FreeFlow(gas) + flame = ct.IdealGasFlow(gas) def test_badInstantiate(self): solid = ct.Solution('diamond.xml', 'diamond') with self.assertRaises(TypeError): - flame = ct.FreeFlow(solid) + flame = ct.IdealGasFlow(solid) def test_instantiateSurface(self): gas = ct.Solution('diamond.xml', 'gas') @@ -28,7 +28,7 @@ class TestOnedim(utilities.CanteraTest): gas1 = ct.Solution('h2o2.xml') gas2 = ct.Solution('h2o2.xml') inlet = ct.Inlet1D(name='something', phase=gas1) - flame = ct.FreeFlow(gas1) + flame = ct.IdealGasFlow(gas1) sim = ct.Sim1D((inlet, flame)) self.assertEqual(inlet.name, 'something') @@ -53,7 +53,7 @@ class TestOnedim(utilities.CanteraTest): def test_grid_check(self): gas = ct.Solution('h2o2.xml') - flame = ct.FreeFlow(gas) + flame = ct.IdealGasFlow(gas) with self.assertRaises(ct.CanteraError): flame.grid = [0, 0.1, 0.1, 0.2] @@ -64,21 +64,21 @@ class TestOnedim(utilities.CanteraTest): def test_unpicklable(self): import pickle gas = ct.Solution('h2o2.xml') - flame = ct.FreeFlow(gas) + flame = ct.IdealGasFlow(gas) with self.assertRaises(NotImplementedError): pickle.dumps(flame) def test_uncopyable(self): import copy gas = ct.Solution('h2o2.xml') - flame = ct.FreeFlow(gas) + flame = ct.IdealGasFlow(gas) with self.assertRaises(NotImplementedError): copy.copy(flame) def test_invalid_property(self): gas1 = ct.Solution('h2o2.xml') inlet = ct.Inlet1D(name='something', phase=gas1) - flame = ct.FreeFlow(gas1) + flame = ct.IdealGasFlow(gas1) sim = ct.Sim1D((inlet, flame)) for x in (inlet, flame, sim): @@ -90,7 +90,7 @@ class TestOnedim(utilities.CanteraTest): def test_tolerances(self): gas = ct.Solution('h2o2.xml') left = ct.Inlet1D(gas) - flame = ct.FreeFlow(gas) + flame = ct.IdealGasFlow(gas) right = ct.Inlet1D(gas) # Some things don't work until the domains have been added to a Sim1D sim = ct.Sim1D((left, flame, right)) @@ -938,7 +938,7 @@ class TestTwinFlame(utilities.CanteraTest): self.solve(phi=0.4, T=300, width=0.05, P=0.1) -class TestIonFlame(utilities.CanteraTest): +class TestIonFreeFlame(utilities.CanteraTest): def test_ion_profile(self): reactants = 'CH4:0.216, O2:2' p = ct.one_atm @@ -948,7 +948,7 @@ class TestIonFlame(utilities.CanteraTest): # IdealGasMix object used to compute mixture properties self.gas = ct.Solution('ch4_ion.cti') self.gas.TPX = Tin, p, reactants - self.sim = ct.IonFlame(self.gas, width=width) + self.sim = ct.IonFreeFlame(self.gas, width=width) self.sim.set_refine_criteria(ratio=4, slope=0.8, curve=1.0) # Ionized species may require tighter absolute tolerances self.sim.flame.set_steady_tolerances(Y=(1e-4, 1e-12)) @@ -962,3 +962,28 @@ class TestIonFlame(utilities.CanteraTest): # Regression test self.assertNear(max(self.sim.E), 132.1922, 1e-3) + + +class TestIonBurnerFlame(utilities.CanteraTest): + def test_ion_profile(self): + reactants = 'CH4:1.0, O2:2.0, N2:7.52' + p = ct.one_atm + Tburner = 400 + width = 0.03 + + # IdealGasMix object used to compute mixture properties + self.gas = ct.Solution('ch4_ion.cti') + self.gas.TPX = Tburner, p, reactants + self.sim = ct.IonBurnerFlame(self.gas, width=width) + self.sim.set_refine_criteria(ratio=4, slope=0.8, curve=1.0) + self.sim.burner.mdot = self.gas.density * 0.15 + self.sim.transport_model = 'Ion' + + # stage one + self.sim.solve(loglevel=0, auto=True) + + #stage two + self.sim.solve(loglevel=0, stage=2, enable_energy=True) + + # Regression test + self.assertNear(max(self.sim.E), 469.7287, 1e-3)