Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect unspecified thirdbody collision partners #1015

Merged
merged 7 commits into from
Apr 27, 2021
4 changes: 4 additions & 0 deletions include/cantera/kinetics/Reaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ class ThreeBodyReaction : public ElementaryReaction
//! Relative efficiencies of third-body species in enhancing the reaction
//! rate.
ThirdBody third_body;

bool specified_collision_partner = false; //!< Input specifies collision partner
};

//! A reaction that is first-order in [M] at low pressure, like a third-body
Expand Down Expand Up @@ -505,6 +507,8 @@ void parseReactionEquation(Reaction& R, const AnyValue& equation,
const Kinetics& kin);

// declarations of setup functions
void setupReaction(Reaction& R, const XML_Node& rxn_node);

void setupElementaryReaction(ElementaryReaction&, const XML_Node&);
//! @internal May be changed without notice in future versions
void setupElementaryReaction(ElementaryReaction&, const AnyMap&,
Expand Down
2 changes: 1 addition & 1 deletion interfaces/cython/cantera/test/test_kinetics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ def test_modify_invalid(self):
self.gas.modify_reaction(0, R2)

# different reactants
R = self.gas.reaction(7)
R = self.gas.reaction(4)
with self.assertRaisesRegex(ct.CanteraError, 'Reactants are different'):
self.gas.modify_reaction(23, R)

Expand Down
105 changes: 105 additions & 0 deletions interfaces/cython/cantera/test/test_reaction.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,114 @@
from math import exp
from pathlib import Path

import cantera as ct
from . import utilities


class TestImplicitThirdBody(utilities.CanteraTest):

@classmethod
def setUpClass(cls):
utilities.CanteraTest.setUpClass()
cls.gas = ct.Solution("gri30.yaml")

def test_implicit_three_body(self):
yaml1 = """
equation: H + 2 O2 <=> HO2 + O2
rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
"""
rxn1 = ct.Reaction.fromYaml(yaml1, self.gas)
self.assertEqual(rxn1.reaction_type, "three-body")
self.assertEqual(rxn1.default_efficiency, 0.)
self.assertEqual(rxn1.efficiencies, {"O2": 1})

yaml2 = """
equation: H + O2 + M <=> HO2 + M
rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
type: three-body
default-efficiency: 0
efficiencies: {O2: 1.0}
"""
rxn2 = ct.Reaction.fromYaml(yaml2, self.gas)
self.assertEqual(rxn1.efficiencies, rxn2.efficiencies)
self.assertEqual(rxn1.default_efficiency, rxn2.default_efficiency)

def test_duplicate(self):
# @todo simplify this test
# duplicates are currently only checked for import from file
gas1 = ct.Solution(thermo="IdealGas", kinetics="GasKinetics",
species=self.gas.species(), reactions=[])

yaml1 = """
equation: H + O2 + H2O <=> HO2 + H2O
rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0}
"""
rxn1 = ct.Reaction.fromYaml(yaml1, gas1)

yaml2 = """
equation: H + O2 + M <=> HO2 + M
rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0}
type: three-body
default-efficiency: 0
efficiencies: {H2O: 1}
"""
rxn2 = ct.Reaction.fromYaml(yaml2, gas1)

self.assertEqual(rxn1.reaction_type, rxn2.reaction_type)
self.assertEqual(rxn1.reactants, rxn2.reactants)
self.assertEqual(rxn1.products, rxn2.products)
self.assertEqual(rxn1.efficiencies, rxn2.efficiencies)
self.assertEqual(rxn1.default_efficiency, rxn2.default_efficiency)

gas1.add_reaction(rxn1)
gas1.add_reaction(rxn2)

fname = "duplicate.yaml"
gas1.write_yaml(fname)

with self.assertRaisesRegex(Exception, "Undeclared duplicate reactions"):
gas2 = ct.Solution(fname)

Path(fname).unlink()

def test_short_serialization(self):
yaml = """
equation: H + O2 + H2O <=> HO2 + H2O
rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0}
"""
rxn = ct.Reaction.fromYaml(yaml, self.gas)
input_data = rxn.input_data

self.assertNotIn("type", input_data)
self.assertNotIn("default-efficiency", input_data)
self.assertNotIn("efficiencies", input_data)

def test_non_integer_stoich(self):
yaml = """
equation: H + 1.5 O2 <=> HO2 + O2
rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
"""
rxn = ct.Reaction.fromYaml(yaml, self.gas)
self.assertEqual(rxn.reaction_type, "elementary")

def test_not_three_body(self):
yaml = """
equation: HCNO + H <=> H + HNCO # Reaction 270
rate-constant: {A: 2.1e+15, b: -0.69, Ea: 2850.0}
"""
rxn = ct.Reaction.fromYaml(yaml, self.gas)
self.assertEqual(rxn.reaction_type, "elementary")

def test_user_override(self):
yaml = """
equation: H + 2 O2 <=> HO2 + O2
rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
type: elementary
"""
rxn = ct.Reaction.fromYaml(yaml, self.gas)
self.assertEqual(rxn.reaction_type, "elementary")


class TestElementary(utilities.CanteraTest):

_cls = ct.ElementaryReaction
Expand Down
102 changes: 87 additions & 15 deletions src/kinetics/Reaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,28 +276,54 @@ ThreeBodyReaction::ThreeBodyReaction(const Composition& reactants_,
}

std::string ThreeBodyReaction::reactantString() const {
return ElementaryReaction::reactantString() + " + M";
if (specified_collision_partner) {
return ElementaryReaction::reactantString() + " + "
+ third_body.efficiencies.begin()->first;
} else {
return ElementaryReaction::reactantString() + " + M";
}
}

std::string ThreeBodyReaction::productString() const {
return ElementaryReaction::productString() + " + M";
if (specified_collision_partner) {
return ElementaryReaction::productString() + " + "
+ third_body.efficiencies.begin()->first;
} else {
return ElementaryReaction::productString() + " + M";
}
}

void ThreeBodyReaction::calculateRateCoeffUnits(const Kinetics& kin)
{
ElementaryReaction::calculateRateCoeffUnits(kin);
const ThermoPhase& rxn_phase = kin.thermo(kin.reactionPhaseIndex());
rate_units *= rxn_phase.standardConcentrationUnits().pow(-1);
bool specified_collision_partner_ = false;
for (const auto& reac : reactants) {
// While this reaction was already identified as a three-body reaction in a
// pre-processing step, this method is often called before a three-body
// reaction is fully instantiated. For the determination of the correct units,
// it is necessary to check whether the reaction uses a generic 'M' or an
// explicitly specified collision partner that may not have been deleted yet.
if (reac.first != "M" && products.count(reac.first)) {
// detected specified third-body collision partner
specified_collision_partner_ = true;
}
}
if (!specified_collision_partner_) {
const ThermoPhase& rxn_phase = kin.thermo(kin.reactionPhaseIndex());
rate_units *= rxn_phase.standardConcentrationUnits().pow(-1);
}
}

void ThreeBodyReaction::getParameters(AnyMap& reactionNode) const
{
ElementaryReaction::getParameters(reactionNode);
reactionNode["type"] = "three-body";
reactionNode["efficiencies"] = third_body.efficiencies;
reactionNode["efficiencies"].setFlowStyle();
if (third_body.default_efficiency != 1.0) {
reactionNode["default-efficiency"] = third_body.default_efficiency;
if (!specified_collision_partner) {
reactionNode["type"] = "three-body";
reactionNode["efficiencies"] = third_body.efficiencies;
reactionNode["efficiencies"].setFlowStyle();
if (third_body.default_efficiency != 1.0) {
reactionNode["default-efficiency"] = third_body.default_efficiency;
}
}
}

Expand Down Expand Up @@ -858,6 +884,46 @@ BlowersMasel readBlowersMasel(const Reaction& R, const AnyValue& rate,
return BlowersMasel(A, b, Ta0, w);
}

bool detectEfficiencies(ThreeBodyReaction& R)
{
for (const auto& reac : R.reactants) {
// detect explicitly specified collision partner
if (R.products.count(reac.first)) {
R.third_body.efficiencies[reac.first] = 1.;
}
}

if (R.third_body.efficiencies.size() == 0) {
return false;
} else if (R.third_body.efficiencies.size() > 1) {
throw CanteraError("detectEfficiencies",
"Found more than one explicitly specified collision partner\n"
"in reaction '{}'.", R.equation());
}

R.third_body.default_efficiency = 0.;
R.specified_collision_partner = true;
auto sp = R.third_body.efficiencies.begin();

// adjust reactant coefficients
auto reac = R.reactants.find(sp->first);
if (trunc(reac->second) != 1) {
reac->second -= 1.;
} else {
R.reactants.erase(reac);
}

// adjust product coefficients
auto prod = R.products.find(sp->first);
if (trunc(prod->second) != 1) {
prod->second -= 1.;
} else {
R.products.erase(prod);
}

return true;
}

void setupReaction(Reaction& R, const XML_Node& rxn_node)
{
// Reactant and product stoichiometries
Expand Down Expand Up @@ -1000,20 +1066,26 @@ void setupThreeBodyReaction(ThreeBodyReaction& R, const XML_Node& rxn_node)
{
readEfficiencies(R.third_body, rxn_node.child("rateCoeff"));
setupElementaryReaction(R, rxn_node);
if (R.third_body.efficiencies.size() == 0) {
detectEfficiencies(R);
}
}

void setupThreeBodyReaction(ThreeBodyReaction& R, const AnyMap& node,
const Kinetics& kin)
{
setupElementaryReaction(R, node, kin);
if (R.reactants.count("M") != 1 || R.products.count("M") != 1) {
throw InputFileError("setupThreeBodyReaction", node["equation"],
"Reaction equation '{}' does not contain third body 'M'",
node["equation"].asString());
if (!detectEfficiencies(R)) {
throw InputFileError("setupThreeBodyReaction", node["equation"],
"Reaction equation '{}' does not contain third body 'M'",
node["equation"].asString());
}
} else {
R.reactants.erase("M");
R.products.erase("M");
readEfficiencies(R.third_body, node);
}
R.reactants.erase("M");
R.products.erase("M");
readEfficiencies(R.third_body, node);
}

void setupFalloffReaction(FalloffReaction& R, const XML_Node& rxn_node)
Expand Down
56 changes: 56 additions & 0 deletions src/kinetics/ReactionFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,44 @@ ReactionFactory::ReactionFactory()
});
}

