This example is available as an ipynb (Jupyter Notebook) file in the main GitHub repository at https://github.com/pr-omethe-us/PyKED/blob/master/docs/rcm-example.ipynb
The ChemKED file that will be used in this example can be found in the
tests directory of the PyKED
repository at https://github.com/pr-omethe-us/PyKED/blob/master/pyked/tests/testfile_rcm.yaml.
Examining that file, we find the first section specifies the information about
the ChemKED file itself:
file-authors:
- name: Kyle E Niemeyer
ORCID: 0000-0003-4425-7097
file-version: 0
chemked-version: 0.4.0
Then, we find the information regarding the article in the literature from which this data was taken. In this case, the dataset comes from the work of Mittal et al.:
reference:
doi: 10.1002/kin.20180
authors:
- name: Gaurav Mittal
- name: Chih-Jen Sung
ORCID: 0000-0003-2046-8076
- name: Richard A Yetter
journal: International Journal of Chemical Kinetics
year: 2006
volume: 38
pages: 516-529
detail: Fig. 6, open circle
experiment-type: ignition delay
apparatus:
kind: rapid compression machine
institution: Case Western Reserve University
facility: CWRU RCM
Finally, this file contains just a single datapoint, which describes the experimental ignition delay, initial mixture composition, initial temperature, initial pressure, compression time, ignition type, and volume history that specifies how the volume of the reactor varies with time, for simulating the compression stroke and post-compression processes:
datapoints:
- temperature:
- 297.4 kelvin
ignition-delay:
- 1.0 ms
pressure:
- 958.0 torr
composition:
kind: mole fraction
species:
- species-name: H2
InChI: 1S/H2/h1H
amount:
- 0.12500
- species-name: O2
InChI: 1S/O2/c1-2
amount:
- 0.06250
- species-name: N2
InChI: 1S/N2/c1-2
amount:
- 0.18125
- species-name: Ar
InChI: 1S/Ar
amount:
- 0.63125
ignition-type:
target: pressure
type: d/dt max
rcm-data:
compression-time:
- 38.0 ms
time-histories:
- type: volume
time:
units: s
column: 0
volume:
units: cm3
column: 1
values:
- [0.00E+000, 5.47669375000E+002]
- [1.00E-003, 5.46608789894E+002]
- [2.00E-003, 5.43427034574E+002]
...
The values for the volume history in the time-histories key are truncated here to save space. One application of the
data stored in this file is to perform a simulation using Cantera to
calculate the ignition delay, including the facility-dependent effects represented in the volume
trace. All information required to perform this simulation is present in the ChemKED file, with the
exception of a chemical kinetic model for H2/CO combustion.
In Python, additional functionality can be imported into a script or session by the import
keyword. Cantera, NumPy, and PyKED must be imported into the session so that we can work with the
code. In the case of Cantera and NumPy, we will use many functions from these libraries, so we
assign them abbreviations (ct and np, respectively) for convenience. From PyKED, we
will only be using the ChemKED class, so this is all that is imported:
In [1]:
import cantera as ct
import numpy as np
from pyked import ChemKED
Next, we have to load the ChemKED file and retrieve the first element of the datapoints
list. Although this file only encodes a single experiment, the datapoints attribute will
always be a list (in this case, of length 1). The elements of the
datapoints list are instances of the DataPoint class, which we store in the variable
dp. To load the YAML file from the web, we also import and use the PyYAML package, and the built-in urllib package, and use the dict_input argument to ChemKED to read the information.
In [2]:
from urllib.request import urlopen
import yaml
rcm_link = 'https://raw.githubusercontent.com/pr-omethe-us/PyKED/master/pyked/tests/testfile_rcm.yaml'
with urlopen(rcm_link) as response:
testfile_rcm = yaml.safe_load(response.read())
ck = ChemKED(dict_input=testfile_rcm)
dp = ck.datapoints[0]
The initial temperature, pressure, and mixture composition can be read from the
instance of the DataPoint class. PyKED uses instances of the Pint Quantity class to
store values with units, while Cantera expects a floating-point value in SI
units as input. Therefore, we use the built-in capabilities of Pint to convert
the units from those specified in the ChemKED file to SI units, and we use the magnitude
attribute of the Quantity class to take only the numerical part. We also retrieve the
initial mixture mole fractions in a format Cantera will understand:
In [3]:
T_initial = dp.temperature.to('K').magnitude
P_initial = dp.pressure.to('Pa').magnitude
X_initial = dp.get_cantera_mole_fraction()
With these properties defined, we have to create the objects in Cantera that represent the physical
state of the system to be studied. In Cantera, the Solution class stores the thermodynamic,
kinetic, and transport data from an input file in the CTI format. After the Solution object
is created, we can set the initial temperature, pressure, and mole fractions using the TPX
attribute of the Solution class. In this example, we will use the GRI-3.0 as the chemical kinetic mechanism for H2/CO combustion. GRI-3.0 is built-in to Cantera, so no other input files are needed.
In [4]:
gas = ct.Solution('gri30.xml')
gas.TPX = T_initial, P_initial, X_initial
With the thermodynamic and kinetic data loaded and the initial conditions defined, we need to
install the Solution instance into an IdealGasReactor which implements the equations
for mass, energy, and species conservation. In addition, we create a Reservoir to represent
the environment external to the reaction chamber. The input file used for the environment,
air.xml, is also included with Cantera and represents an average composition of air.
In [5]:
reac = ct.IdealGasReactor(gas)
env = ct.Reservoir(ct.Solution('air.xml'))
To apply the effect of the volume trace to the IdealGasReactor, a Wall must be
installed between the reactor and environment and assigned a velocity. The Wall allows the
environment to do work on the reactor (or vice versa) and change the reactor's thermodynamic state;
we use a Reservoir for the environment because in Cantera, Reservoirs always have a
constant thermodynamic state and composition. Using a Reservoir accelerates the solution
compared to using two IdealGasReactors, since the composition and state of the environment
are typically not necessary for the solution of autoignition problems. Although we do not show the
details here, a reference implementation of a class that computes the wall velocity given the volume
history of the reactor is available in CanSen, in the
cansen.profiles.VolumeProfile class, which we import here:
In [6]:
from cansen.profiles import VolumeProfile
exp_time = dp.volume_history.time.magnitude
exp_volume = dp.volume_history.volume.magnitude
keywords = {'vproTime': exp_time, 'vproVol': exp_volume}
ct.Wall(reac, env, velocity=VolumeProfile(keywords));
Then, the IdealGasReactor is installed in a ReactorNet. The ReactorNet
implements the connection to the numerical solver (CVODES is
used in Cantera) to solve the energy and species equations. For this example, it is best practice
to set the maximum time step allowed in the solution to be the minimum time difference in the time array from the volume trace:
In [7]:
netw = ct.ReactorNet([reac])
netw.set_max_time_step(np.min(np.diff(exp_time)))
To calculate the ignition delay, we will follow the definition specified in the ChemKED file for this experiment, where the experimentalists used the maximum of the time derivative of the pressure to define the ignition delay. To calculate this derivative, we need to store the state variables and the composition on each time step, so we initialize several Python lists to act as storage:
In [8]:
time = []
temperature = []
pressure = []
volume = []
mass_fractions = []
Finally, the problem is integrated using the step method of the ReactorNet. The
step method takes one timestep forward on each call, with step size determined by the CVODES
solver (CVODES uses an adaptive time-stepping algorithm). On each step, we add the relevant variables
to their respective lists. The problem is integrated until a user-specified end time, in this case
50 ms, although in principle, the user could end the simulation on any condition
they choose:
In [9]:
while netw.time < 0.05:
time.append(netw.time)
temperature.append(reac.T)
pressure.append(reac.thermo.P)
volume.append(reac.volume)
mass_fractions.append(reac.Y)
netw.step()
At this point, the user would post-process the information in the pressure list to calculate the derivative by whatever algorithm they choose. We will plot the pressure versus the time of the simulation using the Matplotlib library:
In [10]:
%matplotlib notebook
import matplotlib.pyplot as plt
plt.figure()
plt.plot(time, pressure)
plt.ylabel('Pressure [Pa]')
plt.xlabel('Time [s]');
We can also plot the volume trace and compare to the values derived from the ChemKED file.
In [11]:
plt.figure()
plt.plot(exp_time, exp_volume/exp_volume[0], label='Experimental volume', linestyle='--')
plt.plot(time, volume, label='Simulated volume')
plt.legend(loc='best')
plt.ylabel('Volume [m^3]')
plt.xlabel('Time [s]');