mirror of
https://github.com/Cantera/cantera.git
synced 2026-08-08 12:08:13 -05:00
1536 lines
57 KiB
Python
1536 lines
57 KiB
Python
import numpy as np
|
|
import pytest
|
|
from pytest import approx
|
|
|
|
import cantera as ct
|
|
|
|
|
|
class RateExpressionTests:
|
|
"""
|
|
Generic test class to check derivatives evaluated for a single reaction within
|
|
a reaction mechanism. Derived classes must provide the following fixtures:
|
|
|
|
- gas: Cantera Solution object representing the gas phase.
|
|
- rxn_idx: Index of the reaction to be tested.
|
|
- equation: String containing the reaction equation.
|
|
- rate_type: String indicating the type of rate expression.
|
|
- orders: Dictionary specifying reaction orders (if applicable).
|
|
- ix3b: List of species indices involved in three-body interactions (if any).
|
|
"""
|
|
rtol = 1e-5 # relative tolerance for comparisons
|
|
|
|
rxn_idx = None
|
|
equation = None
|
|
rate_type = None
|
|
orders = None
|
|
ix3b = []
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn(self, gas, rxn_idx):
|
|
"""Fixture to retrieve the reaction object."""
|
|
return gas.reactions()[rxn_idx]
|
|
|
|
@pytest.fixture(scope='class')
|
|
def r_stoich(self, gas):
|
|
"""Fixture to retrieve reactant stoichiometric coefficients."""
|
|
return gas.reactant_stoich_coeffs
|
|
|
|
@pytest.fixture(scope='class')
|
|
def p_stoich(self, gas):
|
|
"""Fixture to retrieve product stoichiometric coefficients."""
|
|
return gas.product_stoich_coeffs
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rix(self, gas, rxn):
|
|
"""Fixture to retrieve reactant species indices."""
|
|
return [gas.species_index(k) for k in rxn.reactants.keys()]
|
|
|
|
@pytest.fixture(scope='class')
|
|
def pix(self, gas, rxn):
|
|
"""Fixture to retrieve product species indices."""
|
|
return [gas.species_index(k) for k in rxn.products.keys()]
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn_idx(self, test_data):
|
|
return test_data["rxn_idx"]
|
|
|
|
@pytest.fixture(scope='class')
|
|
def equation(self, test_data):
|
|
return test_data["equation"]
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rate_type(self, test_data):
|
|
return test_data["rate_type"]
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ix3b(self, test_data):
|
|
"""Fixture to retrieve species indices involved in three-body interactions."""
|
|
return test_data.get("ix3b", [])
|
|
|
|
@pytest.fixture(scope='class')
|
|
def orders(self, test_data):
|
|
"""Fixture to retrieve reaction orders."""
|
|
return test_data.get("orders", None)
|
|
|
|
@pytest.fixture(scope='class')
|
|
def setup_rate_expression_tests(self, gas, r_stoich, p_stoich, rxn):
|
|
"""
|
|
Sets the TPX state for the gas object and provides stoichiometric coefficients
|
|
and species indices as well as setting test-specific attributes. Runs once
|
|
per class.
|
|
"""
|
|
tpx = gas.TPX
|
|
return {
|
|
"tpx": tpx,
|
|
"r_stoich": r_stoich,
|
|
"p_stoich": p_stoich
|
|
}
|
|
|
|
@pytest.fixture(scope='function', autouse=True)
|
|
def setup_rate_expression_data(self, gas, setup_rate_expression_tests, rxn_idx, rxn):
|
|
"""
|
|
Resets the gas state before each test and configures reaction multipliers.
|
|
Also verifies stoichiometric coefficients.
|
|
"""
|
|
data = setup_rate_expression_tests
|
|
tpx = data["tpx"]
|
|
r_stoich = data["r_stoich"]
|
|
p_stoich = data["p_stoich"]
|
|
|
|
# Reset gas phase
|
|
gas.TPX = tpx
|
|
gas.set_multiplier(0.0)
|
|
gas.set_multiplier(1.0, rxn_idx)
|
|
gas.derivative_settings = {}
|
|
|
|
# Check stoichiometric coefficients for reactants
|
|
for k, v in rxn.reactants.items():
|
|
ix = gas.species_index(k)
|
|
actual = r_stoich[ix, rxn_idx]
|
|
assert actual == v, f"Reactant stoich mismatch for species '{k}'"
|
|
|
|
# Check stoichiometric coefficients for products
|
|
for k, v in rxn.products.items():
|
|
ix = gas.species_index(k)
|
|
actual = p_stoich[ix, rxn_idx]
|
|
assert actual == v, f"Product stoich mismatch for species '{k}'"
|
|
|
|
def test_input(self, equation, rate_type, rxn):
|
|
"""Ensure that correct equation is referenced"""
|
|
assert equation == rxn.equation
|
|
assert rate_type == rxn.rate.type
|
|
|
|
def rop_derivs(self, gas, spc_ix, mode, const_t=True, rtol_deltac=1e-5, atol_deltac=1e-20, ddX=True):
|
|
"""
|
|
Numerical derivative for rates-of-progress with respect to mole fractions
|
|
"""
|
|
def calc():
|
|
if mode == "forward":
|
|
return gas.forward_rates_of_progress
|
|
if mode == "reverse":
|
|
return gas.reverse_rates_of_progress
|
|
if mode == "net":
|
|
return gas.net_rates_of_progress
|
|
|
|
tpx = gas.TPX
|
|
rop0 = calc()
|
|
conc = gas.concentrations
|
|
ctot0 = conc.sum()
|
|
|
|
# perturb concentration
|
|
dconc = conc[spc_ix] * rtol_deltac + atol_deltac
|
|
conc[spc_ix] += dconc
|
|
ctot1 = conc.sum()
|
|
if const_t:
|
|
# adjust pressure to compensate for concentration change
|
|
pnew = gas.P * ctot1 / ctot0
|
|
gas.TPX = gas.T, pnew, conc / ctot1
|
|
else:
|
|
# adjust temperature to compensate for concentration change
|
|
tnew = gas.T * ctot1 / ctot0
|
|
gas.TPX = tnew, gas.P, conc / ctot1
|
|
drop = (calc() - rop0) / dconc
|
|
|
|
gas.TPX = tpx # reset state
|
|
if ddX:
|
|
return drop * gas.density_mole
|
|
else:
|
|
return drop
|
|
|
|
def test_forward_rop_ddX(self, gas, r_stoich, rxn_idx, rix, rxn, ix3b, orders):
|
|
# check derivatives of forward rates of progress with respect to mole fractions
|
|
# against analytic result
|
|
dropm = gas.forward_rates_of_progress_ddX
|
|
dropp = gas.forward_rates_of_progress_ddP
|
|
|
|
gas.derivative_settings = {"skip-third-bodies": True}
|
|
drop = gas.forward_rates_of_progress_ddX
|
|
rop = gas.forward_rates_of_progress
|
|
for spc_ix in rix:
|
|
if orders is None:
|
|
order = r_stoich[spc_ix, rxn_idx]
|
|
else:
|
|
order = orders[gas.species_names[spc_ix]]
|
|
assert rop[rxn_idx] == approx(
|
|
drop[rxn_idx, spc_ix] * gas.X[spc_ix] / order)
|
|
|
|
drop_num = self.rop_derivs(gas, spc_ix, mode="forward")
|
|
assert dropm[:, spc_ix] + dropp * gas.P == approx(drop_num, rel=self.rtol)
|
|
|
|
if isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
for spc_ix in set(rix + ix3b):
|
|
assert dropm[rxn_idx, spc_ix] # non-zero
|
|
dropm[rxn_idx, spc_ix] = 0
|
|
assert not dropm.any()
|
|
|
|
def test_reverse_rop_ddX(self, gas, rxn_idx, p_stoich, pix, rxn, ix3b):
|
|
# check derivatives of reverse rates of progress with respect to mole fractions
|
|
# against analytic result
|
|
dropm = gas.reverse_rates_of_progress_ddX
|
|
dropp = gas.reverse_rates_of_progress_ddP
|
|
|
|
gas.derivative_settings = {"skip-third-bodies": True}
|
|
drop = gas.reverse_rates_of_progress_ddX
|
|
rop = gas.reverse_rates_of_progress
|
|
for spc_ix in pix:
|
|
order = p_stoich[spc_ix, rxn_idx]
|
|
assert rop[rxn_idx] == approx(
|
|
drop[rxn_idx, spc_ix] * gas.X[spc_ix] / order)
|
|
|
|
drop_num = self.rop_derivs(gas, spc_ix, mode="reverse")
|
|
assert dropm[:, spc_ix] + dropp * gas.P == approx(drop_num, rel=self.rtol)
|
|
|
|
if not rxn.reversible or isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
for spc_ix in set(pix + ix3b):
|
|
assert dropm[rxn_idx, spc_ix] # non-zero
|
|
dropm[rxn_idx, spc_ix] = 0
|
|
assert not dropm.any()
|
|
|
|
def test_net_rop_ddX(self, gas, rxn_idx, rix, pix, rxn, ix3b):
|
|
# check derivatives of net rates of progress with respect to mole fractions
|
|
# against numeric result
|
|
drop = gas.net_rates_of_progress_ddX
|
|
dropp = gas.net_rates_of_progress_ddP
|
|
|
|
for spc_ix in rix + pix:
|
|
drop_num = self.rop_derivs(gas, spc_ix, mode="net")
|
|
ix = drop[:, spc_ix] != 0
|
|
drop_ = drop[:, spc_ix] + dropp * gas.P
|
|
assert drop_[ix] == approx(drop_num[ix], rel=self.rtol)
|
|
|
|
if not rxn.reversible or isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
for spc_ix in set(rix + pix + ix3b):
|
|
assert drop[rxn_idx, spc_ix] # non-zero
|
|
drop[rxn_idx, spc_ix] = 0
|
|
assert not drop.any()
|
|
|
|
def test_forward_rop_ddCi(self, gas, rxn_idx, r_stoich, rix, rxn, ix3b, orders):
|
|
# check derivatives of forward rates of progress with respect to species
|
|
# concentrations against analytic result
|
|
dropm = gas.forward_rates_of_progress_ddCi
|
|
dropp = gas.forward_rates_of_progress_ddP
|
|
|
|
gas.derivative_settings = {"skip-third-bodies": True}
|
|
drop = gas.forward_rates_of_progress_ddCi
|
|
rop = gas.forward_rates_of_progress
|
|
for spc_ix in rix:
|
|
if orders is None:
|
|
order = r_stoich[spc_ix, rxn_idx]
|
|
else:
|
|
order = orders[gas.species_names[spc_ix]]
|
|
assert rop[rxn_idx] == approx(
|
|
drop[rxn_idx, spc_ix] * gas.concentrations[spc_ix] / order)
|
|
|
|
drop_num = self.rop_derivs(gas, spc_ix, mode="forward", ddX=False)
|
|
assert dropm[:, spc_ix] + dropp * gas.P == approx(drop_num, rel=1e-3)
|
|
|
|
if isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
for spc_ix in set(rix + ix3b):
|
|
assert dropm[rxn_idx, spc_ix] # non-zero
|
|
dropm[rxn_idx, spc_ix] = 0
|
|
assert not dropm.any()
|
|
|
|
def test_reverse_rop_ddCi(self, gas, rxn_idx, r_stoich, pix, p_stoich, rix, rxn, ix3b, orders):
|
|
# check derivatives of reverse rates of progress with respect to species
|
|
# concentrations against analytic result
|
|
dropm = gas.reverse_rates_of_progress_ddCi
|
|
dropp = gas.reverse_rates_of_progress_ddP
|
|
|
|
gas.derivative_settings = {"skip-third-bodies": True}
|
|
drop = gas.reverse_rates_of_progress_ddCi
|
|
rop = gas.reverse_rates_of_progress
|
|
for spc_ix in pix:
|
|
order = p_stoich[spc_ix, rxn_idx]
|
|
assert rop[rxn_idx] == approx(
|
|
drop[rxn_idx, spc_ix] * gas.concentrations[spc_ix] / order)
|
|
|
|
drop_num = self.rop_derivs(gas, spc_ix, mode="reverse", ddX=False)
|
|
assert dropm[:, spc_ix] + dropp * gas.P == approx(drop_num, rel=1e-3)
|
|
|
|
if not rxn.reversible or isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
for spc_ix in set(pix + ix3b):
|
|
assert dropm[rxn_idx, spc_ix] # non-zero
|
|
dropm[rxn_idx, spc_ix] = 0
|
|
assert not dropm.any()
|
|
|
|
def test_net_rop_ddCi(self, gas, rxn_idx, r_stoich, rix, pix, rxn, ix3b, orders):
|
|
# check derivatives of net rates of progress with respect to species
|
|
# concentrations against numeric result
|
|
drop = gas.net_rates_of_progress_ddCi
|
|
dropp = gas.net_rates_of_progress_ddP
|
|
|
|
for spc_ix in rix + pix:
|
|
drop_num = self.rop_derivs(gas, spc_ix, mode="net", ddX=False)
|
|
ix = drop[:, spc_ix] != 0
|
|
drop_ = drop[:, spc_ix] + dropp * gas.P
|
|
assert drop_[ix] == approx(drop_num[ix], rel=1e-4)
|
|
|
|
if not rxn.reversible or isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
def rop_ddT(self, gas, rxn_idx, mode=None, const_p=False, rtol=1e-6):
|
|
# numerical derivative for rates-of-progress at constant pressure
|
|
def calc():
|
|
if mode == "forward":
|
|
return gas.forward_rates_of_progress
|
|
if mode == "reverse":
|
|
return gas.reverse_rates_of_progress
|
|
if mode == "net":
|
|
return gas.net_rates_of_progress
|
|
|
|
tpx = gas.TPX
|
|
dt = tpx[0] * rtol
|
|
dp = 0 if const_p else tpx[1] * rtol
|
|
gas.TP = tpx[0] + dt, tpx[1] + dp
|
|
rop1 = calc()
|
|
gas.TP = tpx[:2]
|
|
rop0 = calc()
|
|
gas.TPX = tpx
|
|
return (rop1[rxn_idx] - rop0[rxn_idx]) / dt
|
|
|
|
def test_forward_rop_ddT(self, gas, rxn_idx, rxn):
|
|
# check derivatives of forward rop with respect to temperature
|
|
|
|
# constant pressure - need to account for density change
|
|
dcdt = - gas.density_mole / gas.T
|
|
drop = gas.forward_rates_of_progress_ddT
|
|
drop += gas.forward_rates_of_progress_ddC * dcdt
|
|
drop_num = self.rop_ddT(gas, rxn_idx, mode="forward", const_p=True)
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
# constant density (volume) - need to account for pressure change
|
|
dpdt = gas.P / gas.T
|
|
drop = gas.forward_rates_of_progress_ddT
|
|
drop += gas.forward_rates_of_progress_ddP * dpdt
|
|
drop_num = self.rop_ddT(gas, rxn_idx, mode="forward")
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
if isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
assert drop[rxn_idx] # non-zero
|
|
drop[rxn_idx] = 0
|
|
assert not drop.any()
|
|
|
|
def test_reverse_rop_ddT(self, gas, rxn_idx, rxn):
|
|
# check derivatives of reverse rop with respect to temperature
|
|
|
|
# constant pressure - need to account for density change
|
|
dcdt = - gas.density_mole / gas.T
|
|
drop = gas.reverse_rates_of_progress_ddT
|
|
drop += gas.reverse_rates_of_progress_ddC * dcdt
|
|
drop_num = self.rop_ddT(gas, rxn_idx, mode="reverse", const_p=True)
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
# constant density (volume) - need to account for pressure change
|
|
dpdt = gas.P / gas.T
|
|
drop = gas.reverse_rates_of_progress_ddT
|
|
drop += gas.reverse_rates_of_progress_ddP * dpdt
|
|
drop_num = self.rop_ddT(gas, rxn_idx, mode="reverse")
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
if not rxn.reversible or isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
assert drop[rxn_idx] # non-zero
|
|
drop[rxn_idx] = 0
|
|
assert not drop.any()
|
|
|
|
def test_net_rop_ddT(self, gas, rxn_idx, rxn):
|
|
# check derivatives of net rop with respect to temperature
|
|
|
|
# constant pressure - need to account for density change
|
|
dcdt = - gas.density_mole / gas.T
|
|
drop = gas.net_rates_of_progress_ddT
|
|
drop += gas.net_rates_of_progress_ddC * dcdt
|
|
drop_num = self.rop_ddT(gas, rxn_idx, mode="net", const_p=True)
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
# constant density (volume) - need to account for pressure change
|
|
dpdt = gas.P / gas.T
|
|
drop = gas.net_rates_of_progress_ddT
|
|
drop += gas.net_rates_of_progress_ddP * dpdt
|
|
drop_num = self.rop_ddT(gas, rxn_idx, mode="forward") - self.rop_ddT(gas, rxn_idx, mode="reverse")
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
if not rxn.reversible or isinstance(rxn.rate, ct.FalloffRate):
|
|
return
|
|
|
|
# ensure all zeros are in the correct spots
|
|
assert drop[rxn_idx] # non-zero
|
|
drop[rxn_idx] = 0
|
|
assert not drop.any()
|
|
|
|
def rop_ddP(self, gas, rxn_idx, mode=None, rtol=1e-6):
|
|
# numerical derivative for rates-of-progress at constant pressure
|
|
def calc():
|
|
if mode == "forward":
|
|
return gas.forward_rates_of_progress
|
|
if mode == "reverse":
|
|
return gas.reverse_rates_of_progress
|
|
if mode == "net":
|
|
return gas.net_rates_of_progress
|
|
|
|
tpx = gas.TPX
|
|
dp = tpx[1] * rtol
|
|
gas.TP = tpx[0], tpx[1] + dp
|
|
rop1 = calc()
|
|
gas.TP = tpx[:2]
|
|
rop0 = calc()
|
|
return (rop1[rxn_idx] - rop0[rxn_idx]) / dp
|
|
|
|
def test_forward_rop_ddP(self, gas, rxn_idx):
|
|
# check derivatives of forward rop with respect to pressure
|
|
|
|
# constant temperature - need to account for density change
|
|
dcdp = gas.density_mole / gas.P
|
|
drop = gas.forward_rates_of_progress_ddP
|
|
drop += gas.forward_rates_of_progress_ddC * dcdp
|
|
drop_num = self.rop_ddP(gas, rxn_idx, mode="forward")
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
def test_reverse_rop_ddP(self, gas, rxn_idx):
|
|
# check derivatives of reverse rop with respect to pressure
|
|
|
|
# constant temperature - need to account for density change
|
|
dcdp = gas.density_mole / gas.P
|
|
drop = gas.reverse_rates_of_progress_ddP
|
|
drop += gas.reverse_rates_of_progress_ddC * dcdp
|
|
drop_num = self.rop_ddP(gas, rxn_idx, mode="reverse")
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
def test_net_rop_ddP(self, gas, rxn_idx):
|
|
# check derivatives of net rop with respect to pressure
|
|
|
|
# constant temperature - need to account for density change
|
|
dcdp = gas.density_mole / gas.P
|
|
drop = gas.net_rates_of_progress_ddP
|
|
drop += gas.net_rates_of_progress_ddC * dcdp
|
|
drop_num = self.rop_ddP(gas, rxn_idx, mode="net")
|
|
assert drop[rxn_idx] == approx(drop_num, rel=self.rtol)
|
|
|
|
def rate_ddT(self, gas, mode=None, const_p=False, rtol=1e-6):
|
|
# numerical derivative for production rates with respect to temperature
|
|
def calc():
|
|
if mode == "creation":
|
|
return gas.creation_rates
|
|
if mode == "destruction":
|
|
return gas.destruction_rates
|
|
if mode == "net":
|
|
return gas.net_production_rates
|
|
|
|
tpx = gas.TPX
|
|
dt = tpx[0] * rtol
|
|
dp = 0 if const_p else tpx[1] * rtol
|
|
gas.TP = tpx[0] + dt, tpx[1] + dp
|
|
rate1 = calc()
|
|
gas.TP = tpx[:2]
|
|
rate0 = calc()
|
|
gas.TPX = tpx
|
|
return (rate1 - rate0) / dt
|
|
|
|
def test_net_rate_ddT(self, gas, rix, pix):
|
|
# check equivalence of numerical and analytical derivatives of net creation
|
|
# rates with respect to temperature
|
|
|
|
# constant pressure - need to account for density change
|
|
# numeric: d(omegadot)/dT =
|
|
# analytic: d(omegadot)/dT + dC/dT d(omegadot)/dC
|
|
dcdt = - gas.density_mole / gas.T
|
|
drate = gas.net_production_rates_ddT
|
|
drate += gas.net_production_rates_ddC * dcdt
|
|
drate_num = self.rate_ddT(gas, mode="net", const_p=True)
|
|
for spc_ix in rix + pix:
|
|
assert drate[spc_ix] == approx(drate_num[spc_ix], rel=self.rtol)
|
|
|
|
# constant density (volume) - need to account for pressure change
|
|
# numeric: d(omegadot)/dT =
|
|
# analytic: d(omegadot)/dT + dP/dT d(omegadot)/dP
|
|
dpdt = gas.P / gas.T
|
|
drate = gas.net_production_rates_ddT
|
|
drate += gas.net_production_rates_ddP * dpdt
|
|
drate_num = self.rate_ddT(gas, mode="creation") - self.rate_ddT(gas, mode="destruction")
|
|
for spc_ix in rix + pix:
|
|
assert drate[spc_ix] == approx(drate_num[spc_ix], rel=self.rtol)
|
|
|
|
def rate_ddX(self, gas, spc_ix, mode=None, const_t=True, rtol_deltac=1e-6,
|
|
atol_deltac=1e-20, ddX=True):
|
|
# numerical derivative for production rates with respect to mole fractions
|
|
def calc(mode):
|
|
if mode == "creation":
|
|
return gas.creation_rates
|
|
if mode == "destruction":
|
|
return gas.destruction_rates
|
|
if mode == "net":
|
|
return gas.net_production_rates
|
|
|
|
tpx = gas.TPX
|
|
|
|
rate0 = calc(mode)
|
|
conc = gas.concentrations
|
|
ctot0 = conc.sum()
|
|
|
|
# perturb concentration
|
|
dconc = conc[spc_ix] * rtol_deltac + atol_deltac
|
|
conc[spc_ix] += dconc
|
|
ctot1 = conc.sum()
|
|
if const_t:
|
|
# adjust pressure to compensate for concentration change
|
|
pnew = gas.P * ctot1 / ctot0
|
|
gas.TPX = gas.T, pnew, conc / ctot1
|
|
else:
|
|
# adjust temperature to compensate for concentration change
|
|
tnew = gas.T * ctot1 / ctot0
|
|
gas.TPX = tnew, gas.P, conc / ctot1
|
|
drate = (calc(mode) - rate0) / dconc
|
|
|
|
gas.TPX = tpx # reset state
|
|
# cantera calculates kinetics derivatives with respect to mole fractions
|
|
# and concentrations, when ddX flag is true it will return the numerical
|
|
# derivatives in the form of mole fractions but otherwise return concentrations
|
|
if ddX:
|
|
return drate * gas.density_mole
|
|
else:
|
|
return drate
|
|
|
|
def test_creation_ddX(self, gas, rix, pix):
|
|
# check derivatives of creation rates with respect to mole fractions
|
|
drate = gas.creation_rates_ddX
|
|
dratep = gas.creation_rates_ddP
|
|
for spc_ix in rix + pix:
|
|
drate_num = self.rate_ddX(gas, spc_ix, "creation")
|
|
ix = drate[:, spc_ix] != 0
|
|
drate[:, spc_ix] += dratep * gas.P
|
|
assert drate[ix, spc_ix] == approx(drate_num[ix], rel=self.rtol)
|
|
|
|
def test_destruction_ddX(self, gas, rix, pix):
|
|
# check derivatives of destruction rates with respect to mole fractions
|
|
drate = gas.destruction_rates_ddX
|
|
dratep = gas.destruction_rates_ddP
|
|
for spc_ix in rix + pix:
|
|
drate_num = self.rate_ddX(gas, spc_ix, "destruction")
|
|
ix = drate[:, spc_ix] != 0
|
|
drate[:, spc_ix] += dratep * gas.P
|
|
assert drate[ix, spc_ix] == approx(drate_num[ix], rel=self.rtol)
|
|
|
|
def test_net_production_ddX(self, gas, rix, pix):
|
|
# check derivatives of destruction rates with respect to mole fractions
|
|
drate = gas.net_production_rates_ddX
|
|
dratep = gas.net_production_rates_ddP
|
|
for spc_ix in rix + pix:
|
|
drate_num = self.rate_ddX(gas, spc_ix, "net")
|
|
ix = drate[:, spc_ix] != 0
|
|
drate[:, spc_ix] += dratep * gas.P
|
|
assert drate[ix, spc_ix] == approx(drate_num[ix], rel=self.rtol)
|
|
|
|
def test_creation_ddCi(self, gas, rix, pix):
|
|
# check derivatives of creation rates with respect to mole fractions
|
|
drate = gas.creation_rates_ddCi
|
|
dratep = gas.creation_rates_ddP
|
|
for spc_ix in rix + pix:
|
|
drate_num = self.rate_ddX(gas, spc_ix, "creation", ddX=False)
|
|
ix = drate[:, spc_ix] != 0
|
|
drate[:, spc_ix] += dratep * gas.P
|
|
assert drate[ix, spc_ix] == approx(drate_num[ix], rel=1e-3)
|
|
|
|
def test_destruction_ddCi(self, gas, rix, pix):
|
|
# check derivatives of destruction rates with respect to mole fractions
|
|
drate = gas.destruction_rates_ddCi
|
|
dratep = gas.destruction_rates_ddP
|
|
for spc_ix in rix + pix:
|
|
drate_num = self.rate_ddX(gas, spc_ix, "destruction", ddX=False)
|
|
ix = drate[:, spc_ix] != 0
|
|
drate[:, spc_ix] += dratep * gas.P
|
|
assert drate[ix, spc_ix] == approx(drate_num[ix], rel=1e-3)
|
|
|
|
def test_net_production_ddCi(self, gas, rix, pix):
|
|
# check derivatives of destruction rates with respect to mole fractions
|
|
drate = gas.net_production_rates_ddCi
|
|
dratep = gas.net_production_rates_ddP
|
|
for spc_ix in rix + pix:
|
|
drate_num = self.rate_ddX(gas, spc_ix, "net", ddX=False)
|
|
ix = drate[:, spc_ix] != 0
|
|
# drate[:, spc_ix] += dratep * gas.P
|
|
assert drate[ix, spc_ix] == approx(drate_num[ix], rel=1e-3)
|
|
|
|
|
|
class HydrogenOxygen(RateExpressionTests):
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution("h2o2.yaml", transport_model=None)
|
|
# species: [H2, H, O, O2, OH, H2O, HO2, H2O2, AR, N2]
|
|
gas.X = [0.1, 1e-4, 1e-5, 0.2, 2e-4, 0.3, 1e-6, 5e-5, 0.3, 0.1]
|
|
gas.TP = 800, 2 * ct.one_atm
|
|
return gas
|
|
|
|
class TestElementaryRev(HydrogenOxygen):
|
|
"""Standard elementary reaction with two reactants"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 2,
|
|
"equation": "H2 + O <=> H + OH",
|
|
"rate_type": "Arrhenius"
|
|
}
|
|
|
|
class TestElementarySelf(HydrogenOxygen):
|
|
"""Elementary reaction with reactant reacting with itself"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 27,
|
|
"equation": "2 HO2 <=> H2O2 + O2",
|
|
"rate_type": "Arrhenius"
|
|
}
|
|
|
|
|
|
class TestFalloff(HydrogenOxygen):
|
|
""" Fall-off reaction"""
|
|
rtol = 2e-4
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 21,
|
|
"equation": "2 OH (+M) <=> H2O2 (+M)",
|
|
"rate_type": "falloff"
|
|
}
|
|
|
|
class TestThreeBody(HydrogenOxygen):
|
|
""" Three-body reaction with default efficiency"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self, gas):
|
|
return {
|
|
"rxn_idx": 1,
|
|
"equation": "H + O + M <=> OH + M",
|
|
"rate_type": "Arrhenius",
|
|
"ix3b": list(range(gas.n_species))
|
|
}
|
|
|
|
def test_thirdbodies_forward(self, gas, rxn_idx):
|
|
drop = gas.forward_rates_of_progress_ddX
|
|
gas.derivative_settings = {"skip-third-bodies": True}
|
|
drops = gas.forward_rates_of_progress_ddX
|
|
dropm = drop - drops
|
|
rop = gas.forward_rates_of_progress
|
|
assert rop[rxn_idx] == approx((dropm[rxn_idx] * gas.X).sum())
|
|
|
|
def test_thirdbodies_reverse(self, gas, rxn_idx):
|
|
drop = gas.reverse_rates_of_progress_ddX
|
|
gas.derivative_settings = {"skip-third-bodies": True}
|
|
drops = gas.reverse_rates_of_progress_ddX
|
|
dropm = drop - drops
|
|
rop = gas.reverse_rates_of_progress
|
|
assert rop[rxn_idx] == approx((dropm[rxn_idx] * gas.X).sum())
|
|
|
|
class EdgeCases(RateExpressionTests):
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution("jacobian-tests.yaml", transport_model=None)
|
|
# species: [H2, H, O, O2, OH, H2O, HO2, H2O2, AR]
|
|
gas.X = [0.1, 1e-4, 1e-5, 0.2, 2e-4, 0.3, 1e-6, 5e-5, 0.4]
|
|
gas.TP = 800, 2 * ct.one_atm
|
|
return gas
|
|
|
|
class TestElementaryIrr(EdgeCases):
|
|
"""Irreversible elementary reaction with two reactants"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 0,
|
|
"equation": "HO2 + O => O2 + OH",
|
|
"rate_type": "Arrhenius"
|
|
}
|
|
|
|
class TestElementaryOne(EdgeCases):
|
|
"""Three-body reaction with single reactant species"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 1,
|
|
"equation": "H2 <=> 2 H",
|
|
"rate_type": "Arrhenius"
|
|
}
|
|
|
|
class TestElementaryThree(EdgeCases):
|
|
"""Elementary reaction with three reactants"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 2,
|
|
"equation": "2 H + O <=> H2O",
|
|
"rate_type": "Arrhenius"
|
|
}
|
|
|
|
|
|
class TestElementaryFrac(EdgeCases):
|
|
"""Elementary reaction with specified reaction order"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 3,
|
|
"equation": "0.7 H2 + 0.2 O2 + 0.6 OH => H2O",
|
|
"rate_type": "Arrhenius",
|
|
"orders": {"H2": 0.8, "O2": 1.0, "OH": 2.0}
|
|
}
|
|
|
|
|
|
class TestThreeBodyNoDefault(EdgeCases):
|
|
"""Three body reaction without default efficiency"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self, gas):
|
|
efficiencies = {"H2": 2.0, "H2O": 6.0, "AR": 0.7}
|
|
return {
|
|
"rxn_idx": 4,
|
|
"equation": "H + O + M <=> OH + M",
|
|
"rate_type": "Arrhenius",
|
|
"ix3b": [gas.species_index(k) for k in efficiencies.keys()]
|
|
}
|
|
|
|
|
|
class FromScratchCases(RateExpressionTests):
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution("kineticsfromscratch.yaml", transport_model=None)
|
|
# species: [AR, O, H2, H, OH, O2, H2O, H2O2, HO2]
|
|
gas.X = [0.1, 3e-4, 5e-5, 6e-6, 3e-3, 0.6, 0.25, 1e-6, 2e-5]
|
|
gas.TP = 2000, 5 * ct.one_atm
|
|
return gas
|
|
|
|
@pytest.mark.usefixtures("has_temperature_derivative_warnings")
|
|
def test_forward_rop_ddT(self, gas, rxn_idx, rxn):
|
|
"""Override to handle temperature derivative warnings."""
|
|
super().test_forward_rop_ddT(gas, rxn_idx, rxn)
|
|
|
|
@pytest.mark.usefixtures("has_temperature_derivative_warnings")
|
|
def test_reverse_rop_ddT(self, gas, rxn_idx, rxn):
|
|
"""Override to handle temperature derivative warnings."""
|
|
super().test_reverse_rop_ddT(gas, rxn_idx, rxn)
|
|
|
|
@pytest.mark.usefixtures("has_temperature_derivative_warnings")
|
|
def test_net_rop_ddT(self, gas, rxn_idx, rxn):
|
|
"""Override to handle temperature derivative warnings."""
|
|
super().test_net_rop_ddT(gas, rxn_idx, rxn)
|
|
|
|
@pytest.mark.usefixtures("has_temperature_derivative_warnings")
|
|
def test_net_rate_ddT(self, gas, rix, pix):
|
|
"""Override to handle temperature derivative warnings."""
|
|
super().test_net_rate_ddT(gas, rix, pix)
|
|
|
|
|
|
class TestPlog(FromScratchCases):
|
|
""" Plog reaction"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 3,
|
|
"equation": "H2 + O2 <=> 2 OH",
|
|
"rate_type": "pressure-dependent-Arrhenius"
|
|
}
|
|
|
|
|
|
class TestChebyshev(FromScratchCases):
|
|
"""Chebyshev reaction"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 4,
|
|
"equation": "HO2 <=> O + OH",
|
|
"rate_type": "Chebyshev"
|
|
}
|
|
|
|
|
|
class TestBlowersMasel(FromScratchCases):
|
|
"""Blowers-Masel"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def test_data(self):
|
|
return {
|
|
"rxn_idx": 6,
|
|
"equation": "H2 + O <=> H + OH",
|
|
"rate_type": "Blowers-Masel"
|
|
}
|
|
|
|
@pytest.mark.xfail(reason="Change of reaction enthalpy is not considered")
|
|
@pytest.mark.filterwarnings("ignore:.*does not consider.*(electron|enthalpy).*:UserWarning")
|
|
def test_forward_rop_ddT(self, gas, rxn_idx, rxn):
|
|
"""Override to handle issues with Blowers-Masel derivatives."""
|
|
super().test_forward_rop_ddT(gas, rxn_idx, rxn)
|
|
|
|
@pytest.mark.xfail(reason="Change of reaction enthalpy is not considered")
|
|
@pytest.mark.filterwarnings("ignore:.*does not consider.*(electron|enthalpy).*:UserWarning")
|
|
def test_reverse_rop_ddT(self, gas, rxn_idx, rxn):
|
|
"""Override to handle issues with Blowers-Masel derivatives."""
|
|
super().test_reverse_rop_ddT(gas, rxn_idx, rxn)
|
|
|
|
@pytest.mark.xfail(reason="Change of reaction enthalpy is not considered")
|
|
@pytest.mark.filterwarnings("ignore:.*does not consider.*(electron|enthalpy).*:UserWarning")
|
|
def test_net_rop_ddT(self, gas, rxn_idx, rxn):
|
|
"""Override to handle issues with Blowers-Masel derivatives."""
|
|
super().test_net_rop_ddT(gas, rxn_idx, rxn)
|
|
|
|
@pytest.mark.xfail(reason="Change of reaction enthalpy is not considered")
|
|
@pytest.mark.filterwarnings("ignore:.*does not consider.*(electron|enthalpy).*:UserWarning")
|
|
def test_net_rate_ddT(self, gas, rix, pix):
|
|
"""Override to handle issues with Blowers-Masel derivatives."""
|
|
super().test_net_rate_ddT(gas, rix, pix)
|
|
|
|
|
|
|
|
class FullTests:
|
|
"""
|
|
Generic test class to check derivatives evaluated for an entire reaction mechanisms
|
|
"""
|
|
rtol = 1e-4
|
|
|
|
@pytest.fixture(scope='class')
|
|
def initial_conditions(self, gas):
|
|
"""Store initial TPX conditions for gas."""
|
|
return gas.TPX
|
|
|
|
@pytest.fixture(scope='class', autouse=True)
|
|
def setup_full_tests(self, gas, initial_conditions):
|
|
"""
|
|
Equilibrate the gas phase at constant enthalpy and pressure.
|
|
"""
|
|
gas.equilibrate("HP")
|
|
|
|
@pytest.fixture(scope='function', autouse=True)
|
|
def full_tests_data(self, gas, initial_conditions):
|
|
"""
|
|
Reset TPX and derivative settings before each test.
|
|
"""
|
|
gas.TPX = initial_conditions
|
|
gas.derivative_settings = {} # Reset to defaults
|
|
|
|
def rop_derivs(self, gas, mode, rtol_deltac=1e-9, atol_deltac=1e-20, ddX=True):
|
|
# numerical derivative for rates-of-progress with respect to mole fractions
|
|
def calc():
|
|
if mode == "forward":
|
|
return gas.forward_rates_of_progress
|
|
if mode == "reverse":
|
|
return gas.reverse_rates_of_progress
|
|
if mode == "net":
|
|
return gas.net_rates_of_progress
|
|
|
|
n_spc, n_rxn = gas.n_species, gas.n_reactions
|
|
drop = np.zeros((n_rxn, n_spc))
|
|
|
|
tpx = gas.TPX
|
|
rop0 = calc()
|
|
ctot0 = gas.density_mole
|
|
ctot0 = gas.concentrations.sum()
|
|
|
|
for spc_ix in range(n_spc):
|
|
conc = gas.concentrations
|
|
dconc = conc[spc_ix] * rtol_deltac + atol_deltac
|
|
conc[spc_ix] += dconc
|
|
ctot1 = conc.sum()
|
|
gas.TPX = tpx[0], tpx[1] * ctot1 / ctot0, conc / ctot1
|
|
drop[:, spc_ix] = (calc() - rop0) / dconc
|
|
gas.TPX = tpx
|
|
|
|
if ddX:
|
|
return drop * gas.density_mole
|
|
else:
|
|
return drop
|
|
|
|
def test_forward_rop_ddX(self, gas):
|
|
"""
|
|
Check forward rop against numerical derivative with respect to mole fractions
|
|
"""
|
|
drop = gas.forward_rates_of_progress_ddX
|
|
dropp = gas.forward_rates_of_progress_ddP
|
|
drop_num = self.rop_derivs(gas, mode="forward")
|
|
stoich = gas.reactant_stoich_coeffs
|
|
for i in range(gas.n_reactions):
|
|
try:
|
|
# test entries that are not spurious
|
|
ix = np.abs((stoich[:, i] != 0) * drop[i, :]) > 1e-6
|
|
drop_ = drop[i, ix] + dropp[i] * gas.P
|
|
assert drop_ == approx(drop_num[i, ix], rel=self.rtol)
|
|
except AssertionError as err:
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(np.vstack([drop[i, ix], drop_num[i, ix]]).T)
|
|
raise err
|
|
|
|
def test_reverse_rop_ddX(self, gas):
|
|
"""
|
|
Check reverse rop against numerical derivative with respect to mole fractions
|
|
"""
|
|
drop = gas.reverse_rates_of_progress_ddX
|
|
dropp = gas.reverse_rates_of_progress_ddP
|
|
drop_num = self.rop_derivs(gas, mode="reverse")
|
|
stoich = gas.product_stoich_coeffs
|
|
for i in range(gas.n_reactions):
|
|
try:
|
|
# test entries that are not spurious
|
|
ix = np.abs((stoich[:, i] != 0) * drop[i, :]) > 1e-6
|
|
drop_ = drop[i, ix] + dropp[i] * gas.P
|
|
assert drop_ == approx(drop_num[i, ix], rel=self.rtol)
|
|
except AssertionError as err:
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(np.vstack([drop[i, ix], drop_num[i, ix]]).T)
|
|
raise err
|
|
|
|
def test_net_rop_ddX(self, gas):
|
|
"""
|
|
Check net rop against numerical derivative with respect to mole fractions
|
|
"""
|
|
drop = gas.net_rates_of_progress_ddX
|
|
dropp = gas.net_rates_of_progress_ddP
|
|
drop_num = self.rop_derivs(gas, mode="net")
|
|
stoich = gas.product_stoich_coeffs - gas.reactant_stoich_coeffs
|
|
for i in range(gas.n_reactions):
|
|
try:
|
|
# test entries that are not spurious
|
|
ix = np.abs((stoich[:, i] != 0) * drop[i, :]) > 1e-6
|
|
drop_ = drop[i, ix] + dropp[i] * gas.P
|
|
assert drop_ == approx(drop_num[i, ix], rel=self.rtol)
|
|
except AssertionError as err:
|
|
if gas.reaction(i).reversible:
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(np.vstack([drop[i, ix], drop_num[i, ix]]).T)
|
|
raise err
|
|
|
|
def test_forward_rop_ddCi(self, gas):
|
|
"""
|
|
Check forward rop against numerical derivative with respect to species
|
|
concentrations
|
|
"""
|
|
drop = gas.forward_rates_of_progress_ddCi
|
|
dropp = gas.forward_rates_of_progress_ddP
|
|
drop_num = self.rop_derivs(gas, mode="forward", ddX=False)
|
|
stoich = gas.reactant_stoich_coeffs
|
|
for i in range(gas.n_reactions):
|
|
try:
|
|
# test entries that are not spurious
|
|
ix = np.abs((stoich[:, i] != 0) * drop[i, :]) > 1e-6
|
|
drop_ = drop[i, ix] + dropp[i] * gas.P
|
|
assert drop_ == approx(drop_num[i, ix], rel=self.rtol)
|
|
except AssertionError as err:
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(np.vstack([drop[i, ix], drop_num[i, ix]]).T)
|
|
raise err
|
|
|
|
def test_reverse_rop_ddCi(self, gas):
|
|
"""
|
|
Check reverse rop against numerical derivative with respect to species
|
|
concentrations
|
|
"""
|
|
drop = gas.reverse_rates_of_progress_ddCi
|
|
dropp = gas.reverse_rates_of_progress_ddP
|
|
drop_num = self.rop_derivs(gas, mode="reverse", ddX=False)
|
|
stoich = gas.product_stoich_coeffs
|
|
for i in range(gas.n_reactions):
|
|
try:
|
|
# test entries that are not spurious
|
|
ix = np.abs((stoich[:, i] != 0) * drop[i, :]) > 1e-6
|
|
drop_ = drop[i, ix] + dropp[i] * gas.P
|
|
assert drop_ == approx(drop_num[i, ix], rel=self.rtol)
|
|
except AssertionError as err:
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(np.vstack([drop[i, ix], drop_num[i, ix]]).T)
|
|
raise err
|
|
|
|
def test_net_rop_ddCi(self, gas):
|
|
"""
|
|
Check net rop against numerical derivative with respect to species
|
|
concentrations
|
|
"""
|
|
drop = gas.net_rates_of_progress_ddCi
|
|
dropp = gas.net_rates_of_progress_ddP
|
|
drop_num = self.rop_derivs(gas, mode="net", ddX=False)
|
|
stoich = gas.product_stoich_coeffs - gas.reactant_stoich_coeffs
|
|
for i in range(gas.n_reactions):
|
|
try:
|
|
# test entries that are not spurious
|
|
ix = np.abs((stoich[:, i] != 0) * drop[i, :]) > 1e-6
|
|
drop_ = drop[i, ix] + dropp[i] * gas.P
|
|
assert drop_ == approx(drop_num[i, ix], rel=self.rtol)
|
|
except AssertionError as err:
|
|
if gas.reaction(i).reversible:
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(np.vstack([drop[i, ix], drop_num[i, ix]]).T)
|
|
raise err
|
|
|
|
def rop_ddT(self, gas, mode=None, dt=1e-6):
|
|
"""Numerical derivative for rates-of-progress at constant pressure"""
|
|
def calc():
|
|
if mode == "forward":
|
|
return gas.forward_rates_of_progress
|
|
if mode == "reverse":
|
|
return gas.reverse_rates_of_progress
|
|
if mode == "net":
|
|
return gas.net_rates_of_progress
|
|
return None
|
|
|
|
tpx = gas.TPX
|
|
gas.TP = tpx[0] + dt, tpx[1]
|
|
rop1 = calc()
|
|
gas.TP = tpx[:2]
|
|
rop0 = calc()
|
|
gas.TPX = tpx
|
|
return (rop1 - rop0) / dt
|
|
|
|
def test_forward_rop_ddT(self, gas):
|
|
"""
|
|
Check forward rop against numerical derivative with respect to temperature
|
|
"""
|
|
dcdt = - gas.density_mole / gas.T
|
|
drop = gas.forward_rates_of_progress_ddT
|
|
drop += gas.forward_rates_of_progress_ddC * dcdt
|
|
drop_num = self.rop_ddT(gas, mode="forward")
|
|
assert drop == approx(drop_num, rel=self.rtol)
|
|
|
|
def test_reverse_rop_ddT(self, gas):
|
|
"""
|
|
Check reverse rop against numerical derivative with respect to temperature
|
|
"""
|
|
dcdt = - gas.density_mole / gas.T
|
|
drop = gas.reverse_rates_of_progress_ddT
|
|
drop += gas.reverse_rates_of_progress_ddC * dcdt
|
|
drop_num = self.rop_ddT(gas, mode="reverse")
|
|
assert drop == approx(drop_num, rel=self.rtol)
|
|
|
|
def test_net_rop_ddT(self, gas):
|
|
"""
|
|
Check net rop against numerical derivative with respect to temperature
|
|
"""
|
|
dcdt = - gas.density_mole / gas.T
|
|
drop = gas.net_rates_of_progress_ddT
|
|
drop += gas.net_rates_of_progress_ddC * dcdt
|
|
drop_num = self.rop_ddT(gas, mode="net")
|
|
try:
|
|
assert drop == approx(drop_num, rel=self.rtol)
|
|
except AssertionError as err:
|
|
i = np.argmax(2 * (drop - drop_num) / (drop + drop_num + 2e-4))
|
|
print(i, gas.reaction(i).rate.type)
|
|
print(gas.reaction(i))
|
|
print(drop[i])
|
|
print(drop_num[i])
|
|
raise err
|
|
|
|
class TestFullHydrogenOxygen(FullTests):
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution("h2o2.yaml", transport_model=None)
|
|
gas.TPX = 300, 5 * ct.one_atm, "H2:1, O2:3"
|
|
return gas
|
|
|
|
class TestFullGriMech(FullTests):
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution("gri30.yaml", transport_model=None)
|
|
gas.TPX = 300, 5 * ct.one_atm, "CH4:1, C3H8:.1, O2:1, N2:3.76"
|
|
return gas
|
|
|
|
class TestFullEdgeCases(FullTests):
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution("jacobian-tests.yaml", transport_model=None)
|
|
# species: [H2, H, O, O2, OH, H2O, HO2, H2O2, AR]
|
|
gas.TPX = 300, 2 * ct.one_atm, "H2:1, O2:3, AR:0.4"
|
|
return gas
|
|
|
|
|
|
class SurfaceRateExpressionTests:
|
|
"""
|
|
Generic test class to check derivatives evaluated for a single reaction within
|
|
a reaction mechanism for surfaces.
|
|
"""
|
|
rtol = 1e-5 # Relative tolerance for approximate comparisons
|
|
|
|
@pytest.fixture(scope='class')
|
|
def all_species(self, gas, surf):
|
|
"""Combine species from both gas and surface phases."""
|
|
return surf.species() + gas.species()
|
|
|
|
@pytest.fixture(scope='class')
|
|
def sidxs(self, all_species):
|
|
"""Create a dictionary mapping species names to indices."""
|
|
return {spec.name: i for i, spec in enumerate(all_species)}
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn_data(self, surf, sidxs, rxn_idx):
|
|
"""
|
|
Retrieve reaction-specific data based on the reaction index.
|
|
Returns a dictionary containing reaction, reactant indices, and product indices.
|
|
"""
|
|
rxn = surf.reactions()[rxn_idx]
|
|
r_stoich = surf.reactant_stoich_coeffs
|
|
p_stoich = surf.product_stoich_coeffs
|
|
rix = [sidxs[k] for k in rxn.reactants.keys()]
|
|
pix = [sidxs[k] for k in rxn.products.keys()]
|
|
return {
|
|
"rxn": rxn,
|
|
"r_stoich": r_stoich,
|
|
"p_stoich": p_stoich,
|
|
"rix": rix,
|
|
"pix": pix
|
|
}
|
|
|
|
@pytest.fixture(scope='class')
|
|
def initial_conditions(self, gas, surf):
|
|
"""Store initial TPX conditions for gas and surface."""
|
|
return {
|
|
"gas_tpx": gas.TPX,
|
|
"surf_tpx": surf.TPX
|
|
}
|
|
|
|
@pytest.fixture(scope='function', autouse=True)
|
|
def setup_surface_rate_expression_data(self, gas, surf, initial_conditions, rxn_data, rxn_idx, sidxs):
|
|
"""
|
|
Reset TPX and derivative settings before each test and configure reaction multipliers.
|
|
Also verify stoichiometric coefficients.
|
|
"""
|
|
# Reset gas phase
|
|
gas.TPX = initial_conditions["gas_tpx"]
|
|
gas.set_multiplier(0)
|
|
gas.derivative_settings = {} # Reset to defaults
|
|
|
|
# Reset surface phase
|
|
surf.TPX = initial_conditions["surf_tpx"]
|
|
surf.set_multiplier(0.0)
|
|
surf.set_multiplier(1.0, rxn_idx) # Enable multiplier for specific reaction
|
|
surf.derivative_settings = {
|
|
"skip-coverage-dependence": True,
|
|
"skip-electrochemistry": True
|
|
}
|
|
|
|
# Retrieve reaction data
|
|
rxn = rxn_data["rxn"]
|
|
r_stoich = rxn_data["r_stoich"]
|
|
p_stoich = rxn_data["p_stoich"]
|
|
|
|
# Check stoichiometric coefficients for reactants
|
|
for k, v in rxn.reactants.items():
|
|
ix = sidxs[k]
|
|
actual = r_stoich[ix, rxn_idx]
|
|
assert actual == v, f"Reactant stoich mismatch for species '{k}'"
|
|
|
|
# Check stoichiometric coefficients for products
|
|
for k, v in rxn.products.items():
|
|
ix = sidxs[k]
|
|
actual = p_stoich[ix, rxn_idx]
|
|
assert actual == v, f"Product stoich mismatch for species '{k}'"
|
|
|
|
def get_concentrations(self, surf, gas):
|
|
"""Concatenate concentrations from surface and gas phases."""
|
|
return np.concatenate((surf.concentrations, gas.concentrations))
|
|
|
|
# Test methods
|
|
def test_input(self, rxn_data, equation, rate_type):
|
|
"""Ensure that the correct equation and rate type are referenced."""
|
|
rxn = rxn_data["rxn"]
|
|
assert equation == rxn.equation
|
|
assert rate_type == rxn.rate.type
|
|
|
|
def test_forward_rop_ddCi(self, surf, gas, rxn_data, orders, rxn_idx):
|
|
"""Test forward rates of progress derivatives."""
|
|
drop = surf.forward_rates_of_progress_ddCi
|
|
rop = surf.forward_rates_of_progress
|
|
concentrations = self.get_concentrations(surf, gas)
|
|
specs = surf.species_names + gas.species_names
|
|
|
|
rix = rxn_data["rix"]
|
|
r_stoich = rxn_data["r_stoich"]
|
|
|
|
for spc_ix in rix:
|
|
if orders is None:
|
|
order = r_stoich[spc_ix, rxn_idx]
|
|
else:
|
|
order = orders.get(specs[spc_ix], 1)
|
|
expected_rop = drop[rxn_idx, spc_ix] * concentrations[spc_ix] / order
|
|
assert rop[rxn_idx] == pytest.approx(expected_rop, rel=self.rtol), (
|
|
f"Forward ROP derivative mismatch for species index {spc_ix}"
|
|
)
|
|
|
|
def test_reverse_rop_ddCi(self, surf, gas, rxn_data, orders, rxn_idx):
|
|
"""Test reverse rates of progress derivatives."""
|
|
drop = surf.reverse_rates_of_progress_ddCi
|
|
rop = surf.reverse_rates_of_progress
|
|
concentrations = self.get_concentrations(surf, gas)
|
|
specs = surf.species_names + gas.species_names
|
|
|
|
pix = rxn_data["pix"]
|
|
p_stoich = rxn_data["p_stoich"]
|
|
|
|
for spc_ix in pix:
|
|
if orders is None:
|
|
order = p_stoich[spc_ix, rxn_idx]
|
|
else:
|
|
order = orders.get(specs[spc_ix], 1)
|
|
expected_rop = drop[rxn_idx, spc_ix] * concentrations[spc_ix] / order
|
|
assert rop[rxn_idx] == pytest.approx(expected_rop, rel=self.rtol), (
|
|
f"Reverse ROP derivative mismatch for species index {spc_ix}"
|
|
)
|
|
|
|
def test_net_rop_ddCi(self, surf, gas, rxn_data, rxn_idx, orders):
|
|
"""Test net rates of progress derivatives."""
|
|
rop = surf.net_rates_of_progress
|
|
drop = surf.net_rates_of_progress_ddCi
|
|
concentrations = self.get_concentrations(surf, gas)
|
|
|
|
drop_result = drop @ concentrations
|
|
|
|
# Adjust forward and reverse rates by reaction orders
|
|
ropf = surf.forward_rates_of_progress.copy()
|
|
ropr = surf.reverse_rates_of_progress.copy()
|
|
|
|
rxn = rxn_data["rxn"]
|
|
orders_rxn = rxn.orders
|
|
|
|
# Calculate total reactant orders
|
|
total_orders_fwd = sum(orders_rxn.get(k, v) for k, v in rxn.reactants.items())
|
|
ropf[rxn_idx] *= total_orders_fwd
|
|
|
|
# Calculate total product orders
|
|
total_orders_rev = sum(orders_rxn.get(k, v) for k, v in rxn.products.items())
|
|
ropr[rxn_idx] *= total_orders_rev
|
|
|
|
expected_net_rop = ropf - ropr
|
|
|
|
assert drop_result[rxn_idx] == pytest.approx(expected_net_rop[rxn_idx], rel=self.rtol), (
|
|
f"Net ROP derivative mismatch for reaction index {rxn_idx}"
|
|
)
|
|
|
|
|
|
class PlatinumHydrogen(SurfaceRateExpressionTests):
|
|
"""
|
|
Derived test class for Platinum-Hydrogen system.
|
|
Provides specific fixtures for gas and surface phases.
|
|
"""
|
|
|
|
phase_defs = """
|
|
units: {length: cm, quantity: mol, activation-energy: J/mol}
|
|
phases:
|
|
- name: gas
|
|
thermo: ideal-gas
|
|
species:
|
|
- gri30.yaml/species: [H2, H2O, H2O2, O2]
|
|
kinetics: gas
|
|
reactions:
|
|
- gri30.yaml/reactions: declared-species
|
|
skip-undeclared-third-bodies: true
|
|
- name: Pt_surf
|
|
thermo: ideal-surface
|
|
species:
|
|
- ptcombust.yaml/species: [PT(S), H(S), H2O(S), OH(S), O(S)]
|
|
kinetics: surface
|
|
reactions: [ptcombust.yaml/reactions: declared-species]
|
|
site-density: 3e-09
|
|
"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Create and configure the gas phase."""
|
|
gas = ct.Solution(yaml=self.phase_defs, name="gas")
|
|
gas.TPX = 800, 2 * ct.one_atm, "H2:1.5, O2:1.0, H2O2:0.75, H2O:0.3"
|
|
return gas
|
|
|
|
@pytest.fixture(scope='class')
|
|
def surf(self, gas):
|
|
"""Create and configure the surface phase."""
|
|
surf = ct.Interface(yaml=self.phase_defs, name="Pt_surf", adjacent=[gas])
|
|
surf.TPX = 800, 2 * ct.one_atm, "PT(S):4.0, H(S):0.5, H2O(S):0.1, OH(S):0.2, O(S):0.8"
|
|
return surf
|
|
|
|
class TestSurfInterfaceArrhenius(PlatinumHydrogen):
|
|
"""Test case for interface-Arrhenius reaction H(S) + O(S) <=> OH(S) + PT(S)."""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn_idx(self):
|
|
return 7
|
|
|
|
@pytest.fixture(scope='class')
|
|
def equation(self):
|
|
return "H(S) + O(S) <=> OH(S) + PT(S)"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rate_type(self):
|
|
return "interface-Arrhenius"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def orders(self):
|
|
return None
|
|
|
|
class TestSurfGasFwdStickingArrhenius(PlatinumHydrogen):
|
|
"""Test case for sticking-Arrhenius reaction H2O + PT(S) => H2O(S)."""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn_idx(self):
|
|
return 5
|
|
|
|
@pytest.fixture(scope='class')
|
|
def equation(self):
|
|
return "H2O + PT(S) => H2O(S)"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rate_type(self):
|
|
return "sticking-Arrhenius"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def orders(self):
|
|
return None
|
|
|
|
class TestSurfGasInterfaceArrhenius(PlatinumHydrogen):
|
|
"""Test case for interface-Arrhenius reaction H2 + 2 PT(S) => 2 H(S)."""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn_idx(self):
|
|
return 0
|
|
|
|
@pytest.fixture(scope='class')
|
|
def equation(self):
|
|
return "H2 + 2 PT(S) => 2 H(S)"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rate_type(self):
|
|
return "interface-Arrhenius"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def orders(self):
|
|
return {"PT(S)": 1, "H2": 1, "H(S)": 2}
|
|
|
|
class TestGasSurfInterfaceArrhenius(PlatinumHydrogen):
|
|
"""Test case for interface-Arrhenius reaction H2O(S) => H2O + PT(S)."""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rxn_idx(self):
|
|
return 6
|
|
|
|
@pytest.fixture(scope='class')
|
|
def equation(self):
|
|
return "H2O(S) => H2O + PT(S)"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def rate_type(self):
|
|
return "interface-Arrhenius"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def orders(self):
|
|
return None
|
|
|
|
|
|
class SurfaceFullTests:
|
|
# Generic test class to check derivatives evaluated for an entire reaction
|
|
# mechanisms
|
|
rtol = 1e-4
|
|
|
|
@pytest.fixture(scope='class')
|
|
def all_species(self, gas, surf):
|
|
"""Combine species from both gas and surface phases."""
|
|
return surf.species() + gas.species()
|
|
|
|
@pytest.fixture(scope='class')
|
|
def sidxs(self, all_species):
|
|
"""Create a dictionary mapping species names to indices."""
|
|
return {spec.name: i for i, spec in enumerate(all_species)}
|
|
|
|
@pytest.fixture(scope='class')
|
|
def initial_conditions(self, gas, surf):
|
|
"""Store initial TPX conditions for gas and surface."""
|
|
return {
|
|
"gas_tpx": gas.TPX,
|
|
"surf_tpx": surf.TPX
|
|
}
|
|
|
|
@pytest.fixture(scope='function', autouse=True)
|
|
def setup_surface_full_data(self, gas, surf, initial_conditions):
|
|
"""Reset TPX and derivative settings before each test."""
|
|
# Initialize gas phase
|
|
gas.TPX = initial_conditions["gas_tpx"]
|
|
gas.derivative_settings = {}
|
|
|
|
# Initialize surface phase
|
|
surf.TPX = initial_conditions["surf_tpx"]
|
|
surf.derivative_settings = {
|
|
"skip-coverage-dependence": True,
|
|
"skip-electrochemistry": True
|
|
}
|
|
|
|
def get_concentrations(self, surf, gas):
|
|
"""Concatenate concentrations from surface and gas phases."""
|
|
return np.concatenate((surf.concentrations, gas.concentrations))
|
|
|
|
def test_forward_rop_ddCi(self, gas, surf):
|
|
# matrix multiplication of the forward rates of progress derivatives w.r.t
|
|
# concentration and the concentrations should provide the rate of progress
|
|
# for each species and can be compared to the directly calculated rate
|
|
drop = surf.forward_rates_of_progress_ddCi
|
|
rop = surf.forward_rates_of_progress
|
|
conc = self.get_concentrations(surf, gas)
|
|
# multiply derivatives with concentrations
|
|
drop = drop @ conc
|
|
# get total reactant reaction orders
|
|
total_orders = []
|
|
for rxn in surf.reactions():
|
|
orders = rxn.orders
|
|
curr_order = 0
|
|
for k, v in rxn.reactants.items():
|
|
if k in orders:
|
|
curr_order += orders[k]
|
|
else:
|
|
curr_order += v
|
|
total_orders.append(curr_order)
|
|
total_orders = np.array(total_orders)
|
|
# rates of progress do not factor in reaction order it must be accounted for
|
|
drop /= total_orders
|
|
# compare the rate of progress vectors produced in different ways
|
|
assert drop == approx(rop, rel=self.rtol)
|
|
|
|
def test_reverse_rop_ddCi(self, gas, surf):
|
|
# matrix multiplication of the reverse rate of progress derivatives w.r.t
|
|
# concentration and the concentrations should provide the rate of progress
|
|
# for each species and can be compared to the directly calculated rate
|
|
drop = surf.reverse_rates_of_progress_ddCi
|
|
rop = surf.reverse_rates_of_progress
|
|
conc = self.get_concentrations(surf, gas)
|
|
# multiply derivatives with concentrations
|
|
drop = drop @ conc
|
|
# get total reactant reaction orders
|
|
total_orders = []
|
|
for rxn in surf.reactions():
|
|
orders = rxn.orders
|
|
curr_order = 0
|
|
for k, v in rxn.products.items():
|
|
if k in orders:
|
|
curr_order += orders[k]
|
|
else:
|
|
curr_order += v
|
|
total_orders.append(curr_order)
|
|
total_orders = np.array(total_orders)
|
|
# rates of progress do not factor in reaction order it must be accounted for
|
|
drop /= total_orders
|
|
# compare the rate of progress vectors produced in different ways
|
|
assert drop == approx(rop, rel=self.rtol)
|
|
|
|
def test_net_rop_ddCi(self, gas, surf):
|
|
# check derivatives of net rates of progress with respect to species
|
|
# concentrations against analytic
|
|
ropf = surf.forward_rates_of_progress
|
|
ropr = surf.reverse_rates_of_progress
|
|
drop = surf.net_rates_of_progress_ddCi
|
|
conc = self.get_concentrations(surf, gas)
|
|
# multiply derivatives with concentrations
|
|
drop = drop @ conc
|
|
# reaction orders are not yet accounted for in rates of progress
|
|
# so they must be included manually
|
|
for i, rxn in enumerate(surf.reactions()):
|
|
orders = rxn.orders
|
|
curr_order = 0
|
|
# adjust forward rates by reactant order
|
|
for k, v in rxn.reactants.items():
|
|
curr_order += orders[k] if k in orders else v
|
|
ropf[i] *= curr_order
|
|
curr_order = 0
|
|
# adjust reverse rates by product order
|
|
for k, v in rxn.products.items():
|
|
curr_order += orders[k] if k in orders else v
|
|
ropr[i] *= curr_order
|
|
# compare the rate of progress vectors produced in different ways
|
|
assert drop == approx(ropf - ropr, rel=self.rtol)
|
|
|
|
|
|
class TestFullPlatinumHydrogen(SurfaceFullTests):
|
|
|
|
phase_defs = """
|
|
units: {length: cm, quantity: mol, activation-energy: J/mol}
|
|
phases:
|
|
- name: gas
|
|
thermo: ideal-gas
|
|
species:
|
|
- gri30.yaml/species: [H2, H2O, H2O2, O2]
|
|
kinetics: gas
|
|
reactions:
|
|
- gri30.yaml/reactions: declared-species
|
|
skip-undeclared-third-bodies: true
|
|
- name: Pt_surf
|
|
thermo: ideal-surface
|
|
species:
|
|
- ptcombust.yaml/species: [PT(S), H(S), H2O(S), OH(S), O(S)]
|
|
kinetics: surface
|
|
reactions: [ptcombust.yaml/reactions: declared-species]
|
|
site-density: 3e-09
|
|
"""
|
|
|
|
@pytest.fixture(scope='class')
|
|
def gas(self):
|
|
"""Fixture to create and configure the gas phase."""
|
|
gas = ct.Solution(yaml=self.phase_defs, name="gas")
|
|
gas.TPX = 800, 2 * ct.one_atm, "H2:1.5, O2:1.0, H2O2:0.75, H2O:0.3"
|
|
return gas
|
|
|
|
@pytest.fixture(scope='class')
|
|
def surf(self, gas):
|
|
"""Fixture to create and configure the surface phase."""
|
|
surf = ct.Interface(yaml=self.phase_defs, name="Pt_surf", adjacent=[gas])
|
|
surf.TPX = 800, 2 * ct.one_atm, "PT(S):1.0, H(S):0.5, H2O(S):0.1, OH(S):0.2, O(S):0.8"
|
|
return surf
|