bool isThreeBody(const Reaction& R)
{
// detect explicitly specified collision partner
size_t found = 0;
for (const auto& reac : R.reactants) {
auto prod = R.products.find(reac.first);
if (prod != R.products.end() &&
trunc(reac.second) == reac.second && trunc(prod->second) == prod->second) {
// candidate species with integer stoichiometric coefficients on both sides
found += 1;
}
}
if (found != 1) {
return false;
}

// ensure that all reactants have integer stoichiometric coefficients
size_t nreac = 0;
for (const auto& reac : R.reactants) {
if (trunc(reac.second) != reac.second) {
return false;
}
nreac += reac.second;
}

// ensure that all products have integer stoichiometric coefficients
size_t nprod = 0;
for (const auto& prod : R.products) {
if (trunc(prod.second) != prod.second) {
return false;
}
nprod += prod.second;
}

// either reactant or product side involves exactly three species
return (nreac == 3) || (nprod == 3);
}

bool isElectrochemicalReaction(Reaction& R, const Kinetics& kin)
{
vector_fp e_counter(kin.nPhases(), 0.0);
Expand Down Expand Up @@ -212,6 +250,16 @@ unique_ptr<Reaction> newReaction(const XML_Node& rxn_node)
throw CanteraError("newReaction",
"Unknown reaction type '" + rxn_node["type"] + "'");
}
if (type.empty()) {
// Reaction type is not specified
// See if this is a three-body reaction with a specified collision partner
ElementaryReaction testReaction;
setupReaction(testReaction, rxn_node);
if (isThreeBody(testReaction)) {
type = "three-body";
R = ReactionFactory::factory()->create(type);
}
}
if (type != "electrochemical") {
type = R->type();
}
Expand All @@ -226,6 +274,14 @@ unique_ptr<Reaction> newReaction(const AnyMap& rxn_node,
std::string type = "elementary";
if (rxn_node.hasKey("type")) {
type = rxn_node["type"].asString();
} else if (kin.thermo().nDim() == 3) {
// Reaction type is not specified
// See if this is a three-body reaction with a specified collision partner
ElementaryReaction testReaction;
parseReactionEquation(testReaction, rxn_node["equation"], kin);
if (isThreeBody(testReaction)) {
type = "three-body";
}
}

if (kin.thermo().nDim() < 3 && type == "elementary") {
Expand Down
Loading