Examples

Some example input files are included in the data directory.

After installing pyJac, or in the package directory, you can see all of the usage options with a standard --help or -h option:

python -m pyjac --help

Jacobian file generation

To generate the Jacobian source files for a hydrogen-air system in C (without any cache optimization):

python -m pyjac --lang c --input data/h2o2.inp

CUDA source code can be generated similarly:

python -m pyjac --lang cuda --input data/h2o2.inp

Functional testing

Functional testing (i.e., testing whether pyJac gives the correct results) requires thermochemical state data. This can be generated using the built-in partially stirred reactor (PaSR) module:

python -m pyjac.functional_tester.partially_stirred_reactor \
--mech data/h2o2.cti --input data/pasr_input.yaml \
--output h2_pasr_output.npy

Then, functional testing using this data can be performed via:

python -m pyjac.functional_tester --mech data/h2o2.cti --lang c \
--pasr_output h2_pasr_output.npy

Alternatively, you can perform the test using provided example data:

python -m pyjac.functional_tester --mech data/h2o2.cti --lang c \
--pasr_output data/h2_pasr_output.npy

Detailed error statistics are saved in error_arrays.npz, and overall results printed to screen.

Performance testing

The performance of the analytical Jacobian matrix evaluation using pyJac can be tested against finite differencing and TChem. With the appropriate environment established, this test can be performed by giving only two arguments: a base directory and a number of OpenMP threads to use. The program scans for subdirectories in the base directory, looking for the following keys:

  • A Cantera mechanism (ending with .cti)
  • A Chemkin mechanism of the same name (ending with .dat)
  • An (optional) Chemkin thermodynamic file (with “therm” in filename) if required. If the thermo file is not specified, the mechanism is assumed to contain the thermo data.
  • Thermochemical state data (as generated previously using the PaSR module) files ending with *.npy

Note that all *.npy files in a directory will be used for testing purposes.

The performance tester can be called using:

python -m pyjac.performance_tester -w data/

Library Generation

pyJac also has the ability to generate shared / static libraries for linkage to external programs. This functionality is available via the pyjac.libgen submodule, and requires a gcc/nvcc installation available on the path. It can be called as:

python -m pyjac.libgen --source_dir /path/to/generated/pyjac/output \
       --lang cuda --static

Note that for linkage into an external program, CUDA requires use of a static library.

Python Wrapper Generation

In addition to the library generation described above, pyJac can directly generate a python wrapper for chemical source term / Jacobian evaluation (among others) directly from python. This functionality can be called via (e.g.,):

python -m pyjac.pywrap --source_dir /path/to/generated/pyjac/output \
       --lang cuda

For details of the functions included in the python wrapper, look at the .pyx files in pyjac.pywrap, or the calls in pyjac.functional_tester.test.cpyjac_evaluator

Using the Python Interface

Once generated, the python wrapper can be imported from a python script in the same directory, e.g.:

import pyjacob

Then the dydt() or eval_jacob() functions can be called, e.g. for the GRI-Mech 3.0 model as:

import pyjacob
import cantera as ct
import numpy as np

#create gas from original mechanism file gri30.cti
gas = ct.Solution('gri30.cti')
#reorder the gas to match pyJac
n2_ind = gas.species_index('N2')
specs = gas.species()[:]
gas = ct.Solution(thermo='IdealGas', kinetics='GasKinetics',
        species=specs[:n2_ind] + specs[n2_ind + 1:] + [specs[n2_ind]],
        reactions=gas.reactions())

#set the gas state
T = 1000
P = ct.one_atm
gas.TPY = T, P, "CH4:1.0, O2:2, N2:7.52"

#setup the state vector
y = np.zeros(gas.n_species)
y[0] = T
y[1:] = gas.Y[:-1]

#create a dydt vector
dydt = np.zeros_like(y)
pyjacob.py_dydt(0, P, y, dydt)

#create a jacobian vector
jac = np.zeros(gas.n_species * gas.n_species)

#evaluate the Jacobian
pyjacob.py_eval_jacobian(0, P, y, jac)

The above uses the state vector discussed in (What state vector should I pass into pyJac?), as well as the reordering in (What does this mean for comparison to say, Cantera?) to enable direct comparison to Cantera. Also note that we can pass a dummy time of 0, as explained in (What is the difference between “t” and “T” in the Python interface?).

The CUDA interface is less modular, and currently only supports evaluating the Jacobian directly (which in turn populates the other values). For example, if we have 1000 states to evaluate:

import cu_pyjacob
import cantera as ct
import numpy as np

#create gas from original mechanism file gri30.cti
gas = ct.Solution('gri30.cti')
#reorder the gas to match pyJac
n2_ind = gas.species_index('N2')
specs = gas.species()[:]
gas = ct.Solution(thermo='IdealGas', kinetics='GasKinetics',
        species=specs[:n2_ind] + specs[n2_ind + 1:] + [specs[n2_ind]],
        reactions=gas.reactions())

N_state = 1000
#setup the state vectors
y = np.zeros((N_state, gas.n_species))
pres = np.zeros(N_state)

#populate with dummy data
for i in range(N_state):
    #use cantera to normalize mass fractions
    gas.TPY = 2400 * np.random.rand(), 25 * ct.one_atm * np.random.rand(), \
        np.random.random(gas.n_species)

    #set state
    y[i, 0] = gas.T
    y[i, 1:] = gas.Y[:-1]
    pres[i] = gas.P

#flatten
y = y.flatten(order='f').astype(np.dtype('d'), order='c')

#find # of reversible reactions
num_rev = np.array([rxn.reversible
                            for rxn in gas.reactions()]
                            ).sum()
def __is_pdep(rxn):
    return (isinstance(rxn, ct.ThreeBodyReaction) or
        isinstance(rxn, ct.FalloffReaction) or
        isinstance(rxn, ct.ChemicallyActivatedReaction)
        )

num_pdep = np.array([__is_pdep(rxn)
                         for rxn in gas.reactions()]
                         ).sum()

#create other arrays
def __czeros(shape):
    #Return array of zeros in C ordering.
    arr = np.zeros(shape)
    return arr.flatten(order='c')

concs = __czeros((N_state, gas.n_species))
fwd_rates = __czeros((N_state, gas.n_reactions))
rev_rates = __czeros((N_state, num_rev))
pres_mod = __czeros((N_state, num_pdep))
spec_rates = __czeros((N_state, gas.n_species))
dydt = __czeros((N_state, gas.n_species))
jac = __czeros((N_state, gas.n_species * gas.n_species))

#intialize and get padding
N_pad = cu_pyjacob.py_cuinit(N_state)

#call jacobian function
cu_pyjacob.py_cujac(N_state, N_pad, pres, y, concs,
                        fwd_rates, rev_rates, pres_mod,
                        spec_rates, dydt, jac
                        )

#finally reshape arrays for sensible comparison
dydt = dydt.reshape((N_state, gas.n_species), order='f').astype(
    np.dtype('d'), order='c')
jac = jac.reshape((N_state, gas.n_species * gas.n_species),
    order='f').astype(np.dtype('d'), order='c')

Note that this uses the ordering discussed in (How do I pass numpy arrays to the C/CUDA pyJac functions from Python?), while the Jacobian values are explained in (What is in the Jacobian?).