diff --git a/include/cantera/kinetics/MultiRate.h b/include/cantera/kinetics/MultiRate.h index 0bdb00c596..6d4d822a1f 100644 --- a/include/cantera/kinetics/MultiRate.h +++ b/include/cantera/kinetics/MultiRate.h @@ -61,7 +61,7 @@ class MultiRateBase //! A class template handling all reaction rates specific to `BulkKinetics`. template -class MultiBulkRates final : public MultiRateBase +class MultiBulkRate final : public MultiRateBase { public: virtual void add(const size_t rxn_index, diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index 84f6511c0b..79ffe1206e 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -142,9 +142,7 @@ class Reaction } //! Set reaction rate pointer - void setRate(shared_ptr rate) { - m_rate = rate; - } + void setRate(shared_ptr rate); //! Get pointer to third-body shared_ptr thirdBody() { @@ -497,7 +495,6 @@ class ElementaryReaction3 : public Reaction virtual std::string type() const { return "elementary"; } - virtual void getParameters(AnyMap& reactionNode) const; }; @@ -542,7 +539,6 @@ class PlogReaction3 : public Reaction virtual std::string type() const { return "pressure-dependent-Arrhenius"; } - virtual void getParameters(AnyMap& reactionNode) const; }; @@ -560,8 +556,6 @@ class ChebyshevReaction3 : public Reaction virtual std::string type() const { return "Chebyshev"; } - - virtual void getParameters(AnyMap& reactionNode) const; }; diff --git a/include/cantera/kinetics/ReactionRate.h b/include/cantera/kinetics/ReactionRate.h index 53448d84cd..13034dac97 100644 --- a/include/cantera/kinetics/ReactionRate.h +++ b/include/cantera/kinetics/ReactionRate.h @@ -21,6 +21,7 @@ namespace Cantera { class Func1; +class MultiRateBase; //! Abstract base class for reaction rate definitions /** @@ -43,6 +44,9 @@ class ReactionRateBase //! Identifier of reaction type virtual std::string type() const = 0; + //! Create multi-rate evaluator + virtual unique_ptr newMultiRate() const = 0; + //! Update reaction rate data based on temperature //! @param T temperature [K] virtual void update(double T) = 0; @@ -104,6 +108,10 @@ class ReactionRateBase //! @param rate_units Description of units used for rate parameters virtual void setParameters(const AnyMap& node, const Units& rate_units); + //! Set rate units + //! @param rate_units Description of units used for rate parameters + virtual void setUnits(const Units& rate_units); + protected: //! Get parameters //! Store the parameters of a ReactionRate needed to reconstruct an identical @@ -250,7 +258,9 @@ class ArrheniusRate final : public ReactionRate, public Arrhenius virtual void getParameters(AnyMap& rateNode, const Units& rate_units) const override; - virtual std::string type() const override { return "ArrheniusRate"; } + virtual std::string type() const override { return "Arrhenius"; } + + virtual unique_ptr newMultiRate() const override; //! Update information specific to reaction static bool usesUpdate() { return false; } @@ -312,7 +322,12 @@ class PlogRate final : public ReactionRate, public Plog //! @param node AnyMap containing rate information PlogRate(const AnyMap& node); - virtual std::string type() const override { return "PlogRate"; } + virtual std::string type() const override + { + return "pressure-dependent-Arrhenius"; + } + + virtual unique_ptr newMultiRate() const override; virtual void setParameters(const AnyMap& node, const Units& rate_units) override; virtual void getParameters(AnyMap& rateNode, @@ -393,7 +408,9 @@ class ChebyshevRate3 final : public ReactionRate, public Chebyshe //! @param node AnyMap containing rate information ChebyshevRate3(const AnyMap& node); - virtual std::string type() const override { return "ChebyshevRate"; } + virtual std::string type() const override { return "Chebyshev"; } + + virtual unique_ptr newMultiRate() const override; virtual void setParameters(const AnyMap& node, const Units& rate_units) override; virtual void getParameters(AnyMap& rateNode, @@ -433,7 +450,9 @@ class CustomFunc1Rate final : public ReactionRate //! @param rate_units Description of units used for rate parameters CustomFunc1Rate(const AnyMap& rate, const Units& rate_units) {} - virtual std::string type() const override { return "custom-function"; } + virtual std::string type() const override { return "custom-rate-function"; } + + virtual unique_ptr newMultiRate() const override; virtual void setParameters(const AnyMap& node, const Units& rate_units) override { units = rate_units; diff --git a/include/cantera/kinetics/ReactionRateFactory.h b/include/cantera/kinetics/ReactionRateFactory.h new file mode 100644 index 0000000000..fddb7dbe45 --- /dev/null +++ b/include/cantera/kinetics/ReactionRateFactory.h @@ -0,0 +1,88 @@ +/** + * @file ReactionRateFactory.h + * Factory class for reaction rate objects. Used by classes that implement kinetics + * (see \ref reactionGroup and class \link Cantera::Rate Rate\endlink). + */ + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://cantera.org/license.txt for license and copyright information. + +#ifndef CT_NEWRATE_H +#define CT_NEWRATE_H + +#include "cantera/base/FactoryBase.h" +#include "cantera/kinetics/ReactionRate.h" + +namespace Cantera +{ + +class Kinetics; +class Units; + +/** + * Factory class to construct reaction rate calculators. + * The reaction factory is accessed through the static method factory: + * + * @code + * Rate* f = ReactionRateFactory::factory()->newReactionRate(type, c) + * @endcode + * + * @ingroup reactionGroup + */ +class ReactionRateFactory + : public Factory +{ +public: + /** + * Return a pointer to the factory. On the first call, a new instance is + * created. Since there is no need to instantiate more than one factory, + * on all subsequent calls, a pointer to the existing factory is returned. + */ + static ReactionRateFactory* factory() { + std::unique_lock lock(rate_mutex); + if (!s_factory) { + s_factory = new ReactionRateFactory; + } + return s_factory; + } + + virtual void deleteFactory() { + std::unique_lock lock(rate_mutex); + delete s_factory; + s_factory = 0; + } + +private: + //! Pointer to the single instance of the factory + static ReactionRateFactory* s_factory; + + //! default constructor, which is defined as private + ReactionRateFactory(); + + //! Mutex for use when calling the factory + static std::mutex rate_mutex; +}; + + +//! Create a new empty ReactionRateBase object +/*! + * @param type string identifying type of reaction rate. + */ +shared_ptr newReactionRate(const std::string& type); + +//! Create a new Rate object using the specified parameters +/*! + * @param rate_node AnyMap node describing reaction rate. + * @param rate_units Unit system of the reaction rate + */ +shared_ptr newReactionRate( + const AnyMap& rate_node, const Units& rate_units); + +//! Create a new Rate object using the specified parameters +/*! + * @param rate_node AnyMap node describing reaction rate. + */ +shared_ptr newReactionRate(const AnyMap& rate_node); + +} +#endif diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 4e45b52719..e04149791d 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -341,6 +341,10 @@ cdef extern from "cantera/kinetics/ReactionFactory.h" namespace "Cantera": cdef shared_ptr[CxxReaction] CxxNewReaction "newReaction" (XML_Node&) except +translate_exception cdef shared_ptr[CxxReaction] CxxNewReaction "newReaction" (CxxAnyMap&, CxxKinetics&) except +translate_exception +cdef extern from "cantera/kinetics/ReactionRateFactory.h" namespace "Cantera": + cdef shared_ptr[CxxReactionRateBase] CxxNewReactionRate "newReactionRate" (string) except +translate_exception + cdef shared_ptr[CxxReactionRateBase] CxxNewReactionRate "newReactionRate" (CxxAnyMap&) except +translate_exception + cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera": cdef vector[shared_ptr[CxxReaction]] CxxGetReactions "getReactions" (XML_Node&) except +translate_exception cdef vector[shared_ptr[CxxReaction]] CxxGetReactions "getReactions" (CxxAnyValue&, CxxKinetics&) except +translate_exception @@ -1146,27 +1150,14 @@ cdef class ThermoPhase(_SolutionBase): cdef class InterfacePhase(ThermoPhase): cdef CxxSurfPhase* surf -cdef class _ReactionRate: - cdef shared_ptr[CxxReactionRateBase] _base - cdef CxxReactionRateBase* base - -cdef class ArrheniusRate(_ReactionRate): - cdef CxxArrheniusRate* rate - @staticmethod - cdef wrap(shared_ptr[CxxReactionRateBase]) - -cdef class PlogRate(_ReactionRate): - cdef CxxPlogRate* rate - @staticmethod - cdef wrap(shared_ptr[CxxReactionRateBase]) - -cdef class ChebyshevRate(_ReactionRate): - cdef CxxChebyshevRate3* rate +cdef class ReactionRate: + cdef shared_ptr[CxxReactionRateBase] _rate + cdef CxxReactionRateBase* rate @staticmethod cdef wrap(shared_ptr[CxxReactionRateBase]) -cdef class CustomRate(_ReactionRate): - cdef CxxCustomFunc1Rate* rate +cdef class CustomRate(ReactionRate): + cdef CxxCustomFunc1Rate* cxx_object(self) cdef Func1 _rate_func cdef class Reaction: diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index cd025c8587..2112a32190 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -6,16 +6,21 @@ cdef dict _reaction_class_registry = {} -cdef class _ReactionRate: +# dictionary to store reaction rate classes +cdef dict _reaction_rate_class_registry = {} + + +cdef class ReactionRate: """ Base class for ReactionRate objects. ReactionRate objects are used to calculate reaction rates and are associated with a Reaction object. """ + _reaction_rate_type = "" def __repr__(self): - return f"<{pystr(self.base.type())} at {id(self):0x}>" + return f"<{type(self).__name__} at {id(self):0x}>" def __call__(self, double temperature, pressure=None): """ @@ -24,32 +29,108 @@ cdef class _ReactionRate: will raise an exception. """ if pressure: - self.base.update(temperature, pressure) - return self.base.eval(temperature, pressure) + self.rate.update(temperature, pressure) + return self.rate.eval(temperature, pressure) else: - self.base.update(temperature) - return self.base.eval(temperature) + self.rate.update(temperature) + return self.rate.eval(temperature) + + @staticmethod + cdef wrap(shared_ptr[CxxReactionRateBase] rate): + """ + Wrap a C++ Reaction object with a Python object of the correct derived type. + """ + # ensure all reaction types are registered + if not _reaction_rate_class_registry: + def register_subclasses(cls): + for c in cls.__subclasses__(): + rate_type = getattr(c, "_reaction_rate_type") + _reaction_rate_class_registry[rate_type] = c + register_subclasses(c) + + # update global reaction class registry + register_subclasses(ReactionRate) + + # identify class + rate_type = pystr(rate.get().type()) + cls = _reaction_rate_class_registry.get(rate_type, ReactionRate) + + # wrap C++ reaction rate + cdef ReactionRate rr + rr = cls(init=False) + rr._rate = rate + rr.rate = rr._rate.get() + return rr + + @classmethod + def from_dict(cls, data): + """ + Create a `ReactionRate` object from a dictionary corresponding to its YAML + representation. + + An example for the creation of a `ReactionRate` from a dictionary is:: + + rate = ReactionRate.from_dict( + {"rate-constant": {"A": 38.7, "b": 2.7, "Ea": 26191840.0}}) + + :param data: + A dictionary corresponding to the YAML representation. + """ + if cls._reaction_rate_type != "": + raise TypeError( + f"Class method 'from_dict' was invoked from '{cls.__name__}' but " + "should be called from base class 'ReactionRate'") + + cdef CxxAnyMap any_map = dict_to_anymap(data) + cxx_rate = CxxNewReactionRate(any_map) + return ReactionRate.wrap(cxx_rate) + + @classmethod + def from_yaml(cls, text): + """ + Create a `ReactionRate` object from its YAML string representation. + + An example for the creation of a `ReactionRate` from a YAML string is:: + + rate = ReactionRate.from_yaml( + "rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol}") + + Units for ``A`` require a unit system with length in ``m`` and quantity in + ``kmol`` (standard Cantera units). + + :param text: + The YAML reaction rate string. + """ + if cls._reaction_rate_type != "": + raise TypeError( + f"Class method 'from_yaml' was invoked from '{cls.__name__}' but " + "should be called from base class 'ReactionRate'") + + cdef CxxAnyMap any_map + any_map = AnyMapFromYamlString(stringify(text)) + cxx_rate = CxxNewReactionRate(any_map) + return ReactionRate.wrap(cxx_rate) def ddT(self, double temperature, pressure=None): """ Evaluate derivative of rate expression with respect to temperature. """ if pressure: - self.base.update(temperature, pressure) - return self.base.ddT(temperature, pressure) + self.rate.update(temperature, pressure) + return self.rate.ddT(temperature, pressure) else: - self.base.update(temperature) - return self.base.ddT(temperature) + self.rate.update(temperature) + return self.rate.ddT(temperature) property input_data: """ Get input data for this reaction rate with its current parameter values. """ def __get__(self): - return anymap_to_dict(self.base.parameters()) + return anymap_to_dict(self.rate.parameters()) -cdef class ArrheniusRate(_ReactionRate): +cdef class ArrheniusRate(ReactionRate): r""" A reaction rate coefficient which depends on temperature only and follows the modified Arrhenius form: @@ -58,59 +139,50 @@ cdef class ArrheniusRate(_ReactionRate): k_f = A T^b \exp{-\tfrac{E}{RT}} - where *A* is the `pre_exponential_factor`, *b* is the `temperature_exponent`, - and *Ea* is the `activation_energy`. + where ``A`` is the `pre_exponential_factor`, ``b`` is the `temperature_exponent`, + and ``Ea`` is the `activation_energy`. """ + _reaction_rate_type = "Arrhenius" + def __cinit__(self, A=None, b=None, Ea=None, input_data=None, init=True): if init: if isinstance(input_data, dict): - self._base.reset(new CxxArrheniusRate(dict_to_anymap(input_data))) + self._rate.reset(new CxxArrheniusRate(dict_to_anymap(input_data))) elif all([arg is not None for arg in [A, b, Ea]]): - self._base.reset(new CxxArrheniusRate(A, b, Ea)) + self._rate.reset(new CxxArrheniusRate(A, b, Ea)) elif all([arg is None for arg in [A, b, Ea, input_data]]): - self._base.reset(new CxxArrheniusRate(dict_to_anymap({}))) + self._rate.reset(new CxxArrheniusRate(dict_to_anymap({}))) elif input_data: raise TypeError("Invalid parameter 'input_data'") else: raise TypeError("Invalid parameters 'A', 'b' or 'Ea'") - self.base = self._base.get() - self.rate = (self.base) + self.rate = self._rate.get() - @staticmethod - cdef wrap(shared_ptr[CxxReactionRateBase] rate): - """ - Wrap a C++ ReactionRateBase object with a Python object. - """ - # wrap C++ reaction - cdef ArrheniusRate arr - arr = ArrheniusRate(init=False) - arr._base = rate - arr.base = arr._base.get() - arr.rate = (arr.base) - return arr + cdef CxxArrheniusRate* cxx_object(self): + return self.rate property pre_exponential_factor: """ - The pre-exponential factor *A* in units of m, kmol, and s raised to + The pre-exponential factor ``A`` in units of m, kmol, and s raised to powers depending on the reaction order. """ def __get__(self): - return self.rate.preExponentialFactor() + return self.cxx_object().preExponentialFactor() property temperature_exponent: """ - The temperature exponent *b*. + The temperature exponent ``b``. """ def __get__(self): - return self.rate.temperatureExponent() + return self.cxx_object().temperatureExponent() property activation_energy: """ - The activation energy *E* [J/kmol]. + The activation energy ``E`` [J/kmol]. """ def __get__(self): - return self.rate.activationEnergy() + return self.cxx_object().activationEnergy() property allow_negative_pre_exponential_factor: """ @@ -118,16 +190,18 @@ cdef class ArrheniusRate(_ReactionRate): pre-exponential factor. """ def __get__(self): - return self.rate.allow_negative_pre_exponential_factor + return self.cxx_object().allow_negative_pre_exponential_factor def __set__(self, allow): - self.rate.allow_negative_pre_exponential_factor = allow + self.cxx_object().allow_negative_pre_exponential_factor = allow -cdef class PlogRate(_ReactionRate): +cdef class PlogRate(ReactionRate): r""" A pressure-dependent reaction rate parameterized by logarithmically interpolating between Arrhenius rate expressions at various pressures. """ + _reaction_rate_type = "pressure-dependent-Arrhenius" + def __cinit__(self, rates=None, input_data=None, init=True): if init and isinstance(rates, list): @@ -135,28 +209,17 @@ cdef class PlogRate(_ReactionRate): elif init: if isinstance(input_data, dict): - self._base.reset(new CxxPlogRate(dict_to_anymap(input_data))) + self._rate.reset(new CxxPlogRate(dict_to_anymap(input_data))) elif rates is None: - self._base.reset(new CxxPlogRate(dict_to_anymap({}))) + self._rate.reset(new CxxPlogRate(dict_to_anymap({}))) elif input_data: raise TypeError("Invalid parameter 'input_data'") else: raise TypeError("Invalid parameter 'rates'") - self.base = self._base.get() - self.rate = (self.base) + self.rate = self._rate.get() - @staticmethod - cdef wrap(shared_ptr[CxxReactionRateBase] rate): - """ - Wrap a C++ ReactionRateBase object with a Python object. - """ - # wrap C++ reaction - cdef PlogRate arr - arr = PlogRate(init=False) - arr._base = rate - arr.base = arr._base.get() - arr.rate = (arr.base) - return arr + cdef CxxPlogRate* cxx_object(self): + return self.rate property rates: """ @@ -165,8 +228,9 @@ cdef class PlogRate(_ReactionRate): """ def __get__(self): rates = [] - cdef vector[pair[double, CxxArrhenius]] cxxrates = self.rate.rates() + cdef vector[pair[double, CxxArrhenius]] cxxrates cdef pair[double, CxxArrhenius] p_rate + cxxrates = self.cxx_object().rates() for p_rate in cxxrates: rates.append((p_rate.first, copyArrhenius(&p_rate.second))) return rates @@ -180,34 +244,34 @@ cdef class PlogRate(_ReactionRate): item.second = deref(rate.rate) ratemap.insert(item) - self._base.reset(new CxxPlogRate(ratemap)) - self.base = self._base.get() - self.rate = (self.base) + self._rate.reset(new CxxPlogRate(ratemap)) + self.rate = self._rate.get() -cdef class ChebyshevRate(_ReactionRate): +cdef class ChebyshevRate(ReactionRate): r""" A pressure-dependent reaction rate parameterized by a bivariate Chebyshev polynomial in temperature and pressure. """ + _reaction_rate_type = "Chebyshev" + def __cinit__(self, Tmin=None, Tmax=None, Pmin=None, Pmax=None, data=None, input_data=None, init=True): if init: if isinstance(input_data, dict): - self._base.reset(new CxxChebyshevRate3(dict_to_anymap(input_data))) + self._rate.reset(new CxxChebyshevRate3(dict_to_anymap(input_data))) elif all([arg is not None for arg in [Tmin, Tmax, Pmin, Pmax, data]]): self._setup(Tmin, Tmax, Pmin, Pmax, data) return elif all([arg is None for arg in [Tmin, Tmax, Pmin, Pmax, data, input_data]]): - self._base.reset(new CxxChebyshevRate3(dict_to_anymap({}))) + self._rate.reset(new CxxChebyshevRate3(dict_to_anymap({}))) elif input_data: raise TypeError("Invalid parameter 'input_data'") else: raise TypeError("Invalid parameters") - self.base = self._base.get() - self.rate = (self.base) + self.rate = self._rate.get() def _setup(self, Tmin, Tmax, Pmin, Pmax, coeffs): """ @@ -223,63 +287,53 @@ cdef class ChebyshevRate(_ReactionRate): for j,value in enumerate(row): CxxArray2D_set(data, i, j, value) - self._base.reset(new CxxChebyshevRate3(Tmin, Tmax, Pmin, Pmax, data)) - self.base = self._base.get() - self.rate = (self.base) + self._rate.reset(new CxxChebyshevRate3(Tmin, Tmax, Pmin, Pmax, data)) + self.rate = self._rate.get() - @staticmethod - cdef wrap(shared_ptr[CxxReactionRateBase] rate): - """ - Wrap a C++ ReactionRateBase object with a Python object. - """ - # wrap C++ reaction - cdef ChebyshevRate arr - arr = ChebyshevRate(init=False) - arr._base = rate - arr.base = arr._base.get() - arr.rate = (arr.base) - return arr + cdef CxxChebyshevRate3* cxx_object(self): + return self.rate property Tmin: """ Minimum temperature [K] for the Chebyshev fit """ def __get__(self): - return self.rate.Tmin() + return self.cxx_object().Tmin() property Tmax: """ Maximum temperature [K] for the Chebyshev fit """ def __get__(self): - return self.rate.Tmax() + return self.cxx_object().Tmax() property Pmin: """ Minimum pressure [Pa] for the Chebyshev fit """ def __get__(self): - return self.rate.Pmin() + return self.cxx_object().Pmin() property Pmax: """ Maximum pressure [Pa] for the Chebyshev fit """ def __get__(self): - return self.rate.Pmax() + return self.cxx_object().Pmax() property n_pressure: """ Number of pressures over which the Chebyshev fit is computed """ def __get__(self): - return self.rate.nPressure() + return self.cxx_object().nPressure() property n_temperature: """ Number of temperatures over which the Chebyshev fit is computed """ def __get__(self): - return self.rate.nTemperature() + return self.cxx_object().nTemperature() property coeffs: """ 2D array of Chebyshev coefficients of size `(n_temperature, n_pressure)`. """ def __get__(self): - c = np.fromiter(self.rate.coeffs(), np.double) - return c.reshape((self.rate.nTemperature(), self.rate.nPressure())) + c = np.fromiter(self.cxx_object().coeffs(), np.double) + return c.reshape( + (self.cxx_object().nTemperature(), self.cxx_object().nPressure())) -cdef class CustomRate(_ReactionRate): +cdef class CustomRate(ReactionRate): r""" A custom rate coefficient which depends on temperature only. @@ -287,15 +341,22 @@ cdef class CustomRate(_ReactionRate): for example:: rr = CustomRate(lambda T: 38.7 * T**2.7 * exp(-3150.15/T)) + + Warning: this class is an experimental part of the Cantera API and + may be changed or removed without notice. """ + _reaction_rate_type = "custom-rate-function" + def __cinit__(self, k=None, init=True): if init: - self._base.reset(new CxxCustomFunc1Rate()) - self.base = self._base.get() - self.rate = (self.base) + self._rate.reset(new CxxCustomFunc1Rate()) + self.rate = self._rate.get() self.set_rate_function(k) + cdef CxxCustomFunc1Rate* cxx_object(self): + return self.rate + def set_rate_function(self, k): r""" Set the function describing a custom reaction rate:: @@ -312,7 +373,7 @@ cdef class CustomRate(_ReactionRate): else: self._rate_func = Func1(k) - self.rate.setRateFunction(self._rate_func._func) + self.cxx_object().setRateFunction(self._rate_func._func) cdef class Reaction: @@ -325,7 +386,7 @@ cdef class Reaction: :param products: Value used to set `products` - The static methods `listFromFile`, `listFromYaml`, `listFromCti`, and + The static methods `listFromFile`, `list_from_yaml`, `listFromCti`, and `listFromXml` can be used to create lists of `Reaction` objects from existing definitions in the YAML, CTI, or XML formats. All of the following will produce a list of the 325 reactions which make up the GRI 3.0 @@ -338,7 +399,7 @@ cdef class Reaction: where `gas` is a `Solution` object with the appropriate thermodynamic model, which is the `ideal-gas` model in this case. - The static method `listFromYaml` can be used to create lists of `Reaction` + The static method `list_from_yaml` can be used to create lists of `Reaction` objects from a YAML list:: rxns = ''' @@ -347,15 +408,15 @@ cdef class Reaction: - equation: O + HO2 <=> OH + O2 rate-constant: {A: 2.0e+13, b: 0.0, Ea: 0.0} ''' - R = ct.Reaction.listFromYaml(rxns, gas) + R = ct.Reaction.list_from_yaml(rxns, gas) - The methods `fromYaml`, `fromCti`, and `fromXml` can be used to create + The methods `from_yaml`, `fromCti`, and `fromXml` can be used to create individual `Reaction` objects from definitions in these formats. In the case of using YAML or CTI definitions, it is important to verify that either the pre-exponential factor and activation energy are supplied in SI units, or that they have their units specified:: - R = ct.Reaction.fromYaml('''{equation: O + H2 <=> H + OH, + R = ct.Reaction.from_yaml('''{equation: O + H2 <=> H + OH, rate-constant: {A: 3.87e+04 cm^3/mol/s, b: 2.7, Ea: 6260 cal/mol}}''', gas) @@ -388,7 +449,7 @@ cdef class Reaction: Wrap a C++ Reaction object with a Python object of the correct derived type. """ # ensure all reaction types are registered - if not(_reaction_class_registry): + if not _reaction_class_registry: def register_subclasses(cls): for c in cls.__subclasses__(): rxn_type = getattr(c, "_reaction_type") @@ -440,7 +501,7 @@ cdef class Reaction: return Reaction.wrap(cxx_reaction) @classmethod - def from_dict(cls, data, Kinetics kinetics=None): + def from_dict(cls, data, Kinetics kinetics): """ Create a `Reaction` object from a dictionary corresponding to its YAML representation. @@ -452,7 +513,7 @@ cdef class Reaction: "rate-constant": {"A": 38.7, "b": 2.7, "Ea": 26191840.0}}, kinetics=gas) - In the example, *gas* is a Kinetics (or Solution) object. + In the example, ``gas`` is a Kinetics (or Solution) object. :param data: A dictionary corresponding to the YAML representation. @@ -462,10 +523,8 @@ cdef class Reaction: """ if cls._reaction_type != "": raise TypeError( - "Class method 'from_dict' was invoked from '{}' but should " - "be called from base class 'Reaction'".format(cls.__name__)) - if kinetics is None: - raise ValueError("A Kinetics object is required.") + f"Class method 'from_dict' was invoked from '{cls.__name__}' but " + "should be called from base class 'Reaction'") cdef CxxAnyMap any_map = dict_to_anymap(data) cxx_reaction = CxxNewReaction(any_map, deref(kinetics.kinetics)) @@ -476,14 +535,28 @@ cdef class Reaction: """ Create a `Reaction` object from its YAML string representation. + .. deprecated:: 2.6 + To be deprecated with version 2.6, and removed thereafter. + Replaced by `Reaction.from_yaml`. + """ + warnings.warn("Class method 'fromYaml' is renamed to 'from_yaml' " + "and will be removed after Cantera 2.6.", DeprecationWarning) + + return cls.from_yaml(text, kinetics) + + @classmethod + def from_yaml(cls, text, Kinetics kinetics): + """ + Create a `Reaction` object from its YAML string representation. + An example for the creation of a Reaction from a YAML string is:: - rxn = Reaction.fromYaml(''' + rxn = Reaction.from_yaml(''' equation: O + H2 <=> H + OH rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol} ''', kinetics=gas) - In the example, *gas* is a Kinetics (or Solution) object. + In the example, ``gas`` is a Kinetics (or Solution) object. :param text: The YAML reaction string. @@ -493,10 +566,8 @@ cdef class Reaction: """ if cls._reaction_type != "": raise TypeError( - "Class method 'fromYaml' was invoked from '{}' but should " - "be called from base class 'Reaction'".format(cls.__name__)) - if kinetics is None: - raise ValueError("A Kinetics object is required.") + f"Class method 'from_yaml' was invoked from '{cls.__name__}' but " + "should be called from base class 'Reaction'") cdef CxxAnyMap any_map any_map = AnyMapFromYamlString(stringify(text)) @@ -510,7 +581,7 @@ cdef class Reaction: YAML, CTI, or XML file. For YAML input files, a `Kinetics` object is required as the second - argument, and reactions from the section *section* will be returned. + argument, and reactions from the section ``section`` will be returned. Directories on Cantera's input file path will be searched for the specified file. @@ -569,6 +640,21 @@ cdef class Reaction: """ Create a list of `Reaction` objects from all the reactions defined in a YAML string. + + .. deprecated:: 2.6 + To be deprecated with version 2.6, and removed thereafter. + Replaced by `Reaction.list_from_yaml`. + """ + warnings.warn("Class method 'listFromYaml' is renamed to 'list_from_yaml' " + "and will be removed after Cantera 2.6.", DeprecationWarning) + + return Reaction.list_from_yaml(text, kinetics) + + @staticmethod + def list_from_yaml(text, Kinetics kinetics): + """ + Create a list of `Reaction` objects from all the reactions defined in a + YAML string. """ root = AnyMapFromYamlString(stringify(text)) cxx_reactions = CxxGetReactions(root[stringify("items")], @@ -747,8 +833,8 @@ cdef class Arrhenius: k_f = A T^b \exp{-\tfrac{E}{RT}} - where *A* is the `pre_exponential_factor`, *b* is the `temperature_exponent`, - and *E* is the `activation_energy`. + where ``A`` is the `pre_exponential_factor`, ``b`` is the `temperature_exponent`, + and ``E`` is the `activation_energy`. """ def __cinit__(self, A=0, b=0, E=0, init=True): if init: @@ -764,7 +850,7 @@ cdef class Arrhenius: property pre_exponential_factor: """ - The pre-exponential factor *A* in units of m, kmol, and s raised to + The pre-exponential factor ``A`` in units of m, kmol, and s raised to powers depending on the reaction order. """ def __get__(self): @@ -772,14 +858,14 @@ cdef class Arrhenius: property temperature_exponent: """ - The temperature exponent *b*. + The temperature exponent ``b``. """ def __get__(self): return self.rate.temperatureExponent() property activation_energy: """ - The activation energy *E* [J/kmol]. + The activation energy ``E`` [J/kmol]. """ def __get__(self): return self.rate.activationEnergy_R() * gas_constant @@ -828,12 +914,12 @@ cdef class ElementaryReaction(Reaction): _has_legacy = True _hybrid = True - cdef CxxElementaryReaction3* er(self): + cdef CxxElementaryReaction3* cxx_object(self): if self.uses_legacy: raise AttributeError("Incorrect accessor for updated implementation") return self.reaction - cdef CxxElementaryReaction2* er2(self): + cdef CxxElementaryReaction2* cxx_object2(self): if not self.uses_legacy: raise AttributeError("Incorrect accessor for legacy implementation") return self.reaction @@ -868,16 +954,16 @@ cdef class ElementaryReaction(Reaction): self.rate = rate cdef _legacy_set_rate(self, Arrhenius rate): - cdef CxxElementaryReaction2* r = self.er2() + cdef CxxElementaryReaction2* r = self.cxx_object2() r.rate = deref(rate.rate) property rate: """ Get/Set the `ArrheniusRate` rate coefficient for this reaction. """ def __get__(self): if self.uses_legacy: - return wrapArrhenius(&(self.er2().rate), self) + return wrapArrhenius(&(self.cxx_object2().rate), self) - return ArrheniusRate.wrap(self.er().rate()) + return ArrheniusRate.wrap(self.cxx_object().rate()) def __set__(self, rate): if self.uses_legacy: self._legacy_set_rate(rate) @@ -895,7 +981,7 @@ cdef class ElementaryReaction(Reaction): rate.activation_energy) else: raise TypeError("Invalid rate definition") - self.er().setRate(rate3._base) + self.cxx_object().setRate(rate3._rate) property allow_negative_pre_exponential_factor: """ @@ -908,14 +994,14 @@ cdef class ElementaryReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.er2().allow_negative_pre_exponential_factor + return self.cxx_object2().allow_negative_pre_exponential_factor attr = "allow_negative_pre_exponential_factor" warnings.warn(self._deprecation_warning(attr), DeprecationWarning) return self.rate.allow_negative_pre_exponential_factor def __set__(self, allow): if self.uses_legacy: - self.er2().allow_negative_pre_exponential_factor = allow + self.cxx_object2().allow_negative_pre_exponential_factor = allow return attr = "allow_negative_pre_exponential_factor" @@ -948,20 +1034,20 @@ cdef class ThreeBodyReaction(ElementaryReaction): _has_legacy = True _hybrid = True - cdef CxxThreeBodyReaction3* tbr(self): + cdef CxxThreeBodyReaction3* cxx_threebody(self): if self.uses_legacy: raise AttributeError("Incorrect accessor for updated implementation") return self.reaction - cdef CxxThreeBodyReaction2* tbr2(self): + cdef CxxThreeBodyReaction2* cxx_threebody2(self): if not self.uses_legacy: raise AttributeError("Incorrect accessor for legacy implementation") return self.reaction cdef CxxThirdBody* thirdbody(self): if self.uses_legacy: - return &(self.tbr2().third_body) - return (self.tbr().thirdBody().get()) + return &(self.cxx_threebody2().third_body) + return (self.cxx_threebody().thirdBody().get()) def __init__(self, equation=None, rate=None, efficiencies=None, Kinetics kinetics=None, legacy=False, init=True, **kwargs): @@ -1018,7 +1104,7 @@ cdef class ThreeBodyReaction(ElementaryReaction): def efficiency(self, species): """ - Get the efficiency of the third body named *species* considering both + Get the efficiency of the third body named ``species`` considering both the default efficiency and species-specific efficiencies. """ return self.thirdbody().efficiency(stringify(species)) @@ -1183,7 +1269,7 @@ cdef class FalloffReaction(Reaction): def efficiency(self, species): """ - Get the efficiency of the third body named *species* considering both + Get the efficiency of the third body named ``species`` considering both the default efficiency and species-specific efficiencies. """ return self.frxn().third_body.efficiency(stringify(species)) @@ -1228,12 +1314,12 @@ cdef class PlogReaction(Reaction): _has_legacy = True _hybrid = True - cdef CxxPlogReaction3* pr(self): + cdef CxxPlogReaction3* cxx_object(self): if self.uses_legacy: raise AttributeError("Incorrect accessor for updated implementation") return self.reaction - cdef CxxPlogReaction2* cp2(self): + cdef CxxPlogReaction2* cxx_object2(self): if not self.uses_legacy: raise AttributeError("Incorrect accessor for legacy implementation") return self.reaction @@ -1276,14 +1362,14 @@ cdef class PlogReaction(Reaction): def __get__(self): if self.uses_legacy: raise AttributeError("Legacy implementation does not use rate property.") - return PlogRate.wrap(self.pr().rate()) + return PlogRate.wrap(self.cxx_object().rate()) def __set__(self, PlogRate rate): if self.uses_legacy: raise AttributeError("Legacy implementation does not use rate property.") - self.pr().setRate(rate._base) + self.cxx_object().setRate(rate._rate) cdef list _legacy_get_rates(self): - cdef CxxPlogReaction2* r = self.cp2() + cdef CxxPlogReaction2* r = self.cxx_object2() cdef vector[pair[double,CxxArrhenius]] cxxrates = r.rate.rates() cdef pair[double,CxxArrhenius] p_rate rates = [] @@ -1300,7 +1386,7 @@ cdef class PlogReaction(Reaction): item.second = deref(rate.rate) ratemap.insert(item) - cdef CxxPlogReaction2* r = self.cp2() + cdef CxxPlogReaction2* r = self.cxx_object2() r.rate = CxxPlog(ratemap) property rates: @@ -1332,7 +1418,7 @@ cdef class PlogReaction(Reaction): self.rate = rate_ cdef _legacy_call(self, float T, float P): - cdef CxxPlogReaction2* r = self.cp2() + cdef CxxPlogReaction2* r = self.cxx_object2() cdef double logT = np.log(T) cdef double recipT = 1/T cdef double logP = np.log(P) @@ -1385,12 +1471,12 @@ cdef class ChebyshevReaction(Reaction): _has_legacy = True _hybrid = True - cdef CxxChebyshevReaction3* cr(self): + cdef CxxChebyshevReaction3* cxx_object(self): if self.uses_legacy: raise AttributeError("Incorrect accessor for updated implementation") return self.reaction - cdef CxxChebyshevReaction2* cr2(self): + cdef CxxChebyshevReaction2* cxx_object2(self): if not self.uses_legacy: raise AttributeError("Incorrect accessor for legacy implementation") return self.reaction @@ -1425,11 +1511,11 @@ cdef class ChebyshevReaction(Reaction): def __get__(self): if self.uses_legacy: raise AttributeError("Legacy implementation does not use rate property.") - return ChebyshevRate.wrap(self.cr().rate()) + return ChebyshevRate.wrap(self.cxx_object().rate()) def __set__(self, ChebyshevRate rate): if self.uses_legacy: raise AttributeError("Legacy implementation does not use rate property.") - self.cr().setRate(rate._base) + self.cxx_object().setRate(rate._rate) property Tmin: """ @@ -1441,7 +1527,7 @@ cdef class ChebyshevReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.cr2().rate.Tmin() + return self.cxx_object2().rate.Tmin() warnings.warn(self._deprecation_warning("Tmin"), DeprecationWarning) return self.rate.Tmin @@ -1456,7 +1542,7 @@ cdef class ChebyshevReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.cr2().rate.Tmax() + return self.cxx_object2().rate.Tmax() warnings.warn(self._deprecation_warning("Tmax"), DeprecationWarning) return self.rate.Tmax @@ -1471,7 +1557,7 @@ cdef class ChebyshevReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.cr2().rate.Pmin() + return self.cxx_object2().rate.Pmin() warnings.warn(self._deprecation_warning("Pmin"), DeprecationWarning) return self.rate.Pmin @@ -1485,7 +1571,7 @@ cdef class ChebyshevReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.cr2().rate.Pmax() + return self.cxx_object2().rate.Pmax() warnings.warn(self._deprecation_warning("Pmax"), DeprecationWarning) return self.rate.Pmax @@ -1500,7 +1586,7 @@ cdef class ChebyshevReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.cr2().rate.nPressure() + return self.cxx_object2().rate.nPressure() warnings.warn(self._deprecation_warning("nPressure"), DeprecationWarning) return self.rate.n_pressure @@ -1515,14 +1601,14 @@ cdef class ChebyshevReaction(Reaction): """ def __get__(self): if self.uses_legacy: - return self.cr2().rate.nTemperature() + return self.cxx_object2().rate.nTemperature() warnings.warn( self._deprecation_warning("nTemperature"), DeprecationWarning) return self.rate.n_temperature cdef _legacy_get_coeffs(self): - cdef CxxChebyshevReaction2* r = self.cr2() + cdef CxxChebyshevReaction2* r = self.cxx_object2() c = np.fromiter(r.rate.coeffs(), np.double) return c.reshape((r.rate.nTemperature(), r.rate.nPressure())) @@ -1542,7 +1628,7 @@ cdef class ChebyshevReaction(Reaction): return self.rate.coeffs cdef _legacy_set_parameters(self, Tmin, Tmax, Pmin, Pmax, coeffs): - cdef CxxChebyshevReaction2* r = self.cr2() + cdef CxxChebyshevReaction2* r = self.cxx_object2() cdef CxxArray2D data data.resize(len(coeffs), len(coeffs[0])) @@ -1573,7 +1659,7 @@ cdef class ChebyshevReaction(Reaction): self.rate = ChebyshevRate(Tmin, Tmax, Pmin, Pmax, coeffs) cdef _legacy_call(self, float T, float P): - cdef CxxChebyshevReaction2* r = self.cr2() + cdef CxxChebyshevReaction2* r = self.cxx_object2() cdef double logT = np.log(T) cdef double recipT = 1/T cdef double logP = np.log10(P) @@ -1616,7 +1702,7 @@ cdef class BlowersMasel: property pre_exponential_factor: """ - The pre-exponential factor *A* in units of m, kmol, and s raised to + The pre-exponential factor ``A`` in units of m, kmol, and s raised to powers depending on the reaction order. """ def __get__(self): @@ -1624,14 +1710,14 @@ cdef class BlowersMasel: property temperature_exponent: """ - The temperature exponent *b*. + The temperature exponent ``b``. """ def __get__(self): return self.rate.temperatureExponent() def activation_energy(self, float dH): """ - The activation energy *E* [J/kmol] + The activation energy ``E`` [J/kmol] :param dH: The enthalpy of reaction [J/kmol] at the current temperature """ @@ -1640,14 +1726,14 @@ cdef class BlowersMasel: property bond_energy: """ Average bond dissociation energy of the bond being formed and broken - in the reaction *E* [J/kmol]. + in the reaction ``E`` [J/kmol]. """ def __get__(self): return self.rate.bondEnergy() * gas_constant property intrinsic_activation_energy: """ - The intrinsic activation energy *E0* [J/kmol]. + The intrinsic activation energy ``E0`` [J/kmol]. """ def __get__(self): return self.rate.activationEnergy_R0() * gas_constant @@ -1884,4 +1970,4 @@ cdef class CustomReaction(Reaction): def __set__(self, CustomRate rate): self._rate = rate cdef CxxCustomFunc1Reaction* r = self.reaction - r.setRate(self._rate._base) + r.setRate(self._rate._rate) diff --git a/interfaces/cython/cantera/test/test_kinetics.py b/interfaces/cython/cantera/test/test_kinetics.py index db2becc763..81636400c8 100644 --- a/interfaces/cython/cantera/test/test_kinetics.py +++ b/interfaces/cython/cantera/test/test_kinetics.py @@ -898,8 +898,8 @@ def test_fromXml(self): self.assertEqual(r.efficiencies['H2O'], 15.4) self.assertEqual(r.rate.temperature_exponent, -1.0) - def test_fromYaml(self): - r = ct.Reaction.fromYaml( + def test_from_yaml(self): + r = ct.Reaction.from_yaml( "{equation: 2 O + M <=> O2 + M," " type: three-body," " rate-constant: {A: 1.2e+11, b: -1.0, Ea: 0.0}," @@ -935,6 +935,20 @@ def test_listFromXml(self): eq2 = [r.equation for r in gas.reactions()] self.assertEqual(eq1, eq2) + def test_list_from_yaml(self): + yaml = """ + - equation: O + H2 <=> H + OH # Reaction 3 + rate-constant: {A: 3.87e+04, b: 2.7, Ea: 6260.0} + - equation: O + HO2 <=> OH + O2 # Reaction 4 + rate-constant: {A: 2.0e+13, b: 0.0, Ea: 0.0} + - equation: O + H2O2 <=> OH + HO2 # Reaction 5 + rate-constant: {A: 9.63e+06, b: 2.0, Ea: 4000.0} + """ + R = ct.Reaction.list_from_yaml(yaml, self.gas) + self.assertEqual(len(R), 3) + self.assertIn('HO2', R[2].products) + self.assertEqual(R[0].rate.temperature_exponent, 2.7) + def test_listFromYaml(self): yaml = """ - equation: O + H2 <=> H + OH # Reaction 3 @@ -944,7 +958,8 @@ def test_listFromYaml(self): - equation: O + H2O2 <=> OH + HO2 # Reaction 5 rate-constant: {A: 9.63e+06, b: 2.0, Ea: 4000.0} """ - R = ct.Reaction.listFromYaml(yaml, self.gas) + with self.assertWarnsRegex(DeprecationWarning, "is renamed to 'list_from_yaml'"): + R = ct.Reaction.listFromYaml(yaml, self.gas) self.assertEqual(len(R), 3) self.assertIn('HO2', R[2].products) self.assertEqual(R[0].rate.temperature_exponent, 2.7) @@ -1178,7 +1193,7 @@ def test_chebyshev_bad_shape_yaml(self): species=species, reactions=[]) with self.assertRaisesRegex(ct.CanteraError, "Inconsistent"): - r = ct.Reaction.fromYaml(''' + r = ct.Reaction.from_yaml(''' equation: R5 + H (+ M) <=> P5A + P5B (+M) type: Chebyshev temperature-range: [300.0, 2000.0] diff --git a/interfaces/cython/cantera/test/test_reaction.py b/interfaces/cython/cantera/test/test_reaction.py index df21c31dfb..ac2cfe36b3 100644 --- a/interfaces/cython/cantera/test/test_reaction.py +++ b/interfaces/cython/cantera/test/test_reaction.py @@ -1,5 +1,6 @@ from math import exp from pathlib import Path +import textwrap import cantera as ct import numpy as np @@ -20,7 +21,7 @@ def test_implicit_three_body(self): 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) + rxn1 = ct.Reaction.from_yaml(yaml1, self.gas) self.assertEqual(rxn1.reaction_type, "three-body") self.assertEqual(rxn1.default_efficiency, 0.) self.assertEqual(rxn1.efficiencies, {"O2": 1}) @@ -32,7 +33,7 @@ def test_implicit_three_body(self): default-efficiency: 0 efficiencies: {O2: 1.0} """ - rxn2 = ct.Reaction.fromYaml(yaml2, self.gas) + rxn2 = ct.Reaction.from_yaml(yaml2, self.gas) self.assertEqual(rxn1.efficiencies, rxn2.efficiencies) self.assertEqual(rxn1.default_efficiency, rxn2.default_efficiency) @@ -46,7 +47,7 @@ def test_duplicate(self): equation: H + O2 + H2O <=> HO2 + H2O rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0} """ - rxn1 = ct.Reaction.fromYaml(yaml1, gas1) + rxn1 = ct.Reaction.from_yaml(yaml1, gas1) yaml2 = """ equation: H + O2 + M <=> HO2 + M @@ -55,7 +56,7 @@ def test_duplicate(self): default-efficiency: 0 efficiencies: {H2O: 1} """ - rxn2 = ct.Reaction.fromYaml(yaml2, gas1) + rxn2 = ct.Reaction.from_yaml(yaml2, gas1) self.assertEqual(rxn1.reaction_type, rxn2.reaction_type) self.assertEqual(rxn1.reactants, rxn2.reactants) @@ -80,7 +81,7 @@ def test_short_serialization(self): 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) + rxn = ct.Reaction.from_yaml(yaml, self.gas) input_data = rxn.input_data self.assertNotIn("type", input_data) @@ -93,7 +94,7 @@ def test_non_integer_stoich(self): equation: 2 H + 1.5 O2 <=> H2O + O2 rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0} """ - rxn = ct.Reaction.fromYaml(yaml, self.gas) + rxn = ct.Reaction.from_yaml(yaml, self.gas) self.assertEqual(rxn.reaction_type, "elementary") def test_not_three_body(self): @@ -102,7 +103,7 @@ def test_not_three_body(self): 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) + rxn = ct.Reaction.from_yaml(yaml, self.gas) self.assertEqual(rxn.reaction_type, "elementary") def test_user_override(self): @@ -112,7 +113,7 @@ def test_user_override(self): rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0} type: elementary """ - rxn = ct.Reaction.fromYaml(yaml, self.gas) + rxn = ct.Reaction.from_yaml(yaml, self.gas) self.assertEqual(rxn.reaction_type, "elementary") @@ -124,6 +125,7 @@ class ReactionRateTests: _uses_pressure = False # rate evaluation requires pressure _index = None # index of reaction in "kineticsfromscratch.yaml" _input = None # input parameters (dict corresponding to YAML) + _yaml = None # yaml string specifying parameters @classmethod def setUpClass(cls): @@ -134,7 +136,7 @@ def setUpClass(cls): def test_type(self): # check reaction type - self.assertIn(self._type, "{}".format(self.rate)) + self.assertIn(self._cls.__name__, "{}".format(self.rate)) def test_rate_T(self): # check evaluation as a function of temperature only @@ -154,7 +156,7 @@ def test_rate_TP(self): def test_input(self): # check instantiation based on input_data rate = self._cls(input_data=self._input) - self.assertIn(self._type, "{}".format(rate)) + self.assertIn(self._cls.__name__, "{}".format(rate)) self.assertNear(rate(self.gas.T, self.gas.P), self.rate(self.gas.T, self.gas.P)) @@ -164,17 +166,35 @@ def test_unconfigured(self): self.assertTrue(np.isnan(rate(self.gas.T, self.gas.P))) input_data = rate.input_data self.assertIsInstance(input_data, dict) - self.assertEqual(input_data, {}) + if input_data: + self.assertEqual(input_data, {"type": self._type}) def test_roundtrip(self): # check round-trip instantiation via input_data - if self._index is None: - return input_data = self.rate.input_data rate = self._cls(input_data=input_data) self.assertNear(rate(self.gas.T, self.gas.P), self.rate(self.gas.T, self.gas.P)) + def test_from_dict(self): + # check instantiation from dictionary + input_data = self.rate.input_data + rate = ct.ReactionRate.from_dict(input_data) + self.assertNear(rate(self.gas.T, self.gas.P), + self.rate(self.gas.T, self.gas.P)) + + def test_from_yaml(self): + # check instantiation from yaml string + rate = ct.ReactionRate.from_yaml(self._yaml) + self.assertNear(rate(self.gas.T, self.gas.P), + self.rate(self.gas.T, self.gas.P)) + + def test_with_units(self): + units = "units: {length: cm, quantity: mol}" + yaml = f"{textwrap.dedent(self._yaml)}\n{units}" + with self.assertRaisesRegex(Exception, "not supported"): + rate = ct.ReactionRate.from_yaml(yaml) + class TestArrheniusRate(ReactionRateTests, utilities.CanteraTest): # test Arrhenius rate expressions @@ -184,6 +204,7 @@ class TestArrheniusRate(ReactionRateTests, utilities.CanteraTest): _uses_pressure = False _index = 0 _input = {"rate-constant": {"A": 38.7, "b": 2.7, "Ea": 26191840.0}} + _yaml = "rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol}" def setUp(self): self.A = self.gas.reaction(self._index).rate.pre_exponential_factor @@ -203,12 +224,17 @@ def test_allow_negative_pre_exponential_factor(self): self.rate.allow_negative_pre_exponential_factor = True self.assertTrue(self.rate.allow_negative_pre_exponential_factor) + def test_standalone(self): + yaml = "rate-constant: {A: 4.0e+21 cm^6/mol^2/s, b: 0.0, Ea: 1207.72688}" + with self.assertRaisesRegex(Exception, "not supported"): + rate = ct.ReactionRate.from_yaml(yaml) + class TestPlogRate(ReactionRateTests, utilities.CanteraTest): # test Plog rate expressions _cls = ct.PlogRate - _type = "Plog" + _type = "pressure-dependent-Arrhenius" _uses_pressure = True _index = 3 _input = {"rate-constants": [ @@ -216,6 +242,14 @@ class TestPlogRate(ReactionRateTests, utilities.CanteraTest): {"P": 101325., "A": 4.9108e+31, "b": -4.8507, "Ea": 103649395.2}, {"P": 1013250., "A": 1.2866e+47, "b": -9.0246, "Ea": 166508556.0}, {"P": 10132500., "A": 5.9632e+56, "b": -11.529, "Ea": 220076726.4}]} + _yaml = """ + type: pressure-dependent-Arrhenius + rate-constants: + - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04 cal/mol} + - {P: 1.0 atm, A: 4.9108e+31, b: -4.8507, Ea: 2.47728e+04 cal/mol} + - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04 cal/mol} + - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04 cal/mol} + """ def setUp(self): self.rate = ct.PlogRate([(1013.25, ct.Arrhenius(1.2124e+16, -0.5779, 45491376.8)), @@ -260,6 +294,18 @@ def test_no_rates(self): rate = ct.PlogRate() self.assertIsInstance(rate.rates, list) + def test_standalone(self): + yaml = """ + type: pressure-dependent-Arrhenius + rate-constants: + - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04 cal/mol} + - {P: 1.0 atm, A: 4.9108e+31 cm^6/mol^2/s, b: -4.8507, Ea: 2.47728e+04 cal/mol} + - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04 cal/mol} + - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04 cal/mol} + """ + with self.assertRaisesRegex(Exception, "not supported"): + rate = ct.ReactionRate.from_yaml(yaml) + class TestChebyshevRate(ReactionRateTests, utilities.CanteraTest): # test Chebyshev rate expressions @@ -273,6 +319,15 @@ class TestChebyshevRate(ReactionRateTests, utilities.CanteraTest): [0.3177, 0.26889, 0.094806, -0.0076385]], "pressure-range": [1000.0, 10000000.0], "temperature-range": [290.0, 3000.0]} + _yaml = """ + type: Chebyshev + temperature-range: [290.0, 3000.0] + pressure-range: [9.869232667160128e-03 atm, 98.69232667160128 atm] + data: + - [8.2883, -1.1397, -0.12059, 0.016034] + - [1.9764, 1.0037, 7.2865e-03, -0.030432] + - [0.3177, 0.26889, 0.094806, -7.6385e-03] + """ def setUp(self): self.Tmin = self.gas.reaction(self._index).rate.Tmin @@ -370,14 +425,22 @@ def test_from_yaml(self): # check instantiation from yaml string if self._yaml is None: return - rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) + self.check_rxn(rxn) + + def test_deprecated_yaml(self): + # check instantiation from yaml string + if self._yaml is None: + return + with self.assertWarnsRegex(DeprecationWarning, "is renamed to 'from_yaml'"): + rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) self.check_rxn(rxn) def test_from_dict2(self): # check instantiation from a yaml dictionary if self._yaml is None: return - rxn1 = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn1 = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) input_data = rxn1.input_data rxn2 = ct.Reaction.from_dict(input_data, kinetics=self.gas) # cannot compare types as input_data does not recreate legacy objects @@ -462,7 +525,7 @@ def test_deprecated_getters(self): if self._yaml is None: return - rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) for attr, default in self._deprecated_getters.items(): if self._legacy: self.check_equal(getattr(rxn, attr), default) @@ -475,7 +538,7 @@ def test_deprecated_setters(self): if self._yaml is None: return - rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) for attr, new in self._deprecated_setters.items(): if self._legacy: setattr(rxn, attr, new) @@ -491,7 +554,7 @@ def test_deprecated_callers(self): if self._yaml is None: return - rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) for state, value in self._deprecated_callers.items(): T, P = state if self._legacy: @@ -672,7 +735,7 @@ def check_rates(self, rates, other): def test_deprecated_getters(self): # overload default tester for deprecated property getters - rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) if self._legacy: self.check_rates(rxn.rates, self._rate) else: @@ -684,7 +747,7 @@ def test_deprecated_setters(self): rate = ct.PlogRate(self._rate) rates = rate.rates - rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas) + rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas) if self._legacy: rxn.rates = rates self.check_rates(rxn.rates, self._rate) diff --git a/src/kinetics/BulkKinetics.cpp b/src/kinetics/BulkKinetics.cpp index 36914c73f8..444c9bcf5a 100644 --- a/src/kinetics/BulkKinetics.cpp +++ b/src/kinetics/BulkKinetics.cpp @@ -124,26 +124,10 @@ bool BulkKinetics::addReaction(shared_ptr r) if (!(r->usesLegacy())) { shared_ptr rate = r->rate(); - // If neccessary, add new MultiBulkRates evaluator + // If neccessary, add new MultiBulkRate evaluator if (m_bulk_types.find(rate->type()) == m_bulk_types.end()) { m_bulk_types[rate->type()] = m_bulk_rates.size(); - - if (rate->type() == "ArrheniusRate") { - m_bulk_rates.push_back(std::unique_ptr( - new MultiBulkRates)); - } else if (rate->type() == "PlogRate") { - m_bulk_rates.push_back(std::unique_ptr( - new MultiBulkRates)); - } else if (rate->type() == "ChebyshevRate") { - m_bulk_rates.push_back(std::unique_ptr( - new MultiBulkRates)); - } else if (rate->type() == "custom-function") { - m_bulk_rates.push_back(std::unique_ptr( - new MultiBulkRates)); - } else { - throw CanteraError("BulkKinetics::addReaction", "Adding " - "reaction type '" + rate->type() + "' is not implemented"); - } + m_bulk_rates.push_back(rate->newMultiRate()); m_bulk_rates.back()->resizeSpecies(m_kk); } @@ -193,7 +177,7 @@ void BulkKinetics::modifyReaction(size_t i, shared_ptr rNew) if (!(rNew->usesLegacy())) { shared_ptr rate = rNew->rate(); - // Ensure that MultiBulkRates evaluator is available + // Ensure that MultiBulkRate evaluator is available if (m_bulk_types.find(rate->type()) == m_bulk_types.end()) { throw CanteraError("BulkKinetics::modifyReaction", "Evaluator not available for type '{}'.", rate->type()); diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 21eee16691..3f7362a252 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -7,6 +7,7 @@ #include "cantera/kinetics/Reaction.h" #include "cantera/kinetics/ReactionFactory.h" +#include "cantera/kinetics/ReactionRateFactory.h" #include "cantera/kinetics/FalloffFactory.h" #include "cantera/kinetics/Kinetics.h" #include "cantera/thermo/ThermoPhase.h" @@ -156,6 +157,10 @@ void Reaction::getParameters(AnyMap& reactionNode) const if (allow_nonreactant_orders) { reactionNode["nonreactant-orders"] = true; } + + if (m_rate) { + reactionNode.update(m_rate->parameters(rate_units)); + } } void Reaction::setParameters(const AnyMap& node, const Kinetics& kin) @@ -190,6 +195,16 @@ void Reaction::setParameters(const AnyMap& node, const Kinetics& kin) input = node; } +void Reaction::setRate(shared_ptr rate) +{ + if (!rate) { + // null pointer + m_rate.reset(); + } else { + m_rate = rate; + } +} + std::string Reaction::reactantString() const { std::ostringstream result; @@ -257,6 +272,11 @@ void Reaction::calculateRateCoeffUnits(const Kinetics& kin) rate_units *= phase.standardConcentrationUnits().pow(-stoich.second); } } + + if (m_rate) { + // Ensure that reaction rate object is updated + m_rate->setUnits(rate_units); + } } void updateUndeclared(std::vector& undeclared, @@ -865,7 +885,7 @@ void BlowersMaselInterfaceReaction::getParameters(AnyMap& reactionNode) const ElementaryReaction3::ElementaryReaction3() { - m_rate.reset(new ArrheniusRate); + setRate(newReactionRate(type())); } ElementaryReaction3::ElementaryReaction3(const Composition& reactants, @@ -877,22 +897,19 @@ ElementaryReaction3::ElementaryReaction3(const Composition& reactants, } ElementaryReaction3::ElementaryReaction3(const AnyMap& node, const Kinetics& kin) - : ElementaryReaction3() { - setParameters(node, kin); - setRate(std::make_shared(node, rate_units)); -} - -void ElementaryReaction3::getParameters(AnyMap& reactionNode) const -{ - Reaction::getParameters(reactionNode); - reactionNode.update(m_rate->parameters(rate_units)); + if (!node.empty()) { + setParameters(node, kin); + setRate(newReactionRate(node, rate_units)); + } else { + setRate(newReactionRate(type())); + } } ThreeBodyReaction3::ThreeBodyReaction3() - : ElementaryReaction3() { m_third_body.reset(new ThirdBody); + setRate(newReactionRate(type())); } ThreeBodyReaction3::ThreeBodyReaction3(const Composition& reactants, @@ -905,10 +922,14 @@ ThreeBodyReaction3::ThreeBodyReaction3(const Composition& reactants, } ThreeBodyReaction3::ThreeBodyReaction3(const AnyMap& node, const Kinetics& kin) - : ThreeBodyReaction3() { - setParameters(node, kin); - setRate(std::make_shared(node, rate_units)); + m_third_body.reset(new ThirdBody); + if (!node.empty()) { + setParameters(node, kin); + setRate(newReactionRate(node, rate_units)); + } else { + setRate(newReactionRate(type())); + } } bool ThreeBodyReaction3::detectEfficiencies() @@ -995,7 +1016,7 @@ void ThreeBodyReaction3::setParameters(const AnyMap& node, const Kinetics& kin) void ThreeBodyReaction3::getParameters(AnyMap& reactionNode) const { - ElementaryReaction3::getParameters(reactionNode); + Reaction::getParameters(reactionNode); if (!specified_collision_partner) { reactionNode["type"] = "three-body"; reactionNode["efficiencies"] = m_third_body->efficiencies; @@ -1028,7 +1049,7 @@ std::string ThreeBodyReaction3::productString() const PlogReaction3::PlogReaction3() { - m_rate.reset(new PlogRate); + setRate(newReactionRate(type())); } PlogReaction3::PlogReaction3(const Composition& reactants, @@ -1039,22 +1060,18 @@ PlogReaction3::PlogReaction3(const Composition& reactants, } PlogReaction3::PlogReaction3(const AnyMap& node, const Kinetics& kin) - : PlogReaction3() { - setParameters(node, kin); - setRate(std::make_shared(node, rate_units)); -} - -void PlogReaction3::getParameters(AnyMap& reactionNode) const -{ - Reaction::getParameters(reactionNode); - reactionNode["type"] = "pressure-dependent-Arrhenius"; - reactionNode.update(m_rate->parameters(rate_units)); + if (!node.empty()) { + setParameters(node, kin); + setRate(newReactionRate(node, rate_units)); + } else { + setRate(newReactionRate(type())); + } } ChebyshevReaction3::ChebyshevReaction3() { - m_rate.reset(new ChebyshevRate3); + setRate(newReactionRate(type())); } ChebyshevReaction3::ChebyshevReaction3(const Composition& reactants, @@ -1066,22 +1083,18 @@ ChebyshevReaction3::ChebyshevReaction3(const Composition& reactants, } ChebyshevReaction3::ChebyshevReaction3(const AnyMap& node, const Kinetics& kin) - : ChebyshevReaction3() { - setParameters(node, kin); - setRate(std::make_shared(node, rate_units)); -} - -void ChebyshevReaction3::getParameters(AnyMap& reactionNode) const -{ - Reaction::getParameters(reactionNode); - reactionNode["type"] = "Chebyshev"; - reactionNode.update(m_rate->parameters(rate_units)); + if (!node.empty()) { + setParameters(node, kin); + setRate(newReactionRate(node, rate_units)); + } else { + setRate(newReactionRate(type())); + } } CustomFunc1Reaction::CustomFunc1Reaction() { - m_rate.reset(new CustomFunc1Rate); + setRate(newReactionRate(type())); } CustomFunc1Reaction::CustomFunc1Reaction(const Composition& reactants, @@ -1093,10 +1106,13 @@ CustomFunc1Reaction::CustomFunc1Reaction(const Composition& reactants, } CustomFunc1Reaction::CustomFunc1Reaction(const AnyMap& node, const Kinetics& kin) - : CustomFunc1Reaction() { - setParameters(node, kin); - setRate(std::make_shared(node, rate_units)); + if (!node.empty()) { + setParameters(node, kin); + setRate(newReactionRate(node, rate_units)); + } else { + setRate(newReactionRate(type())); + } } Arrhenius readArrhenius(const XML_Node& arrhenius_node) diff --git a/src/kinetics/ReactionRate.cpp b/src/kinetics/ReactionRate.cpp index 695d210a84..21b5ea1e70 100644 --- a/src/kinetics/ReactionRate.cpp +++ b/src/kinetics/ReactionRate.cpp @@ -4,6 +4,7 @@ // at https://cantera.org/license.txt for license and copyright information. #include "cantera/kinetics/ReactionRate.h" +#include "cantera/kinetics/MultiRate.h" #include "cantera/numerics/Func1.h" namespace Cantera @@ -15,6 +16,11 @@ void ReactionRateBase::setParameters(const AnyMap& node, const Units& rate_units input = node; } +void ReactionRateBase::setUnits(const Units& rate_units) +{ + units = rate_units; +} + AnyMap ReactionRateBase::parameters(const Units& rate_units) const { AnyMap out; @@ -59,6 +65,11 @@ ArrheniusRate::ArrheniusRate(const Arrhenius& arr, bool allow_negative_A) { } +unique_ptr ArrheniusRate::newMultiRate() const +{ + return unique_ptr(new MultiBulkRate); +} + void ArrheniusRate::setParameters(const AnyMap& node, const Units& rate_units) { ReactionRateBase::setParameters(node, rate_units); @@ -67,6 +78,7 @@ void ArrheniusRate::setParameters(const AnyMap& node, const Units& rate_units) Arrhenius::setParameters(AnyValue(), node.units(), rate_units); return; } + Arrhenius::setParameters(node["rate-constant"], node.units(), rate_units); } @@ -108,6 +120,11 @@ PlogRate::PlogRate(const AnyMap& node) setParameters(node, Units(1.)); } +unique_ptr PlogRate::newMultiRate() const +{ + return unique_ptr(new MultiBulkRate); +} + void PlogRate::setParameters(const AnyMap& node, const Units& rate_units) { // @TODO implementation of Plog::setParameters should be transferred here @@ -117,6 +134,7 @@ void PlogRate::setParameters(const AnyMap& node, const Units& rate_units) Plog::setParameters(std::vector (), node.units(), rate_units); return; } + Plog::setParameters(node.at("rate-constants").asVector(), node.units(), rate_units); } @@ -126,6 +144,7 @@ void PlogRate::getParameters(AnyMap& rateNode, const Units& rate_units) const // @TODO implementation of Plog::getParameters should be transferred here // when the Plog class is removed from RxnRates.h after Cantera 2.6 Plog::getParameters(rateNode, rate_units); + rateNode["type"] = type(); } ChebyshevRate3::ChebyshevRate3(double Tmin, double Tmax, double Pmin, double Pmax, @@ -144,6 +163,12 @@ ChebyshevRate3::ChebyshevRate3(const AnyMap& node) setParameters(node, Units(1.)); } +unique_ptr ChebyshevRate3::newMultiRate() const +{ + return unique_ptr( + new MultiBulkRate); +} + void ChebyshevRate3::setParameters(const AnyMap& node, const Units& rate_units) { ReactionRateBase::setParameters(node, rate_units); @@ -162,6 +187,7 @@ void ChebyshevRate3::getParameters(AnyMap& rateNode, // @TODO implementation of Chebyshev::getParameters should be transferred here // when the Chebyshev class is removed from RxnRates.h after Cantera 2.6 Chebyshev::getParameters(rateNode, rate_units); + rateNode["type"] = type(); } void ChebyshevRate3::validate(const std::string& equation) @@ -170,6 +196,12 @@ void ChebyshevRate3::validate(const std::string& equation) CustomFunc1Rate::CustomFunc1Rate() : m_ratefunc(0) {} +unique_ptr CustomFunc1Rate::newMultiRate() const +{ + return unique_ptr( + new MultiBulkRate); +} + void CustomFunc1Rate::setRateFunction(shared_ptr f) { m_ratefunc = f; diff --git a/src/kinetics/ReactionRateFactory.cpp b/src/kinetics/ReactionRateFactory.cpp new file mode 100644 index 0000000000..f3444e1d7d --- /dev/null +++ b/src/kinetics/ReactionRateFactory.cpp @@ -0,0 +1,85 @@ + /** + * @file ReactionRateFactory.cpp + */ + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://cantera.org/license.txt for license and copyright information. + +#include "cantera/kinetics/ReactionRateFactory.h" +#include "cantera/kinetics/MultiRate.h" +#include "cantera/thermo/ThermoPhase.h" +#include "cantera/kinetics/Kinetics.h" +#include "cantera/base/AnyMap.h" + +namespace Cantera +{ + +ReactionRateFactory* ReactionRateFactory::s_factory = 0; +std::mutex ReactionRateFactory::rate_mutex; + +ReactionRateFactory::ReactionRateFactory() +{ + // ArrheniusRate evaluator + reg("Arrhenius", [](const AnyMap& node, const Units& rate_units) { + return new ArrheniusRate(node, rate_units); + }); + addAlias("Arrhenius", ""); + addAlias("Arrhenius", "elementary"); + addAlias("Arrhenius", "three-body"); + + // PlogRate evaluator + reg("pressure-dependent-Arrhenius", [](const AnyMap& node, const Units& rate_units) { + return new PlogRate(node, rate_units); + }); + + // ChebyshevRate evaluator + reg("Chebyshev", [](const AnyMap& node, const Units& rate_units) { + return new ChebyshevRate3(node, rate_units); + }); + + // CustomFunc1Rate evaluator + reg("custom-rate-function", [](const AnyMap& node, const Units& rate_units) { + return new CustomFunc1Rate(node, rate_units); + }); +} + +shared_ptr newReactionRate(const std::string& type) +{ + return shared_ptr ( + ReactionRateFactory::factory()->create(type, AnyMap(), Units(0.0))); +} + +shared_ptr newReactionRate( + const AnyMap& rate_node, const Units& rate_units) +{ + std::string type = ""; + if (rate_node.empty()) { + throw InputFileError("ReactionRateFactory::newReactionRate", rate_node, + "Received invalid empty node."); + } else if (rate_node.hasKey("type")) { + type = rate_node["type"].asString(); + } + + if (!(ReactionRateFactory::factory()->exists(type))) { + throw InputFileError("ReactionRateFactory::newReactionRate", rate_node, + "Unknown reaction rate type '{}'", type); + } + + return shared_ptr ( + ReactionRateFactory::factory()->create(type, rate_node, rate_units)); +} + +shared_ptr newReactionRate(const AnyMap& rate_node) +{ + const UnitSystem& system = rate_node.units(); + if (system.convertTo(1., "m") != 1. || system.convertTo(1., "kmol") != 1.) { + throw InputFileError("ReactionRateFactory::newReactionRate", + rate_node.at("__units__"), + "Alternative units for 'length' or 'quantity` are not supported " + "when creating\na standalone 'ReactionRate' object."); + } + AnyMap node(rate_node); + return newReactionRate(node, Units(0.)); +} + +} diff --git a/src/kinetics/RxnRates.cpp b/src/kinetics/RxnRates.cpp index 892821dc8e..c54fc8c2fa 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -45,6 +45,13 @@ void Arrhenius::setParameters(const AnyValue& rate, m_E = NAN; } else if (rate.is()) { auto& rate_map = rate.as(); + if (rate_units.factor() == 0 && rate_map["A"].is()) { + // A zero rate units factor is used as a sentinel to detect + // stand-alone reaction rate objects + throw InputFileError("Arrhenius::setParameters", rate_map, + "Specification of units is not supported for pre-exponential factor " + "when\ncreating a standalone 'ReactionRate' object."); + } m_A = units.convert(rate_map["A"], rate_units); m_b = rate_map["b"].asDouble(); m_E = units.convertActivationEnergy(rate_map["Ea"], "K");