[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.
This commit is contained in:
bangshiuh 2018-08-04 12:31:12 -04:00 committed by Ray Speth
parent a5762ea6b6
commit 2527869536
7 changed files with 197 additions and 107 deletions

View file

@ -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() {

View file

@ -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:

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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(<cbool>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 <CxxIdealGasPhase*>(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 = <CxxStFlow*>(new CxxIonFlow(gas, thermo.n_species, 2))
self.set_freeFlow()
self.set_viscosityFlag(False)
def set_solvingStage(self, stage):
(<CxxIonFlow*>self.flow).setSolvingStage(stage)
def set_electricPotential(self, v_inlet, v_outlet):
(<CxxIonFlow*>self.flow).setElectricPotential(v_inlet, v_outlet)
property poisson_enabled:
""" Determines whether or not to solve the energy equation."""
def __get__(self):
return (<CxxIonFlow*>self.flow).doPoisson(0)
def __set__(self, enable):
if enable:
(<CxxIonFlow*>self.flow).solvePoissonEqn()
else:
(<CxxIonFlow*>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 = <CxxStFlow*>(new CxxIonFlow(gas, thermo.n_species, 2))
def set_solvingStage(self, stage):
(<CxxIonFlow*>self.flow).setSolvingStage(stage)
def set_electricPotential(self, v_inlet, v_outlet):
(<CxxIonFlow*>self.flow).setElectricPotential(v_inlet, v_outlet)
property poisson_enabled:
""" Determines whether or not to solve the energy equation."""
def __get__(self):
return (<CxxIonFlow*>self.flow).doPoisson(0)
def __set__(self, enable):
if enable:
(<CxxIonFlow*>self.flow).solvePoissonEqn()
else:
(<CxxIonFlow*>self.flow).fixElectricPotential()
cdef class Sim1D:

View file

@ -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)