cantera/Cantera/python/Cantera/Flow.py
2003-04-24 08:55:11 +00:00

720 lines
22 KiB
Python
Executable file

"""
One-dimensional reacting flows.
"""
from Cantera import getCanteraError
from FlowPlotter import FlowPlotter
from exceptions import *
import refine
import sys, types, copy, tempfile
import _cantera
import interp
import math
from Numeric import array, zeros, ones, transpose, size, sort, shape, asarray
_flows = {'Stagnation':0, 'Stag':0,
'OneDimensional':1, '1D':1, 'OneD':1, 'OneDim':1,
'Free':2}
_geom = {'Axisymmetric':0, 'Axi':0, 'Planar':1}
class Flow1D:
""" One-dimensional reacting flows.
Class Flow1D models several types of steady 'one dimensional'
reacting flows. The flows are one-dimensional in the sense that
the governing equations for the steady-state solution can be cast
in the form of a set of ordinary differential equations in one
axial coordinate (z). For the case of stagnation flows, this
results from a similarity transformation that reduces the
physically two-dimensional problem to one that is mathematically
one-dimensional.
The types of flows that may be simulated are:
- One-dimensional reacting flows, such as burner-stabilized
premixed flames;
- Axisymmetric stagnation-point flows
- Planar stagnation-point flows
"""
# Allowed option keywords are defined here so that only these
# keywords may be added to the _opt dictionary.
_timeint_options = ['ftime', 'min_timestep', 'max_timestep',
'nsteps', 'timestep']
_newton_options = ['max_jac_age', 'rtol', 'atol']
_output_options = ['loglevel', 'plotfile']
_options = _newton_options + _timeint_options + _output_options
def __init__(self,
flow_type = 'Stagnation',
flow_geom = 'Axisymmetric',
gas = None,
grid = None,
pressure = 1.01325e5):
"""Flow1D Constructor.
"""
self.__flow_id = -1
self.domainType = 0
self.loglevel = 1
self.initial = {}
if not grid:
raise CanteraError('Grid not specified!')
if not gas:
raise CanteraError('Gas mixture object not specified!')
self.gas = gas
self.nsp = self.gas.nSpecies()
self.nv = self.nsp + 4
if _flows.has_key(flow_type):
self.type = _flows[flow_type]
else:
raise CanteraError('unsupported flow type: '+flow_type)
if self.type == 0:
if flow_geom == 'Planar': self.type = 3
# Create the kernel object. This is an instance of a subclass of
# C++ Cantera class 'StFlow'.
self.__flow_id = _cantera.Flow(self.type, self.gas.phase_id(), len(grid))
# Set the grid. Must be done _after_ creating the kernel
# object, since it sets the grid there too.
self.setGrid(asarray(grid))
# Set the pressure. Note that the pressure is constant
# throughout the flowfield, due to the assumption of low Mach
# number.
self.setPressure(pressure)
# set the thermo, kinetics, and transport managers to those of
# object self.gas.
self.setThermo(self.gas)
self.setKinetics(self.gas)
self.setTransport(self.gas)
# create a NumPy array to hold the solution. Since NumPy
# arrays are stored by row, while Cantera expects arrays
# stored by column, this array is defined with the grid point
# number as the first index. In this way, all solution
# variables for one grid point will be stored in contiguous
# locations.
self.x = zeros((self.npts, self.nsp + 4), 'd')
self.xnew = zeros((self.npts, self.nsp + 4), 'd')
# Default error tolerances
self.setTolerances(
u = (1.e-8, 1.e-15),
V = (1.e-8, 1.e-15),
T = (1.e-8, 1.e-15),
L = (1.e-8, 1.e-15),
Y = (1.e-8, 1.e-15))
self.energy = 0
self.names = ['u','V','T','L']+ list(self.gas.speciesNames())
# finish setting default parameter values
self.restoreDefaults()
self.time = 0.0
# For refiner, only refine the grid based on species mass
# fraction and velocity profiles until the energy equation is
# enabled.
self.refine_components = range(4,4+self.nsp)
if self.type == 0: self.refine_components.append([0,1])
self.refiner = refine.Refiner(components = self.refine_components,
names = self.names)
# Create an object to handle plotting results.
self.plotter = FlowPlotter(self)
#================> end of method '__init__' <===================
def __del__(self):
"""Delete the Flow1D instance."""
if self.__flow_id >= 0:
_cantera.flow_delete(self.__flow_id)
def shape(self):
"""Return (rows, columns) of solution matrix."""
return (len(self.z), self.gas.nSpecies() + 4)
def nPoints(self):
"""Number of grid points."""
return self.npts
def flow_id(self):
"""ID used to accesss the kernel object."""
return self.__flow_id
def option(self, key):
"""Return the value of an option."""
return self._opt[key]
def setGrid(self, z):
"""Set the grid to the values in sequence z.
The values will be sorted, and so the input sequence
does not need to be monotonic.
"""
self.z = array(z)
sort(self.z)
self.npts = len(z)
return _cantera.flow_setupgrid(self.flow_id(), z)
def setThermo(self, th):
"""Set the thermodynamic property manager."""
id = _cantera.flow_setthermo(self.flow_id(), th.phase_id())
return id
def setKinetics(self, kin):
"""Set the kinetics manager."""
id = _cantera.flow_setkinetics(self.__flow_id, kin.ckin)
return id
def setTransport(self, tr, soret=0):
"""Set the transport manager."""
id = _cantera.flow_settransport(self.__flow_id,
tr.transport_id(), soret)
return id
def setPressure(self, p):
"""Set the pressure [Pa].
Since the flow has very nearly the same pressure everywhere,
this pressure value is used in all computations involving the
equation of state.
"""
self.p = p
_cantera.flow_setpressure(self.__flow_id, p)
def holdTemperature(self, points, t0):
"""Hold the temperature at grid points 'points' to t0,
and disable the energy equation.
"""
self.x[points,2] = t0
_cantera.flow_settemperature(self.__flow_id, points, t0)
self.setEnergyEqn('off')
def holdMassFraction(self, j, k, y0):
self.x[j,4+k] = y0
_cantera.flow_setmassfraction(self.__flow_id, j, k, y0)
def setInitialProfiles(self, datatable=None):
"""Set initial velocity, temperature, and/or species profiles.
datatable -- Dictionary mapping variable names to sequences of
(position, value) pairs. The position is specified in
relative terms, as a number in the range [0,1], where the
value zero corresponds to the smallest grid value, and the
value one to the largest.
The keys of datatable must be 'u', 'V', 'T', or a species
name. Velocity and temperature values are entered in SI units,
and species values are entered in arbitrary molar units, and
will be normalized to produce mole fractions.
The profile will be linearly interpolated onto the grid from
the data provided. Each variable may be specified at different
locations.
Example:
data = {}
data['T'] = [(0, 500), (0.3, 2000), (0.8, 2200), (1, 1500)]
data['u'] = [(0, 0.0), (1, 2)]
data['H2'] = [(0, 0.2), (1, 0.3)]
data['O2'] = [(0, 0.8), (1, 0.7)]
flow.setInitialProfiles(data)
"""
if datatable:
self.datatable = datatable
else:
datatable = self.datatable
vars = datatable.keys()
x = zeros((self.npts, self.nsp),'d')
equil = 0
for var in vars:
data = datatable[var]
if not var == 'equil':
data.sort()
zz = []
v = []
for item in data:
zz.append(self.z[0] + item[0]*(self.z[-1] - self.z[0]))
v.append(item[1])
self.initial[var] = (zz, v)
if (var == 'u'):
for j in range(self.npts):
self.x[j,0] = interp.interp(self.z[j],zz,v)
elif (var == 'V'):
for j in range(self.npts):
self.x[j,1] = interp.interp(self.z[j],zz,v)
elif (var == 'T'):
for j in range(self.npts):
self.holdTemperature(j,interp.interp(self.z[j],zz,v))
else:
k = self.gas.speciesIndex(var)
if k < 0:
raise CanteraError('Unknown species name: '+var)
for j in range(self.npts):
x[j,k] = interp.interp(self.z[j],zz,v)
else:
equil = 1
if equil == 1:
xin = self.left.X
x[0,:] = xin
for j in range(1,self.npts):
self.gas.setState_TPX(self.x[j,2],self.p,xin)
try:
self.gas.equilibrate('TP')
except:
pass
x[j,:] = self.gas.moleFractions()
# convert input mole fractions to mass fractions,
# and set the mass fraction profiles
for j in range(self.npts):
self.gas.setMoleFractions(x[j,:])
y = self.gas.massFractions()
for k in range(self.nsp):
self.x[j, k+4] = y[k]
self.holdMassFraction(j,k,y[k])
self.enableSpecies()
def regrid(self, grid):
oldx = self.x
oldgrid = self.z
np, nv = shape(oldx)
v = range(nv)
self.setGrid(grid)
for j in range(self.npts):
for n in v:
self.x[j,n] = interp.interp(self.z[j], oldgrid, oldx)
def __repr__(self):
return self.show()
def show(self):
fname = tempfile.mktemp('.dat')
x = self.x
_cantera.flow_showsolution(self.__flow_id, fname, x)
f = open(fname,'r')
y = f.readlines()
print
for line in y:
print line,
print
f.close()
def showResid(self):
"""Print the current residual values"""
print transpose(self.resid())
def T(self,j):
"""Temperature at grid point j [K]."""
return self.x[j,2]
def u(self,j):
"""Axial velocity at grid point j [m/s]."""
return self.x[j, 0]
def V(self,j):
"""Radial velocity divided by radius at grid point j [1/s]."""
return self.x[j, 1]
def lamb(self,j):
"""(1/r)(dP/dr) at grid point j.
If the solution has converged, this will be the same at all
grid points.
"""
return self.x[j, 3]
def massFraction(self,sp,j):
"""Mass fraction of species 'sp', which may be referenced by
name or by index number."""
k = self.gas.speciesIndex(sp)
return self.x[j, k + 4]
def density(self, j):
"""Density [kg/m^3]."""
self.setGas(j)
return self.gas.density()
def molWt(self, j):
"""Mean molecular weight [kg/kmol]."""
self.setGas(j)
return self.gas.meanMolecularWeight()
def setGas(self, j):
"""Set the state of the internal gas mixture object to be
consistent with the solution at grid point j."""
self.gas.setTemperature(self.T(j))
y = self.x[j,4:]
self.gas.setMassFractions(y)
self.gas.setPressure(self.p)
def setTolerances(self, u = None, V = None, T = None,
L = None, Y = None):
"""Set error tolerances.
The inputs are tuples of (relative, absolute) tolerances for
each of u, V, T, L, and Y.
"""
default = (1.e-7, 1.e-15)
if u == None: u = default
if V == None: V = default
if T == None: T = default
if L == None: L = default
if Y == None: Y = default
rtol = zeros(self.nsp+4,'d')
atol = zeros(self.nsp+4,'d')
rtol[0] = u[0]
rtol[1] = V[0]
rtol[2] = T[0]
rtol[3] = L[0]
for k in range(self.nsp):
rtol[4+k] = Y[0]
atol[0] = u[1]
atol[1] = V[1]
atol[2] = T[1]
atol[3] = L[1]
for k in range(self.nsp):
atol[4+k] = Y[1]
_cantera.flow_settolerances(self.__flow_id, len(rtol), rtol,
len(atol), atol)
def disableSpecies(self):
off = zeros(self.nsp,'d')
_cantera.flow_solvespecies(self.__flow_id, self.nsp, off)
def enableSpecies(self):
o = ones(self.nsp,'d')
_cantera.flow_solvespecies(self.__flow_id, self.nsp, o)
def setEnergyEqn(self, o, loglevel = 0, pt = -1):
"""Enable or disable the energy equation."""
if o == 'on':
self.energy = 1
_cantera.flow_energy(self.__flow_id, pt, 1)
if loglevel > 0: print '\n%%%%%%%%%%%% Enabling energy equation %%%%%%%%%%%%\n'
elif o == 'off':
self.energy = 0
_cantera.flow_energy(self.__flow_id, pt, 0)
if loglevel > 0: print '\n%%%%%%%%%%%% Disabling energy equation %%%%%%%%%%%%\n'
def setEnergyFactor(self, e):
_cantera.flow_setenergyfactor(self.__flow_id, e)
## def resid(self, point = -1):
## """Return the residual vector.
## If 'point' is specified, the residual is only evaluated at
## this point and those adjacent to it. Otherwise, it is
## evaluated at all grid points.
## """
## r = zeros(shape(self.x),'d')
## if point >= 0 and point < self.npts:
## j = point
## else:
## j = -1
## _cantera.flow_eval(self.__flow_id, j, self.x, r)
## return r
def ssnorm(self):
"""Absolute maximum value of the steady-state residual for any
component at any point."""
a = self.container.ssnorm(self.x,self.xnew)
return a
def integrateChem(self, dt, loglevel=1):
"""Condition the species profiles by integrating the
constant-pressure kinetics rate equations
\[
\dot Y_k = \dot\omega_k M_k / \rho.
\]
at each grid point for time 'dt'.
"""
if loglevel > 0:
print '\nIntegrating the chemical ource terms for %10.4g s...' % dt,
_cantera.flow_integratechem(self.__flow_id, self.x, dt)
print _cantera.readlog()
def restoreDefaults(self):
"""Restore default options."""
self._opt = {}
self.setOptions(
max_jac_age = 5,
timestep = 1.e-6,
min_timestep = 1.e-12,
max_timestep = 0.1,
nsteps = 20,
ftime = 3.0,
plotfile = ""
)
def setOptions(self, **options):
"""Set options."""
for kw in options.keys():
if kw in Flow1D._options:
self._opt[kw] = options[kw]
else:
raise OptionError(kw)
def refine(self, loglevel = 2):
"""Refine the grid.
"""
r = self.refiner
r.components = range(4,self.nsp+4)
if self.energy:
r.components.append(2)
#dsave = r.delta
#while 1 > 0:
# try:
znew, zadded, xn, ok = r.refine(grid = self.z, solution = self.x)
nin = len(znew) - len(self.z)
# break
# except:
# r.delta = (0.9*r.delta[0], 0.9*r.delta[1])
#r.delta = dsave
if not ok:
self.setGrid(znew)
self.x = array(xn,'d')
self.xnew = zeros(shape(xn),'d')
# update the fixed temperature values if the energy
# equation is not being solved
if self.energy == 0:
for j in range(self.npts):
zz, tt = self.initial['T']
t = interp.interp(self.z[j],zz,tt)
self.holdTemperature(j,t)
else:
for j in range(self.npts):
self.holdTemperature(j,self.x[j,2])
self.setEnergyEqn('on')
if loglevel > 0:
print 'Refine: ',
print 'added',nin,'points.'
print 'Grid size = ',len(self.z)
return nin
def prune(self, loglevel = 2):
"""Prune the grid.
"""
r = self.refiner
r.components = range(4,self.nsp+4)
if self.energy:
r.components.append(2)
znew, xn = r.prune(grid = self.z, solution = self.x)
nout = len(self.z) - len(znew)
if nout > 0:
self.setGrid(znew)
self.x = array(xn,'d')
self.xnew = zeros(shape(xn),'d')
# update the fixed temperature values if the energy
# equation is not being solved
if self.energy == 0:
for j in range(self.npts):
zz, tt = self.initial['T']
t = interp.interp(self.z[j],zz,tt)
self.holdTemperature(j,t)
else:
for j in range(self.npts):
self.holdTemperature(j,self.x[j,2])
self.setEnergyEqn('on')
if loglevel > 0:
print 'Prune: ',
print 'removed',nout,'points.'
print 'Grid size = ',len(self.z)
return nout
def save(self, filename, id, desc="", append=0):
"""Save a solution to a file.
filename -- file name. If the file, does not exist, it will
be created. The save files are xml files, and the
filename should have the extension '.xml'. If it
does not, this extension will be appended to the
name.
id -- the ID tag of the solution. Multiple solutions may
be saved to the same file. Specifying a unique ID
tag allows this solution to selected later by
method 'restore'.
append -- If append > 0, the solution will be appended to the
file. Otherwise, the file will be overwritten if it
exists.
"""
appnd = append
fn = filename
extn = filename[-4:]
if extn <> '.xml' and extn <> '.XML':
fn = filename + '.xml'
#if self.loglevel > 0:
#print 'Solution saved to file',filename,'as solution',`id`
#_cantera.flow_save(self.__flow_id, fn, id, appnd, self.x)
_cantera.flow_save(self.__flow_id, fn, id, desc, self.x)
print _cantera.readlog()
np, nv = shape(self.x)
f = open('ctsoln.dat','w')
for j in range(np):
f.write('%14.6e %14.6e ' % (100.0*self.z[j], self.x[j,2]))
for k in range(4,nv):
f.write('%14.6e ' % (self.x[j,k],))
f.write('\n')
f.close()
def restore(self, filename, id):
"""Restore a previously-saved solution.
filename -- name of a file containing a solution previously
saved by a call to 'save'
id -- the ID tag of the solution
"""
#try:
(z, s) = _cantera.flow_restore(self.__flow_id, 0, filename, id)
#except:
# raise CanteraError()
self.setGrid(z)
self.x = array(s,'d')
self.xnew = array(self.x,'d')
for j in range(self.npts):
self.holdTemperature(j,self.x[j,2])
self.initial['T'] = (self.z, self.x[:,2])
self.setEnergyEqn('off')
if self.loglevel > 0:
print 'Solution ',`id`,'read from file',filename
print _cantera.readlog()
def outputTEC(self, plotfile="", title="", zone="c0", append=0):
"""Write the current solution to a file in TECPLOT format.
plotfile -- file name (required)
title -- plot title
zone -- zone name
append -- if append > 0, the output is appended to the file
"""
self.plotter.plot(fname = plotfile, title = title,
zone = zone, append=append)
def outputCSV(self, plotfile="", append=0):
"""Write the current solution to a file in CSV format.
plotfile -- file name (required)
append -- if append > 0, the output is appended to the file
"""
self.plotter.plot(fname = plotfile, fmt = 'EXCEL',
append=append)
def plot(self, i):
"""Plot solution component i. Requires the scipy package."""
from scipy import gplt
return gplt.plot(self.z, self.x[:,i])
def setBoundaries(self, left = None, right = None):
"""Install the boundary objects.
The type of boundary object determines the boundary conditions.
"""
nleft = 0
nright = 0
if left:
self.left = left
nleft = left.bdry_id()
if right:
self.right = right
nright = right.bdry_id()
_cantera.flow_setboundaries(self.__flow_id, nleft, nright)
"""
$Author$
$Revision$
$Date$
Copyright 2001 California Institute of Technology
"""