cantera/interfaces/cython/cantera/onedim.py

1030 lines
38 KiB
Python

# This file is part of Cantera. See License.txt in the top-level directory or
# at http://www.cantera.org/license.txt for license and copyright information.
import numpy as np
from ._cantera import *
from .composite import Solution
import csv as _csv
try:
# Python 2.7 or 3.2+
from math import erf
except ImportError:
from scipy.special import erf
class FlameBase(Sim1D):
""" Base class for flames with a single flow domain """
__slots__ = ('gas',)
def __init__(self, domains, gas, grid=None):
"""
:param gas:
object to use to evaluate all gas properties and reaction rates
:param grid:
array of initial grid points
"""
if grid is None:
grid = np.linspace(0.0, 0.1, 6)
self.flame.grid = grid
super(FlameBase, self).__init__(domains)
self.gas = gas
self.flame.P = gas.P
def set_refine_criteria(self, ratio=10.0, slope=0.8, curve=0.8, prune=0.0):
"""
Set the criteria used for grid refinement.
:param ratio:
additional points will be added if the ratio of the spacing on
either side of a grid point exceeds this value
:param slope:
maximum difference in value between two adjacent points, scaled by
the maximum difference in the profile (0.0 < slope < 1.0). Adds
points in regions of high slope.
:param curve:
maximum difference in slope between two adjacent intervals, scaled
by the maximum difference in the profile (0.0 < curve < 1.0). Adds
points in regions of high curvature.
:param prune:
if the slope or curve criteria are satisfied to the level of
'prune', the grid point is assumed not to be needed and is removed.
Set prune significantly smaller than 'slope' and 'curve'. Set to
zero to disable pruning the grid.
>>> f.set_refine_criteria(ratio=3.0, slope=0.1, curve=0.2, prune=0)
"""
super(FlameBase, self).set_refine_criteria(self.flame, ratio, slope,
curve, prune)
def set_profile(self, component, locations, values):
"""
Set an initial estimate for a profile of one component.
:param component:
component name or index
:param positions:
sequence of relative positions, from 0 on the left to 1 on the right
:param values:
sequence of values at the relative positions specified in *positions*
>>> f.set_profile('T', [0.0, 0.2, 1.0], [400.0, 800.0, 1500.0])
"""
super(FlameBase, self).set_profile(self.flame, component, locations,
values)
@property
def transport_model(self):
"""
Get/Set the transport model used by the `Solution` object used for this
simulation.
"""
return self.gas.transport_model
@transport_model.setter
def transport_model(self, model):
self.gas.transport_model = model
self.flame.set_transport(self.gas)
@property
def energy_enabled(self):
""" Get/Set whether or not to solve the energy equation."""
return self.flame.energy_enabled
@energy_enabled.setter
def energy_enabled(self, enable):
self.flame.energy_enabled = enable
@property
def soret_enabled(self):
"""
Get/Set whether or not to include diffusive mass fluxes due to the
Soret effect. Enabling this option works only when using the
multicomponent transport model.
"""
return self.flame.soret_enabled
@soret_enabled.setter
def soret_enabled(self, enable):
self.flame.soret_enabled = enable
@property
def radiation_enabled(self):
"""
Get/Set whether or not to include radiative heat transfer
"""
return self.flame.radiation_enabled
@radiation_enabled.setter
def radiation_enabled(self, enable):
self.flame.radiation_enabled = enable
def set_boundary_emissivities(self, e_left, e_right):
self.flame.set_boundary_emissivities(e_left, e_right)
@property
def grid(self):
""" Array of grid point positions along the flame. """
return self.flame.grid
@property
def P(self):
""" Get/Set the pressure of the flame [Pa] """
return self.flame.P
@P.setter
def P(self, P):
self.flame.P = P
@property
def T(self):
""" Array containing the temperature [K] at each grid point. """
return self.profile(self.flame, 'T')
@property
def u(self):
"""
Array containing the velocity [m/s] normal to the flame at each point.
"""
return self.profile(self.flame, 'u')
@property
def V(self):
"""
Array containing the tangential velocity gradient [1/s] at each point.
"""
return self.profile(self.flame, 'V')
@property
def L(self):
"""
Array containing the radial pressure gradient (1/r)(dP/dr) [N/m^4] at
each point. Note: This value is named 'lambda' in the C++ code.
"""
return self.profile(self.flame, 'lambda')
def elemental_mass_fraction(self, m):
r"""
Get the elemental mass fraction :math:`Z_{\mathrm{mass},m}` of element
:math:`m` at each grid point, which is defined as:
.. math:: Z_{\mathrm{mass},m} = \sum_k \frac{a_{m,k} M_m}{M_k} Y_k
with :math:`a_{m,k}` being the number of atoms of element :math:`m` in
species :math:`k`, :math:`M_m` the atomic weight of element :math:`m`,
:math:`M_k` the molecular weight of species :math:`k`, and :math:`Y_k`
the mass fraction of species :math:`k`.
:param m:
Base element, may be specified by name or by index.
>>> phase.elemental_mass_fraction('H')
[1.0, ..., 0.0]
"""
vals = np.empty(self.flame.n_points)
for i in range(self.flame.n_points):
self.set_gas_state(i)
vals[i] = self.gas.elemental_mass_fraction(m)
return vals
def elemental_mole_fraction(self, m):
r"""
Get the elemental mole fraction :math:`Z_{\mathrm{mole},m}` of element
:math:`m` at each grid point, which is defined as:
.. math:: Z_{\mathrm{mole},m} = \sum_k \frac{a_{m,k}}{\sum_j a_{j,k}} X_k
with :math:`a_{m,k}` being the number of atoms of element :math:`m` in
species :math:`k` and :math:`X_k` the mole fraction of species
:math:`k`.
:param m:
Base element, may be specified by name or by index.
>>> phase.elemental_mole_fraction('H')
[1.0, ..., 0.0]
"""
vals = np.empty(self.flame.n_points)
for i in range(self.flame.n_points):
self.set_gas_state(i)
vals[i] = self.gas.elemental_mole_fraction(m)
return vals
def solution(self, component, point=None):
"""
Get the solution at one point or for the full flame domain (if
`point=None`) for the specified *component*. The *component* can be
specified by name or index.
"""
if point is None:
return self.profile(self.flame, component)
else:
return self.value(self.flame, component, point)
def set_gas_state(self, point):
"""
Set the state of the the Solution object used for calculations,
`self.gas`, to the temperature and composition at the point with index
*point*.
"""
k0 = self.flame.component_index(self.gas.species_name(0))
Y = [self.solution(k, point)
for k in range(k0, k0 + self.gas.n_species)]
self.gas.set_unnormalized_mass_fractions(Y)
self.gas.TP = self.value(self.flame, 'T', point), self.P
@property
def heat_release_rate(self):
"""
Get the total volumetric heat release rate [W/m^3].
"""
return - np.sum(self.partial_molar_enthalpies *
self.net_production_rates, 0)
@property
def heat_production_rates(self):
"""
Get the volumetric heat production rates [W/m^3] on a per-reaction
basis. The sum over all reactions results in the total volumetric heat
release rate.
Example: C. K. Law: Combustion Physics (2006), Fig. 7.8.6
>>> f.heat_production_rates[2] # heat production rate of the 2nd reaction
"""
return - self.net_rates_of_progress * self.delta_standard_enthalpy
def write_csv(self, filename, species='X', quiet=True):
"""
Write the velocity, temperature, density, and species profiles
to a CSV file.
:param filename:
Output file name
:param species:
Attribute to use obtaining species profiles, e.g. ``X`` for
mole fractions or ``Y`` for mass fractions.
"""
z = self.grid
T = self.T
u = self.u
V = self.V
csvfile = open(filename, 'w')
writer = _csv.writer(csvfile)
writer.writerow(['z (m)', 'u (m/s)', 'V (1/s)',
'T (K)', 'rho (kg/m3)'] + self.gas.species_names)
for n in range(self.flame.n_points):
self.set_gas_state(n)
writer.writerow([z[n], u[n], V[n], T[n], self.gas.density] +
list(getattr(self.gas, species)))
csvfile.close()
if not quiet:
print("Solution saved to '{0}'.".format(filename))
def _trim(docstring):
"""Remove block indentation from a docstring."""
if not docstring:
return ''
lines = docstring.splitlines()
# Determine minimum indentation (first line doesn't count):
indent = 999
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < 999:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Return a single string, with trailing and leading blank lines stripped
return '\n'.join(trimmed).strip('\n')
def _array_property(attr, size=None):
"""
Generate a property that retrieves values at each point in the flame. The
'size' argument is the attribute name of the gas object used to set the
leading dimension of the resulting array.
"""
def getter(self):
if size is None:
# 1D array for scalar property
vals = np.empty(self.flame.n_points)
else:
# 2D array
vals = np.empty((getattr(self.gas, size), self.flame.n_points))
for i in range(self.flame.n_points):
self.set_gas_state(i)
vals[...,i] = getattr(self.gas, attr)
return vals
if size is None:
extradoc = "\nReturns an array of length `n_points`."
else:
extradoc = "\nReturns an array of size `%s` x `n_points`." % size
doc = _trim(getattr(Solution, attr).__doc__) +'\n' + extradoc
return property(getter, doc=doc)
# Add scalar properties to FlameBase
for _attr in ['density', 'density_mass', 'density_mole', 'volume_mass',
'volume_mole', 'int_energy_mole', 'int_energy_mass', 'h',
'enthalpy_mole', 'enthalpy_mass', 's', 'entropy_mole',
'entropy_mass', 'g', 'gibbs_mole', 'gibbs_mass', 'cv',
'cv_mole', 'cv_mass', 'cp', 'cp_mole', 'cp_mass',
'isothermal_compressibility', 'thermal_expansion_coeff',
'viscosity', 'thermal_conductivity']:
setattr(FlameBase, _attr, _array_property(_attr))
FlameBase.volume = _array_property('v') # avoid confusion with velocity gradient 'V'
FlameBase.int_energy = _array_property('u') # avoid collision with velocity 'u'
# Add properties with values for each species
for _attr in ['X', 'Y', 'concentrations', 'partial_molar_enthalpies',
'partial_molar_entropies', 'partial_molar_int_energies',
'chemical_potentials', 'electrochemical_potentials', 'partial_molar_cp',
'partial_molar_volumes', 'standard_enthalpies_RT',
'standard_entropies_R', 'standard_int_energies_RT',
'standard_gibbs_RT', 'standard_cp_R', 'creation_rates',
'destruction_rates', 'net_production_rates', 'mix_diff_coeffs',
'mix_diff_coeffs_mass', 'mix_diff_coeffs_mole', 'thermal_diff_coeffs']:
setattr(FlameBase, _attr, _array_property(_attr, 'n_species'))
# Add properties with values for each reaction
for _attr in ['forward_rates_of_progress', 'reverse_rates_of_progress', 'net_rates_of_progress',
'equilibrium_constants', 'forward_rate_constants', 'reverse_rate_constants',
'delta_enthalpy', 'delta_gibbs', 'delta_entropy',
'delta_standard_enthalpy', 'delta_standard_gibbs',
'delta_standard_entropy']:
setattr(FlameBase, _attr, _array_property(_attr, 'n_reactions'))
class FreeFlame(FlameBase):
"""A freely-propagating flat flame."""
__slots__ = ('inlet', 'outlet', 'flame')
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``.
:param grid:
A list of points to be used as the initial grid. Not recommended
unless solving only on a fixed grid; Use the `width` parameter
instead.
:param width:
Defines a grid on the interval [0, width] with internal points
determined automatically by the solver.
"""
self.inlet = Inlet1D(name='reactants', phase=gas)
self.outlet = Outlet1D(name='products', phase=gas)
self.flame = FreeFlow(gas, name='flame')
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
super(FreeFlame, self).__init__((self.inlet, self.flame, self.outlet),
gas, grid)
# Setting X needs to be deferred until linked to the flow domain
self.inlet.T = gas.T
self.inlet.X = gas.X
def set_initial_guess(self):
"""
Set the initial guess for the solution. The adiabatic flame
temperature and equilibrium composition are computed for the inlet gas
composition. The temperature profile rises linearly over 20% of the
domain width to Tad, then is flat. The mass fraction profiles are set
similarly.
"""
super(FreeFlame, self).set_initial_guess()
self.gas.TPY = self.inlet.T, self.P, self.inlet.Y
if not self.inlet.mdot:
# nonzero initial guess increases likelihood of convergence
self.inlet.mdot = 1.0 * self.gas.density
Y0 = self.inlet.Y
u0 = self.inlet.mdot/self.gas.density
T0 = self.inlet.T
# get adiabatic flame temperature and composition
self.gas.equilibrate('HP')
Teq = self.gas.T
Yeq = self.gas.Y
u1 = self.inlet.mdot/self.gas.density
locs = [0.0, 0.3, 0.5, 1.0]
self.set_profile('u', locs, [u0, u0, u1, u1])
self.set_profile('T', locs, [T0, T0, Teq, Teq])
# Pick the location of the fixed temperature point, using an existing
# point if a reasonable choice exists
T = self.T
Tmid = 0.75 * T0 + 0.25 * Teq
i = np.flatnonzero(T < Tmid)[-1] # last point less than Tmid
if Tmid - T[i] < 0.2 * (Tmid - T0):
self.set_fixed_temperature(T[i])
elif T[i+1] - Tmid < 0.2 * (Teq - Tmid):
self.set_fixed_temperature(T[i+1])
else:
self.set_fixed_temperature(Tmid)
for n in range(self.gas.n_species):
self.set_profile(self.gas.species_name(n),
locs, [Y0[n], Y0[n], Yeq[n], Yeq[n]])
class BurnerFlame(FlameBase):
"""A burner-stabilized flat flame."""
__slots__ = ('burner', 'flame', 'outlet')
def __init__(self, gas, grid=None, width=None):
"""
:param gas:
`Solution` (using the IdealGas thermodynamic model) used to
evaluate all gas properties and reaction rates.
:param grid:
A list of points to be used as the initial grid. Not recommended
unless solving only on a fixed grid; Use the `width` parameter
instead.
:param width:
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``.
"""
self.burner = Inlet1D(name='burner', phase=gas)
self.outlet = Outlet1D(name='outlet', phase=gas)
self.flame = AxisymmetricStagnationFlow(gas, name='flame')
if width is not None:
grid = np.array([0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0]) * width
super(BurnerFlame, self).__init__((self.burner, self.flame, self.outlet),
gas, grid)
# Setting X needs to be deferred until linked to the flow domain
self.burner.T = gas.T
self.burner.X = gas.X
def set_initial_guess(self):
"""
Set the initial guess for the solution. The adiabatic flame
temperature and equilibrium composition are computed for the burner
gas composition. The temperature profile rises linearly in the first
20% of the flame to Tad, then is flat. The mass fraction profiles are
set similarly.
"""
super(BurnerFlame, self).set_initial_guess()
self.gas.TPY = self.burner.T, self.P, self.burner.Y
Y0 = self.burner.Y
u0 = self.burner.mdot/self.gas.density
T0 = self.burner.T
# get adiabatic flame temperature and composition
self.gas.equilibrate('HP')
Teq = self.gas.T
Yeq = self.gas.Y
u1 = self.burner.mdot/self.gas.density
locs = [0.0, 0.2, 1.0]
self.set_profile('u', locs, [u0, u1, u1])
self.set_profile('T', locs, [T0, Teq, Teq])
for n in range(self.gas.n_species):
self.set_profile(self.gas.species_name(n),
locs, [Y0[n], Yeq[n], Yeq[n]])
class CounterflowDiffusionFlame(FlameBase):
""" A counterflow diffusion flame """
__slots__ = ('fuel_inlet', 'flame', 'oxidizer_inlet')
def __init__(self, gas, grid=None, width=None):
"""
:param gas:
`Solution` (using the IdealGas thermodynamic model) used to
evaluate all gas properties and reaction rates.
:param grid:
A list of points to be used as the initial grid. Not recommended
unless solving only on a fixed grid; Use the `width` parameter
instead.
:param width:
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``.
"""
self.fuel_inlet = Inlet1D(name='fuel_inlet', phase=gas)
self.fuel_inlet.T = gas.T
self.oxidizer_inlet = Inlet1D(name='oxidizer_inlet', phase=gas)
self.oxidizer_inlet.T = gas.T
self.flame = AxisymmetricStagnationFlow(gas, name='flame')
if width is not None:
grid = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) * width
super(CounterflowDiffusionFlame, self).__init__(
(self.fuel_inlet, self.flame, self.oxidizer_inlet), gas, grid)
def set_initial_guess(self, fuel=None, oxidizer=None, stoich=None):
"""
Set the initial guess for the solution. The initial guess is generated
by assuming infinitely-fast chemistry.
"""
if fuel is not None or oxidizer is not None or stoich is not None:
warnings.warn(
'Arguments to CounterflowDiffusionFlame.set_initial_guess are '
'unused and deprecated and will be removed after Cantera 2.3.')
super(CounterflowDiffusionFlame, self).set_initial_guess()
moles = lambda el: (self.gas.elemental_mass_fraction(el) /
self.gas.atomic_weight(el))
# Compute stoichiometric mixture composition
Yin_f = self.fuel_inlet.Y
self.gas.TPY = self.fuel_inlet.T, self.P, Yin_f
mdotf = self.fuel_inlet.mdot
u0f = mdotf / self.gas.density
T0f = self.fuel_inlet.T
sFuel = moles('O')
if 'C' in self.gas.element_names:
sFuel -= 2 * moles('C')
if 'H' in self.gas.element_names:
sFuel -= 0.5 * moles('H')
Yin_o = self.oxidizer_inlet.Y
self.gas.TPY = self.oxidizer_inlet.T, self.P, Yin_o
mdoto = self.oxidizer_inlet.mdot
u0o = mdoto / self.gas.density
T0o = self.oxidizer_inlet.T
sOx = moles('O')
if 'C' in self.gas.element_names:
sOx -= 2 * moles('C')
if 'H' in self.gas.element_names:
sOx -= 0.5 * moles('H')
zst = 1.0 / (1 - sFuel / sOx)
Yst = zst * Yin_f + (1.0 - zst) * Yin_o
# get adiabatic flame temperature and composition
Tbar = 0.5 * (T0f + T0o)
self.gas.TPY = Tbar, self.P, Yst
self.gas.equilibrate('HP')
Teq = self.gas.T
Yeq = self.gas.Y
# estimate strain rate
zz = self.flame.grid
dz = zz[-1] - zz[0]
a = (u0o + u0f)/dz
kOx = (self.gas.species_index('O2') if 'O2' in self.gas.species_names else
self.gas.species_index('o2'))
f = np.sqrt(a / (2.0 * self.gas.mix_diff_coeffs[kOx]))
x0 = np.sqrt(mdotf*u0f) * dz / (np.sqrt(mdotf*u0f) + np.sqrt(mdoto*u0o))
nz = len(zz)
Y = np.zeros((nz, self.gas.n_species))
T = np.zeros(nz)
for j in range(nz):
x = zz[j] - zz[0]
zeta = f * (x - x0)
zmix = 0.5 * (1.0 - erf(zeta))
if zmix > zst:
Y[j] = Yeq + (Yin_f - Yeq) * (zmix - zst) / (1.0 - zst)
T[j] = Teq + (T0f - Teq) * (zmix - zst) / (1.0 - zst)
else:
Y[j] = Yin_o + zmix * (Yeq - Yin_o) / zst
T[j] = T0o + (Teq - T0o) * zmix / zst
T[0] = T0f
T[-1] = T0o
zrel = (zz - zz[0])/dz
self.set_profile('u', [0.0, 1.0], [u0f, -u0o])
self.set_profile('V', [0.0, x0/dz, 1.0], [0.0, a, 0.0])
self.set_profile('T', zrel, T)
for k,spec in enumerate(self.gas.species_names):
self.set_profile(spec, zrel, Y[:,k])
def extinct(self):
return max(self.T) - max(self.fuel_inlet.T, self.oxidizer_inlet.T) < 10
def solve(self, loglevel=1, refine_grid=True, auto=False):
"""
Solve the problem.
:param loglevel:
integer flag controlling the amount of diagnostic output. Zero
suppresses all output, and 5 produces very verbose output.
:param refine_grid:
if True, enable grid refinement.
:param auto: if True, sequentially execute the different solution stages
and attempt to automatically recover from errors. Attempts to first
solve on the initial grid with energy enabled. If that does not
succeed, a fixed-temperature solution will be tried followed by
enabling the energy equation, and then with grid refinement enabled.
If non-default tolerances have been specified or multicomponent
transport is enabled, an additional solution using these options
will be calculated.
"""
super(CounterflowDiffusionFlame, self).solve(loglevel, refine_grid, auto)
# Do some checks if loglevel is set
if loglevel > 0:
if self.extinct():
print('WARNING: Flame is extinct.')
# Check if the flame is very thick
# crude width estimate based on temperature
z_flame = self.grid[self.T > np.max(self.T) / 2]
flame_width = z_flame[-1] - z_flame[0]
domain_width = self.grid[-1] - self.grid[0]
if flame_width / domain_width > 0.4:
print('WARNING: The flame is thick compared to the domain '
'size. The flame might be affected by the plug-flow '
'boundary conditions. Consider increasing the inlet mass '
'fluxes or using a larger domain.')
# Check if the temperature peak is close to a boundary
z_center = (self.grid[np.argmax(self.T)] - self.grid[0]) / domain_width
if z_center < 0.25:
print('WARNING: The flame temperature peak is close to the '
'fuel inlet. Consider increasing the ratio of the '
'fuel inlet mass flux to the oxidizer inlet mass flux.')
if z_center > 0.75:
print('WARNING: The flame temperature peak is close to the '
'oxidizer inlet. Consider increasing the ratio of the '
'oxidizer inlet mass flux to the fuel inlet mass flux.')
def strain_rate(self, definition, fuel=None, oxidizer='O2', stoich=None):
r"""
Return the axial strain rate of the counterflow diffusion flame in 1/s.
:param definition:
The definition of the strain rate to be calculated. Options are:
``mean``, ``max``, ``stoichiometric``, ``potential_flow_fuel``, and
``potential_flow_oxidizer``.
:param fuel: The fuel species. Used only if *definition* is
``stoichiometric``.
:param oxidizer: The oxidizer species, default ``O2``. Used only if
*definition* is ``stoichiometric``.
:param stoich: The molar stoichiometric oxidizer-to-fuel ratio.
Can be omitted if the oxidizer is ``O2``. Used only if *definition*
is ``stoichiometric``.
The parameter *definition* sets the method to compute the strain rate.
Possible options are:
``mean``:
The mean axial velocity gradient in the entire domain
.. math:: a_{mean} = \left| \frac{\Delta u}{\Delta z} \right|
``max``:
The maximum axial velocity gradient
.. math:: a_{max} = \max \left( \left| \frac{du}{dz} \right| \right)
``stoichiometric``:
The axial velocity gradient at the stoichiometric surface.
.. math::
a_{stoichiometric} = \left| \left. \frac{du}{dz}
\right|_{\phi=1} \right|
This method uses the additional keyword arguments *fuel*,
*oxidizer*, and *stoich*.
>>> f.strain_rate('stoichiometric', fuel='H2', oxidizer='O2',
stoich=0.5)
``potential_flow_fuel``:
The corresponding axial strain rate for a potential flow boundary
condition at the fuel inlet.
.. math:: a_{f} = \sqrt{-\frac{\Lambda}{\rho_{f}}}
``potential_flow_oxidizer``:
The corresponding axial strain rate for a potential flow boundary
condition at the oxidizer inlet.
.. math:: a_{o} = \sqrt{-\frac{\Lambda}{\rho_{o}}}
"""
if definition == 'mean':
return - (self.u[-1] - self.u[0]) / self.grid[-1]
elif definition == 'max':
return np.max(np.abs(np.gradient(self.u) / np.gradient(self.grid)))
elif definition == 'stoichiometric':
if fuel is None:
raise KeyError('Required argument "fuel" not defined')
if oxidizer != 'O2' and stoich is None:
raise KeyError('Required argument "stoich" not defined')
if stoich is None:
# oxidizer is O2
stoich = - 0.5 * self.gas.n_atoms(fuel, 'O')
if 'H' in self.gas.element_names:
stoich += 0.25 * self.gas.n_atoms(fuel, 'H')
if 'C' in self.gas.element_names:
stoich += self.gas.n_atoms(fuel, 'C')
d_u_d_z = np.gradient(self.u) / np.gradient(self.grid)
phi = (self.X[self.gas.species_index(fuel)] * stoich /
np.maximum(self.X[self.gas.species_index(oxidizer)], 1e-20))
z_stoich = np.interp(-1., -phi, self.grid)
return np.abs(np.interp(z_stoich, self.grid, d_u_d_z))
elif definition == 'potential_flow_fuel':
return np.sqrt(- self.L[0] / self.density[0])
elif definition == 'potential_flow_oxidizer':
return np.sqrt(- self.L[0] / self.density[-1])
else:
raise ValueError('Definition "' + definition + '" is not available')
def mixture_fraction(self, m):
r"""
Compute the mixture fraction based on element *m*
The mixture fraction is computed from the elemental mass fraction of
element *m*, normalized by its values on the fuel and oxidizer
inlets:
.. math:: Z = \frac{Z_{\mathrm{mass},m}(z) -
Z_{\mathrm{mass},m}(z_\mathrm{oxidizer})}
{Z_{\mathrm{mass},m}(z_\mathrm{fuel}) -
Z_{\mathrm{mass},m}(z_\mathrm{oxidizer})}
:param m:
The element based on which the mixture fraction is computed,
may be specified by name or by index
>>> f.mixture_fraction('H')
"""
emf = self.elemental_mass_fraction(m)
return (emf - emf[-1]) / (emf[0] - emf[-1])
class ImpingingJet(FlameBase):
"""An axisymmetric flow impinging on a surface at normal incidence."""
__slots__ = ('inlet', 'flame', 'surface')
def __init__(self, gas, grid=None, width=None, surface=None):
"""
:param gas:
`Solution` (using the IdealGas thermodynamic model) used to
evaluate all gas properties and reaction rates.
:param grid:
A list of points to be used as the initial grid. Not recommended
unless solving only on the initial grid; Use the `width` parameter
instead.
:param width:
Defines a grid on the interval [0, width] with internal points
determined automatically by the solver.
: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``.
"""
self.inlet = Inlet1D(name='inlet', phase=gas)
self.flame = AxisymmetricStagnationFlow(gas, name='flame')
if width is not None:
grid = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) * width
if surface is None:
self.surface = Surface1D(name='surface', phase=gas)
self.surface.T = gas.T
else:
self.surface = ReactingSurface1D(name='surface', phase=gas)
self.surface.set_kinetics(surface)
self.surface.T = surface.T
super(ImpingingJet, self).__init__(
(self.inlet, self.flame, self.surface), gas, grid)
# Setting X needs to be deferred until linked to the flow domain
self.inlet.T = gas.T
self.inlet.X = gas.X
def set_initial_guess(self, products='inlet'):
"""
Set the initial guess for the solution. If products = 'equil', then
the equilibrium composition at the adiabatic flame temperature will be
used to form the initial guess. Otherwise the inlet composition will
be used.
"""
super(ImpingingJet, self).set_initial_guess(products=products)
Y0 = self.inlet.Y
T0 = self.inlet.T
self.gas.TPY = T0, self.flame.P, Y0
u0 = self.inlet.mdot / self.gas.density
if products == 'equil':
self.gas.equilibrate('HP')
Teq = self.gas.T
Yeq = self.gas.Y
locs = np.array([0.0, 0.3, 0.7, 1.0])
self.set_profile('T', locs, [T0, Teq, Teq, self.surface.T])
for k in range(self.gas.n_species):
self.set_profile(self.gas.species_name(k), locs,
[Y0[k], Yeq[k], Yeq[k], Yeq[k]])
else:
locs = np.array([0.0, 1.0])
self.set_profile('T', locs, [T0, self.surface.T])
for k in range(self.gas.n_species):
self.set_profile(self.gas.species_name(k), locs,
[Y0[k], Y0[k]])
locs = np.array([0.0, 1.0])
self.set_profile('u', locs, [u0, 0.0])
self.set_profile('V', locs, [0.0, 0.0])
class CounterflowPremixedFlame(FlameBase):
""" A premixed counterflow flame """
__slots__ = ('reactants', 'flame', 'products')
def __init__(self, gas, grid=None, width=None):
"""
:param gas:
`Solution` (using the IdealGas thermodynamic model) used to
evaluate all gas properties and reaction rates.
:param grid:
Array of initial grid points. Not recommended unless solving only on
a fixed grid; Use the `width` parameter instead.
:param width:
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``.
"""
self.reactants = Inlet1D(name='reactants', phase=gas)
self.reactants.T = gas.T
self.products = Inlet1D(name='products', phase=gas)
self.products.T = gas.T
self.flame = AxisymmetricStagnationFlow(gas, name='flame')
if width is not None:
# Create grid points aligned with initial guess profile
grid = np.array([0.0, 0.3, 0.5, 0.7, 1.0]) * width
super(CounterflowPremixedFlame, self).__init__(
(self.reactants, self.flame, self.products), gas, grid)
# Setting X needs to be deferred until linked to the flow domain
self.reactants.X = gas.X
def set_initial_guess(self, equilibrate=True):
"""
Set the initial guess for the solution.
If `equilibrate` is True, then the products composition and temperature
will be set to the equilibrium state of the reactants mixture.
"""
super(CounterflowPremixedFlame, self).set_initial_guess()
Yu = self.reactants.Y
Tu = self.reactants.T
self.gas.TPY = Tu, self.flame.P, Yu
rhou = self.gas.density
uu = self.reactants.mdot / rhou
self.gas.equilibrate('HP')
Teq = self.gas.T
Yeq = self.gas.Y
if equilibrate:
Tb = Teq
Yb = Yeq
self.products.Y = Yb
self.products.T = Tb
else:
Tb = self.products.T
Yb = self.products.Y
self.gas.TPY = Tb, self.flame.P, Yb
rhob = self.gas.density
ub = self.products.mdot / rhob
locs = np.array([0.0, 0.4, 0.6, 1.0])
self.set_profile('T', locs, [Tu, Tu, Teq, Tb])
for k in range(self.gas.n_species):
self.set_profile(self.gas.species_name(k), locs,
[Yu[k], Yu[k], Yeq[k], Yb[k]])
# estimate strain rate
self.gas.TPY = Teq, self.flame.P, Yeq
zz = self.flame.grid
dz = zz[-1] - zz[0]
a = (uu + ub)/dz
# estimate stagnation point
x0 = rhou*uu * dz / (rhou*uu + rhob*ub)
self.set_profile('u', [0.0, 1.0], [uu, -ub])
self.set_profile('V', [0.0, x0/dz, 1.0], [0.0, a, 0.0])
class CounterflowTwinPremixedFlame(FlameBase):
"""
A twin premixed counterflow flame. Two opposed jets of the same composition
shooting into each other.
"""
__slots__ = ('reactants', 'flame', 'products')
def __init__(self, gas, grid=None, width=None):
"""
:param gas:
`Solution` (using the IdealGas thermodynamic model) used to
evaluate all gas properties and reaction rates.
:param grid:
Array of initial grid points. Not recommended unless solving only on
a fixed grid; Use the `width` parameter instead.
:param width:
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``.
"""
self.reactants = Inlet1D(name='reactants', phase=gas)
self.reactants.T = gas.T
self.flame = AxisymmetricStagnationFlow(gas, name='flame')
#The right boundary is a symmetry plane
self.products = SymmetryPlane1D(name='products', phase=gas)
if width is not None:
# Create grid points aligned with initial guess profile
grid = np.array([0.0, 0.2, 0.4, 0.5, 0.6, 0.8, 1.0]) * width
super(CounterflowTwinPremixedFlame, self).__init__(
(self.reactants, self.flame, self.products), gas, grid)
# Setting X needs to be deferred until linked to the flow domain
self.reactants.X = gas.X
def set_initial_guess(self):
"""
Set the initial guess for the solution.
"""
super(CounterflowTwinPremixedFlame, self).set_initial_guess()
Yu = self.reactants.Y
Tu = self.reactants.T
self.gas.TPY = Tu, self.flame.P, Yu
uu = self.reactants.mdot / self.gas.density
self.gas.equilibrate('HP')
Tb = self.gas.T
Yb = self.gas.Y
locs = np.array([0.0, 0.4, 0.6, 1.0])
self.set_profile('T', locs, [Tu, Tu, Tb, Tb])
for k in range(self.gas.n_species):
self.set_profile(self.gas.species_name(k), locs,
[Yu[k], Yu[k], Yb[k], Yb[k]])
# estimate strain rate
zz = self.flame.grid
dz = zz[-1] - zz[0]
a = 2 * uu / dz
self.set_profile('u', [0.0, 1.0], [uu, 0])
self.set_profile('V', [0.0, 1.0], [0.0, a])