diff --git a/doc/sphinx/cython/utilities.rst b/doc/sphinx/cython/utilities.rst index 9fbad19f67..55f3f63388 100644 --- a/doc/sphinx/cython/utilities.rst +++ b/doc/sphinx/cython/utilities.rst @@ -6,8 +6,13 @@ Utilities .. contents:: :local: -YAML Output ------------ +YAML Input/Output +----------------- + +AnyMap +^^^^^^ + +.. autoclass:: AnyMap YamlWriter ^^^^^^^^^^ diff --git a/doc/sphinx/yaml/general.rst b/doc/sphinx/yaml/general.rst index 9e851e0e6e..123a8a4e0b 100644 --- a/doc/sphinx/yaml/general.rst +++ b/doc/sphinx/yaml/general.rst @@ -40,6 +40,8 @@ following structure:: - equation: A + C <=> 2 D # Additional fields come after this +.. _sec-yaml-units: + Units ----- diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 590a084eca..688ce336b4 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -621,6 +621,9 @@ class AnyMap : public AnyBase //! Return the default units that should be used to convert stored values const UnitSystem& units() const { return *m_units; } + //! @copydoc units() + shared_ptr unitsShared() const { return m_units; } + //! Use the supplied UnitSystem to set the default units, and recursively //! process overrides from nodes named `units`. /*! diff --git a/include/cantera/base/Delegator.h b/include/cantera/base/Delegator.h index 9aee98729e..3b38588fe8 100644 --- a/include/cantera/base/Delegator.h +++ b/include/cantera/base/Delegator.h @@ -7,6 +7,7 @@ #define CT_DELEGATOR_H #include "cantera/base/global.h" +#include "cantera/base/Units.h" #include "cantera/base/ctexceptions.h" #include "cantera/base/ExtensionManager.h" #include @@ -145,6 +146,17 @@ class Delegator *m_funcs_v_d[name] = makeDelegate(func, when, *m_funcs_v_d[name]); } + //! set delegates for member functions with the signature `void(AnyMap&)` + void setDelegate(const string& name, const function& func, + const string& when) + { + if (!m_funcs_v_AMr.count(name)) { + throw NotImplementedError("Delegator::setDelegate", + "for function named '{}' with signature 'void(AnyMap&)'.", name); + } + *m_funcs_v_AMr[name] = makeDelegate(func, when, *m_funcs_v_AMr[name]); + } + //! set delegates for member functions with the signature //! `void(AnyMap&, UnitStack&)` void setDelegate(const std::string& name, @@ -160,6 +172,19 @@ class Delegator *m_funcs_v_cAMr_cUSr[name] = makeDelegate(func, when, *m_funcs_v_cAMr_cUSr[name]); } + //! set delegates for member functions with the signature + //! `void(const string&, void*)` + void setDelegate(const string& name, + const function& func, + const string& when) + { + if (!m_funcs_v_csr_vp.count(name)) { + throw NotImplementedError("Delegator::setDelegate", + "for function named '{}' with signature 'void(const string&, void*)'."); + } + *m_funcs_v_csr_vp[name] = makeDelegate(func, when, *m_funcs_v_csr_vp[name]); + } + //! Set delegates for member functions with the signature `void(double*)` void setDelegate(const std::string& name, const std::function, double*)>& func, @@ -254,8 +279,21 @@ class Delegator *m_funcs_sz_csr[name] = makeDelegate(name, func, when, m_base_sz_csr[name]); } - void holdExternalHandle(const shared_ptr& handle) { - m_handles.push_back(handle); + //! Store a handle to a wrapper for the delegate from an external language interface + void holdExternalHandle(const string& name, + const shared_ptr& handle) { + m_handles[name] = handle; + } + + //! Get the handle for a wrapper for the delegate from the external language + //! interface specified by *name*. + //! Returns a null pointer if the requested handle does not exist. + shared_ptr getExternalHandle(const string& name) const { + if (m_handles.count(name)) { + return m_handles.at(name); + } else { + return shared_ptr(); + } } protected: @@ -283,6 +321,14 @@ class Delegator m_funcs_v_d[name] = ⌖ } + //! Install a function with the signature `void(AnyMap&)` as being delegatable + void install(const string& name, function& target, + const function& func) + { + target = func; + m_funcs_v_AMr[name] = ⌖ + } + //! Install a function with the signature `void(const AnyMap&, const UnitStack&)` //! as being delegatable void install(const std::string& name, @@ -293,7 +339,14 @@ class Delegator m_funcs_v_cAMr_cUSr[name] = ⌖ } - + //! Install a function with the signature `void(const string&, void*) as being + //! delegatable + void install(const string& name, function& target, + const function& func) + { + target = func; + m_funcs_v_csr_vp[name] = ⌖ + } //! Install a function with the signature `void(double*)` as being delegatable void install(const std::string& name, @@ -470,8 +523,10 @@ class Delegator std::map*> m_funcs_v; std::map*> m_funcs_v_b; std::map*> m_funcs_v_d; + map*> m_funcs_v_AMr; std::map*> m_funcs_v_cAMr_cUSr; + map*> m_funcs_v_csr_vp; std::map, double*)>*> m_funcs_v_dp; std::map*> m_funcs_sz_csr; //! @} - //! Cleanup functions to be called from the destructor - std::list> m_handles; + //! Handles to wrappers for the delegated object in external language interfaces. + //! Used to provide access to these wrappers as well as managing cleanup functions + //! called from the destructor of the derived ExternalHandle class. + map> m_handles; //! Name of the class in the extension language std::string m_delegatorName; diff --git a/include/cantera/base/ExtensionManager.h b/include/cantera/base/ExtensionManager.h index 2fd262425b..6a03ba1e95 100644 --- a/include/cantera/base/ExtensionManager.h +++ b/include/cantera/base/ExtensionManager.h @@ -88,14 +88,22 @@ class ExtensionManager //! Register a function that can be used to create wrappers for ReactionData objects //! in an external language and link them to the corresponding C++ object - static void registerReactionDataLinker(const std::string& rateName, - std::function link); + //! + //! @param rateName The name of the reaction rate type + //! @param wrapperName The name used for Solution wrappers to be used with this + //! object, corresponding to a type registered with registerSolutionLinker(). + static void registerReactionDataLinker(const string& rateName, + const string& wrapperName, function link); //! Register a function that can be used to create wrappers for Solution objects in //! an external language and link it to the corresponding C++ objects static void registerSolutionLinker(const std::string& wrapperName, std::function(shared_ptr)> link); + //! Get the Solution wrapper type corresponding to the specified user-defined + //! reaction rate type. + static string getSolutionWrapperType(const string& userType); + protected: //! Functions for wrapping and linking ReactionData objects static std::map(shared_ptr)>> s_Solution_linkers; + //! Mapping from user-defined rate types to Solution wrapper types + static map s_userTypeToWrapperType; }; } diff --git a/include/cantera/base/Solution.h b/include/cantera/base/Solution.h index 376c6ba954..6c88891afc 100644 --- a/include/cantera/base/Solution.h +++ b/include/cantera/base/Solution.h @@ -137,7 +137,7 @@ class Solution : public std::enable_shared_from_this AnyMap m_header; //!< Additional input fields; usually from a YAML input file - //! Wrappers for this Kinetics object in extension languages, for evaluation + //! Wrappers for this Solution object in extension languages, for evaluation //! of user-defined reaction rates std::map> m_externalHandles; diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h index 22485cf0f7..76cb98671c 100644 --- a/include/cantera/base/Units.h +++ b/include/cantera/base/Units.h @@ -106,6 +106,8 @@ struct UnitStack UnitStack(std::initializer_list> units) : stack(units) {} + UnitStack() = default; + //! Size of UnitStack size_t size() const { return stack.size(); } @@ -220,6 +222,13 @@ class UnitSystem double convert(const AnyValue& val, const std::string& dest) const; double convert(const AnyValue& val, const Units& dest) const; + //! Convert a generic AnyValue node representing a reaction rate coefficient to the + //! units specified in `dest`. Works like `convert(AnyValue&, Units&)` but with + //! special handling for the case where the destination units are undefined. + //! + //! @since New in Cantera 3.0 + double convertRateCoeff(const AnyValue& val, const Units& dest) const; + //! Convert an array of AnyValue nodes to the units specified in `dest`. For //! each node, if the value is a double, convert it using the default units, //! and if it is a string, treat it as a value with the given dimensions. diff --git a/include/cantera/kinetics/Arrhenius.h b/include/cantera/kinetics/Arrhenius.h index 62145f9b00..92db739eb4 100644 --- a/include/cantera/kinetics/Arrhenius.h +++ b/include/cantera/kinetics/Arrhenius.h @@ -121,24 +121,18 @@ class ArrheniusBase : public ReactionRate return m_Ea_R * GasConstant; } - // Return units of the reaction rate expression - const Units& rateUnits() const { - return m_rate_units; - } - //! Return reaction order associated with the reaction rate double order() const { return m_order; } //! Set units of the reaction rate expression - void setRateUnits(const UnitStack& rate_units) { + void setRateUnits(const UnitStack& rate_units) override { + ReactionRate::setRateUnits(rate_units); if (rate_units.size() > 1) { - m_rate_units = rate_units.product(); - m_order = 1 - m_rate_units.dimension("quantity"); + m_order = 1 - rate_units.product().dimension("quantity"); } else { m_order = NAN; - m_rate_units = rate_units.standardUnits(); } } @@ -164,7 +158,6 @@ class ArrheniusBase : public ReactionRate std::string m_b_str = "b"; //!< The string for temperature exponent std::string m_Ea_str = "Ea"; //!< The string for activation energy std::string m_E4_str = ""; //!< The string for an optional 4th parameter - Units m_rate_units{0.}; //!< Reaction rate units }; //! Arrhenius reaction rate type depends only on temperature diff --git a/include/cantera/kinetics/ChebyshevRate.h b/include/cantera/kinetics/ChebyshevRate.h index 883a0b0025..1a923290a1 100644 --- a/include/cantera/kinetics/ChebyshevRate.h +++ b/include/cantera/kinetics/ChebyshevRate.h @@ -232,8 +232,6 @@ class ChebyshevRate final : public ReactionRate Array2D m_coeffs; //!<< coefficient array vector_fp dotProd_; //!< dot product of coeffs with the reduced pressure polynomial - - Units m_rate_units = Units(0.); //!< Reaction rate units }; } diff --git a/include/cantera/kinetics/InterfaceRate.h b/include/cantera/kinetics/InterfaceRate.h index 24ff67a101..25553eed69 100644 --- a/include/cantera/kinetics/InterfaceRate.h +++ b/include/cantera/kinetics/InterfaceRate.h @@ -456,12 +456,10 @@ class StickingRate : public RateType, public StickingCoverage //! Constructor based on AnyMap content StickingRate(const AnyMap& node, const UnitStack& rate_units) { - // sticking coefficients are dimensionless - setParameters(node, Units(1.0)); + setParameters(node, rate_units); } explicit StickingRate(const AnyMap& node) { - // sticking coefficients are dimensionless - setParameters(node, Units(1.0)); + setParameters(node, {}); } unique_ptr newMultiRate() const override { @@ -473,10 +471,16 @@ class StickingRate : public RateType, public StickingCoverage return "sticking-" + RateType::type(); } + void setRateUnits(const UnitStack& rate_units) override { + // Sticking coefficients are dimensionless + RateType::m_conversion_units = Units(1.0); + } + virtual void setParameters( const AnyMap& node, const UnitStack& rate_units) override { InterfaceRateBase::setParameters(node); + setRateUnits(rate_units); RateType::m_negativeA_ok = node.getBool("negative-A", false); setStickingParameters(node); if (!node.hasKey("sticking-coefficient")) { diff --git a/include/cantera/kinetics/KineticsFactory.h b/include/cantera/kinetics/KineticsFactory.h index 87bbe2ee9c..703b6d8ea2 100644 --- a/include/cantera/kinetics/KineticsFactory.h +++ b/include/cantera/kinetics/KineticsFactory.h @@ -58,10 +58,12 @@ shared_ptr newKinetics(const string& model); * to the Kinetics object. * @param rootNode The root node of the file containing the phase definition, * which will be treated as the default source for reactions + * @param soln The Solution object that this Kinetics object is being added to. */ shared_ptr newKinetics(const vector>& phases, const AnyMap& phaseNode, - const AnyMap& rootNode=AnyMap()); + const AnyMap& rootNode=AnyMap(), + shared_ptr soln={}); //! @copydoc newKinetics(const vector>&, const AnyMap&, const AnyMap&) //! @deprecated To be removed after Cantera 3.0; diff --git a/include/cantera/kinetics/ReactionRate.h b/include/cantera/kinetics/ReactionRate.h index 1dd5610274..8d6d370aeb 100644 --- a/include/cantera/kinetics/ReactionRate.h +++ b/include/cantera/kinetics/ReactionRate.h @@ -53,6 +53,7 @@ class ReactionRate : m_input(other.m_input) , m_rate_index(other.m_rate_index) , m_valid(other.m_valid) + , m_conversion_units(other.m_conversion_units) {} ReactionRate& operator=(const ReactionRate& other) { @@ -62,6 +63,7 @@ class ReactionRate m_input = other.m_input; m_rate_index = other.m_rate_index; m_valid = other.m_valid; + m_conversion_units = other.m_conversion_units; return *this; } @@ -94,6 +96,7 @@ class ReactionRate //! @param node AnyMap object containing reaction rate specification //! @param units unit definitions specific to rate information virtual void setParameters(const AnyMap& node, const UnitStack& units) { + setRateUnits(units); m_input = node; } @@ -102,10 +105,35 @@ class ReactionRate //! handled by the getParameters() method. AnyMap parameters() const { AnyMap out; + out["type"] = type(); getParameters(out); return out; } + //! Get the units for converting the leading term in the reaction rate expression. + //! + //! These units are often the same as the units of the rate expression itself, but + //! not always; sticking coefficients are a notable exception. + //! @since New in Cantera 3.0 + const Units& conversionUnits() const { + return m_conversion_units; + } + + //! Set the units of the reaction rate expression + //! + //! Used to determine the units that should be used for converting terms in the + //! reaction rate expression, which often have the same units (for example, the + //! Arrhenius pre-exponential) but may also be different (for example, sticking + //! coefficients). + //! @since New in Cantera 3.0 + virtual void setRateUnits(const UnitStack& rate_units) { + if (rate_units.size() > 1) { + m_conversion_units = rate_units.product(); + } else { + m_conversion_units = rate_units.standardUnits(); + } + } + //! Check basic syntax and settings of reaction rate expression virtual void check(const std::string& equation) {} @@ -224,6 +252,9 @@ class ReactionRate //! Flag indicating composition dependent rate bool m_composition_dependent_rate = false; + //! Units of the leading term in the reaction rate expression + Units m_conversion_units{0.}; + private: //! Return an object that be used to evaluate the rate by converting general input //! such as temperature and pressure into the `DataType` struct that is particular diff --git a/include/cantera/kinetics/ReactionRateDelegator.h b/include/cantera/kinetics/ReactionRateDelegator.h index 46f129d022..a8e716d06d 100644 --- a/include/cantera/kinetics/ReactionRateDelegator.h +++ b/include/cantera/kinetics/ReactionRateDelegator.h @@ -46,20 +46,10 @@ class ReactionDataDelegator : public Delegator, public ReactionData m_wrappedData = wrapper; } - //! Set the type of the Solution wrapper needed for this delegated reaction type. - //! This should correspond to the name registered for the external language with - //! ExtensionManager::registerSolutionLinker(). - void setSolutionWrapperType(const std::string& type) { - m_solutionWrapperType = type; - } - protected: //! The reaction rate type std::string m_rateType; - //! The name registered for creating Solution wrappers for this delegated reaction - std::string m_solutionWrapperType; - //! An external language's wrapper for the Solution object where this ReactionData //! object is being used shared_ptr m_wrappedSolution; @@ -103,15 +93,23 @@ class ReactionRateDelegator : public Delegator, public ReactionRate m_setParameters(node, units); } + void getParameters(AnyMap& node) const override { + m_getParameters(node); + } + + void validate(const string& equation, const Kinetics& kin) override; + private: //! The name of the reaction rate type - std::string m_rateType; + string m_rateType; //! Delegated `evalFromStruct` method taking a pointer to the corresponding //! ReactionData wrapper object - std::function m_evalFromStruct; + function m_evalFromStruct; - std::function m_setParameters; + function m_validate; + function m_setParameters; + function m_getParameters; }; } diff --git a/interfaces/cython/cantera/_onedim.pyx b/interfaces/cython/cantera/_onedim.pyx index 8eaf3b5aa8..724f2c331a 100644 --- a/interfaces/cython/cantera/_onedim.pyx +++ b/interfaces/cython/cantera/_onedim.pyx @@ -5,7 +5,7 @@ from .interrupts import no_op import warnings import numpy as np -from ._utils cimport stringify, pystr, anymap_to_dict +from ._utils cimport stringify, pystr, anymap_to_py from ._utils import CanteraError from cython.operator import dereference as deref @@ -298,7 +298,7 @@ cdef class Domain1D: def __get__(self): cdef shared_ptr[CxxSolutionArray] arr arr = self.domain.toArray(False) - return anymap_to_dict(arr.get().meta()) + return anymap_to_py(arr.get().meta()) cdef class Boundary1D(Domain1D): @@ -1588,7 +1588,7 @@ cdef class Sim1D: cdef CxxAnyMap header header = self.sim.restore(stringify(str(filename)), stringify(name)) self._initialized = True - return anymap_to_dict(header) + return anymap_to_py(header) def restore_time_stepping_solution(self): """ diff --git a/interfaces/cython/cantera/_utils.pxd b/interfaces/cython/cantera/_utils.pxd index 4918ad98ad..c27d66595a 100644 --- a/interfaces/cython/cantera/_utils.pxd +++ b/interfaces/cython/cantera/_utils.pxd @@ -7,6 +7,7 @@ from libcpp.unordered_map cimport unordered_map from .ctcxx cimport * +from .units cimport UnitSystem, CxxUnits cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": cdef cppclass CxxAnyValue "Cantera::AnyValue" @@ -36,7 +37,8 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": void clear() void update(CxxAnyMap& other, cbool) string keys_str() - void applyUnits() + void applyUnits() except +translate_exception + shared_ptr[CxxUnitSystem] unitsShared() cdef cppclass CxxAnyValue "Cantera::AnyValue": CxxAnyValue() @@ -49,7 +51,11 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": CxxAnyValue& operator=[T](vector[T]) except +translate_exception unordered_map[string, CxxAnyMap*] asMap(string) except +translate_exception CxxAnyMap& getMapWhere(string, string) except +translate_exception + void setQuantity(double, string&, cbool) except +translate_exception + void setQuantity(vector[double]&, string&) except +translate_exception + void setQuantity(double, CxxUnits&) except +translate_exception T& asType "as" [T]() except +translate_exception + vector[T]& asVector[T]() except +translate_exception string type_str() cbool empty() cbool isType "is" [T]() @@ -86,6 +92,9 @@ cdef extern from "cantera/cython/utils_utils.h": cdef void CxxSetLogger "setLogger" (CxxPythonLogger*) +cdef class AnyMap(dict): + cdef _set_CxxUnitSystem(self, shared_ptr[CxxUnitSystem] units) + cdef UnitSystem unitsystem cdef string stringify(x) except * cdef pystr(string x) @@ -93,8 +102,8 @@ cdef pystr(string x) cdef comp_map_to_dict(Composition m) cdef Composition comp_map(X) except * -cdef CxxAnyMap dict_to_anymap(dict data, cbool hyphenize=*) except * -cdef anymap_to_dict(CxxAnyMap& m) +cdef CxxAnyMap py_to_anymap(data, cbool hyphenize=*) except * +cdef anymap_to_py(CxxAnyMap& m) cdef CxxAnyValue python_to_anyvalue(item, name=*) except * cdef anyvalue_to_python(string name, CxxAnyValue& v) diff --git a/interfaces/cython/cantera/_utils.pyx b/interfaces/cython/cantera/_utils.pyx index 8c4e3746b2..f3d9663ad7 100644 --- a/interfaces/cython/cantera/_utils.pyx +++ b/interfaces/cython/cantera/_utils.pyx @@ -7,7 +7,9 @@ import warnings from cpython.ref cimport PyObject import numbers import importlib.metadata +from collections import namedtuple import numpy as np +from .units cimport Units _scipy_sparse = None @@ -151,8 +153,75 @@ cdef comp_map_to_dict(Composition m): class CanteraError(RuntimeError): pass +_DimensionalValue = namedtuple('_DimensionalValue', + ('value', 'units', 'activation_energy'), + defaults=[False]) + cdef public PyObject* pyCanteraError = CanteraError + +cdef class AnyMap(dict): + """ + A key-value store representing objects defined in Cantera's YAML input format. + + Extends the capabilities of a normal `dict` object by providing functions for + converting values between different unit systems. See :ref:`sec-yaml-units` for + details on how units are handled in YAML input files. + """ + def __cinit__(self, *args, **kwawrgs): + self.unitsystem = UnitSystem() + + cdef _set_CxxUnitSystem(self, shared_ptr[CxxUnitSystem] units): + self.unitsystem._set_unitSystem(units) + + def default_units(self): + return self.unitsystem.defaults() + + @property + def units(self): + """Get the `UnitSystem` applicable to this `AnyMap`.""" + return self.unitsystem + + def convert(self, str key, dest): + """ + Convert the value corresponding to the specified *key* to the units defined by + *dest*. *dest* may be a string or a `Units` object. + """ + return self.unitsystem.convert_to(self[key], dest) + + def convert_activation_energy(self, key, dest): + """ + Convert the value corresponding to the specified *key* to the units defined by + *dest*. *dest* may be a string or a `Units` object defining units that are + interpretable as an activation energy. + """ + return self.unitsystem.convert_activation_energy_to(self[key], dest) + + def convert_rate_coeff(self, str key, dest): + """ + Convert the value corresponding to the specified *key* to the units defined by + *dest*, with special handling for `UnitStack` input and potentially-undefined + rate coefficient units. + """ + return self.unitsystem.convert_rate_coeff_to(self[key], dest) + + def set_quantity(self, str key, value, src): + """ + Set the element *key* of this map to the specified value, converting from the + units defined by *src* to the correct unit system for this map when serializing + to YAML. + """ + self[key] = _DimensionalValue(value, src) + + def set_activation_energy(self, str key, value, src): + """ + Set the element *key* of this map to the specified value, converting from the + activation energy units defined by *src* to the correct unit system for this map + when serializing to YAML. + """ + self[key] = _DimensionalValue(value, src, True) + + cdef anyvalue_to_python(string name, CxxAnyValue& v): cdef CxxAnyMap a cdef CxxAnyValue b @@ -175,9 +244,9 @@ cdef anyvalue_to_python(string name, CxxAnyValue& v): "from AnyValue of held type '{}'".format( pystr(name), v.type_str())) elif v.isType[CxxAnyMap](): - return anymap_to_dict(v.asType[CxxAnyMap]()) + return anymap_to_py(v.asType[CxxAnyMap]()) elif v.isType[vector[CxxAnyMap]](): - return [anymap_to_dict(a) for a in v.asType[vector[CxxAnyMap]]()] + return [anymap_to_py(a) for a in v.asType[vector[CxxAnyMap]]()] elif v.isType[vector[double]](): return v.asType[vector[double]]() elif v.isType[vector[string]](): @@ -204,15 +273,33 @@ cdef anyvalue_to_python(string name, CxxAnyValue& v): pystr(name), v.type_str())) -cdef anymap_to_dict(CxxAnyMap& m): +cdef anymap_to_py(CxxAnyMap& m): cdef pair[string,CxxAnyValue] item m.applyUnits() - if m.empty(): - return {} - return {pystr(item.first): anyvalue_to_python(item.first, item.second) - for item in m.ordered()} + cdef AnyMap out = AnyMap() + out._set_CxxUnitSystem(m.unitsShared()) + for item in m.ordered(): + out[pystr(item.first)] = anyvalue_to_python(item.first, item.second) + return out + + +cdef void setQuantity(CxxAnyMap& m, str k, v: _DimensionalValue) except *: + cdef CxxAnyValue testval = python_to_anyvalue(v.value) + cdef CxxAnyValue target + if isinstance(v.units, str): + if testval.isScalar(): + target.setQuantity(testval.asType[double](), stringify(v.units), + v.activation_energy) + else: + target.setQuantity(testval.asVector[double](), stringify(v.units)) + elif isinstance(v.units, Units): + target.setQuantity(testval.asType[double](), (v.units).units) + else: + raise TypeError(f'Expected a string or Units object. Got {type(v.units)}') + m[stringify(k)] = target + -cdef CxxAnyMap dict_to_anymap(dict data, cbool hyphenize=False) except *: +cdef CxxAnyMap py_to_anymap(data, cbool hyphenize=False) except *: cdef CxxAnyMap m if hyphenize: # replace "_" by "-": while Python dictionaries typically use "_" in key names, @@ -225,7 +312,10 @@ cdef CxxAnyMap dict_to_anymap(dict data, cbool hyphenize=False) except *: data = _hyphenize(data) for k, v in data.items(): - m[stringify(k)] = python_to_anyvalue(v, k) + if isinstance(v, _DimensionalValue): + setQuantity(m, k, v) + else: + m[stringify(k)] = python_to_anyvalue(v, k) return m cdef get_types(item): @@ -290,7 +380,7 @@ cdef get_types(item): cdef CxxAnyValue python_to_anyvalue(item, name=None) except *: cdef CxxAnyValue v if isinstance(item, dict): - v = dict_to_anymap(item) + v = py_to_anymap(item) elif isinstance(item, (list, tuple, set, np.ndarray)): itype, ndim = get_types(item) if ndim == 1: @@ -384,7 +474,7 @@ cdef vector[CxxAnyMap] list_dict_to_anyvalue(data) except *: v.resize(len(data)) cdef size_t i for i, item in enumerate(data): - v[i] = dict_to_anymap(item) + v[i] = py_to_anymap(item) return v cdef vector[vector[double]] list2_double_to_anyvalue(data): @@ -432,3 +522,8 @@ def _py_to_any_to_py(dd): cdef string name = stringify("test") cdef CxxAnyValue vv = python_to_anyvalue(dd) return anyvalue_to_python(name, vv), pystr(vv.type_str()) + +def _py_to_anymap_to_py(pp): + # used for internal testing purposes only + cdef CxxAnyMap m = py_to_anymap(pp) + return anymap_to_py(m) diff --git a/interfaces/cython/cantera/ctcxx.pxd b/interfaces/cython/cantera/ctcxx.pxd index 28b77d7ff8..e79645d7ca 100644 --- a/interfaces/cython/cantera/ctcxx.pxd +++ b/interfaces/cython/cantera/ctcxx.pxd @@ -11,6 +11,7 @@ from libcpp.cast cimport dynamic_cast from libcpp.pair cimport pair from libcpp cimport bool as cbool from libcpp.functional cimport function +from libcpp.memory cimport shared_ptr, dynamic_pointer_cast from cpython cimport bool as pybool from cpython.ref cimport PyObject from cython.operator cimport dereference as deref, preincrement as inc @@ -27,10 +28,3 @@ cdef extern from "cantera/cython/funcWrapper.h": cdef cppclass CxxAnyValue "Cantera::AnyValue" cdef cppclass CxxUnits "Cantera::Units" cdef cppclass CxxUnitSystem "Cantera::UnitSystem" - - -cdef extern from "": - cppclass shared_ptr "std::shared_ptr" [T]: - T* get() - void reset(T*) - void reset() diff --git a/interfaces/cython/cantera/delegator.pxd b/interfaces/cython/cantera/delegator.pxd index 39011a392d..721ddd736a 100644 --- a/interfaces/cython/cantera/delegator.pxd +++ b/interfaces/cython/cantera/delegator.pxd @@ -22,16 +22,29 @@ cdef extern from "" namespace "std" nogil: size_t& operator[](size_t) +cdef extern from "cantera/extensions/PythonHandle.h" namespace "Cantera": + cdef cppclass CxxExternalHandle "Cantera::ExternalHandle": + pass + + cdef cppclass CxxPythonHandle "Cantera::PythonHandle" (CxxExternalHandle): + CxxPythonHandle(PyObject*, cbool) + PyObject* get() + + cdef extern from "cantera/base/Delegator.h" namespace "Cantera": cdef cppclass CxxDelegator "Cantera::Delegator": Delegator() void setDelegatorName(string&) + void holdExternalHandle(string&, shared_ptr[CxxExternalHandle]&) + shared_ptr[CxxExternalHandle] getExternalHandle(string&) void setDelegate(string&, function[void()], string&) except +translate_exception void setDelegate(string&, function[void(cbool)], string&) except +translate_exception void setDelegate(string&, function[void(double)], string&) except +translate_exception + void setDelegate(string&, function[void(CxxAnyMap&)], string&) except +translate_exception void setDelegate(string&, function[void(const CxxAnyMap&, const CxxUnitStack&)], string&) except +translate_exception + void setDelegate(string&, function[void(const string&, void*)], string&) except +translate_exception void setDelegate(string&, function[void(size_array1, double*)], string&) except +translate_exception void setDelegate(string&, function[void(size_array1, double, double*)], string&) except +translate_exception void setDelegate(string&, function[void(size_array2, double, double*, double*)], string&) except +translate_exception @@ -47,8 +60,11 @@ cdef extern from "cantera/cython/funcWrapper.h": cdef function[void(double)] pyOverride(PyObject*, void(PyFuncInfo&, double)) cdef function[void(cbool)] pyOverride(PyObject*, void(PyFuncInfo&, cbool)) cdef function[void()] pyOverride(PyObject*, void(PyFuncInfo&)) + cdef function[void(CxxAnyMap&)] pyOverride(PyObject*, void(PyFuncInfo&, CxxAnyMap&)) cdef function[void(const CxxAnyMap&, const CxxUnitStack&)] pyOverride( PyObject*, void(PyFuncInfo&, const CxxAnyMap&, const CxxUnitStack&)) + cdef function[void(const string&, void*)] pyOverride( + PyObject*, void(PyFuncInfo&, const string&, void*)) cdef function[void(size_array1, double*)] pyOverride( PyObject*, void(PyFuncInfo&, size_array1, double*)) cdef function[void(size_array1, double, double*)] pyOverride( diff --git a/interfaces/cython/cantera/delegator.pyx b/interfaces/cython/cantera/delegator.pyx index d7e4b8a99b..fe2dc16cd5 100644 --- a/interfaces/cython/cantera/delegator.pyx +++ b/interfaces/cython/cantera/delegator.pyx @@ -9,14 +9,16 @@ from libc.stdlib cimport malloc from libc.string cimport strcpy from ._utils import CanteraError -from ._utils cimport stringify, pystr, anymap_to_dict -from .units cimport Units +from ._utils cimport stringify, pystr, anymap_to_py, py_to_anymap +from .units cimport Units, UnitStack # from .reaction import ExtensibleRate, ExtensibleRateData from .reaction cimport (ExtensibleRate, ExtensibleRateData, CxxReaction, CxxReactionRateDelegator, CxxReactionDataDelegator) from .solutionbase cimport CxxSolution, _assign_Solution from cython.operator import dereference as deref +from cpython.object cimport PyTypeObject, traverseproc, visitproc, inquiry + # ## Implementation for each delegated function type # # Besides the C++ functions implemented in the `Delegator` class, each delegated @@ -111,12 +113,25 @@ cdef void callback_v_b(PyFuncInfo& funcInfo, cbool arg) noexcept: funcInfo.setExceptionType(exc_type) funcInfo.setExceptionValue(exc_value) +# Wrapper for functions of type void(AnyMap&) +cdef void callback_v_AMr(PyFuncInfo& funcInfo, CxxAnyMap& arg) noexcept: + pyArg = anymap_to_py(arg) # cast away constness + try: + (funcInfo.func())(pyArg) + # return updated AnyMap to C++. Odd syntax is a workaround for Cython's + # unwillingness to assign to a reference + (&arg)[0] = py_to_anymap(pyArg) + except BaseException as e: + exc_type, exc_value = sys.exc_info()[:2] + funcInfo.setExceptionType(exc_type) + funcInfo.setExceptionValue(exc_value) + # Wrapper for functions of type void(const AnyMap&, const UnitStack&) cdef void callback_v_cAMr_cUSr(PyFuncInfo& funcInfo, const CxxAnyMap& arg1, const CxxUnitStack& arg2) noexcept: - pyArg1 = anymap_to_dict(arg1) # cast away constness - pyArg2 = Units.copy(arg2.product()) + pyArg1 = anymap_to_py(arg1) # cast away constness + pyArg2 = UnitStack.copy(arg2) try: (funcInfo.func())(pyArg1, pyArg2) except BaseException as e: @@ -124,6 +139,16 @@ cdef void callback_v_cAMr_cUSr(PyFuncInfo& funcInfo, const CxxAnyMap& arg1, funcInfo.setExceptionType(exc_type) funcInfo.setExceptionValue(exc_value) +# Wrapper for functions of type void(const string&, void*) +cdef void callback_v_csr_vp(PyFuncInfo& funcInfo, + const string& arg1, void* obj) noexcept: + try: + (funcInfo.func())(pystr(arg1), obj) + except BaseException as e: + exc_type, exc_value = sys.exc_info()[:2] + funcInfo.setExceptionType(exc_type) + funcInfo.setExceptionValue(exc_value) + # Wrapper for functions of type void(double*) cdef void callback_v_dp(PyFuncInfo& funcInfo, size_array1 sizes, double* arg) noexcept: cdef double[:] view = arg if sizes[0] else None @@ -308,9 +333,15 @@ cdef int assign_delegates(obj, CxxDelegator* delegator) except -1: elif callback == 'void(double)': delegator.setDelegate(cxx_name, pyOverride(method, callback_v_d), cxx_when) + elif callback == 'void(AnyMap&)': + delegator.setDelegate(cxx_name, + pyOverride(method, callback_v_AMr), cxx_when) elif callback == 'void(AnyMap&,UnitStack&)': delegator.setDelegate(cxx_name, pyOverride(method, callback_v_cAMr_cUSr), cxx_when) + elif callback == 'void(string,void*)': + delegator.setDelegate(cxx_name, + pyOverride(method, callback_v_csr_vp), cxx_when) elif callback == 'void(double*)': delegator.setDelegate(cxx_name, pyOverride(method, callback_v_dp), cxx_when) @@ -405,6 +436,48 @@ cdef public object ct_wrapSolution(shared_ptr[CxxSolution] soln): _assign_Solution(pySoln, soln, False, weak=True) return pySoln +# The pair of (ExtensibleRate, ReactionRateDelegator) objects both hold owned references +# to one another. To allow the Python garbage collector to break this reference cycle, +# we need to implement custom behavior to detect when the only object referring the +# ReactionRateDelegator is the ExtensibleRate. +# Implementation roughly follows from https://github.com/mdavidsaver/cython-c--demo + +# Capture the original implementations of the tp_traverse and tp_clear methods +cdef PyTypeObject* extensibleRate_t = ExtensibleRate +cdef traverseproc extensibleRate_base_traverse = extensibleRate_t.tp_traverse +cdef inquiry extensibleRate_base_clear = extensibleRate_t.tp_clear +assert extensibleRate_base_traverse != NULL +assert extensibleRate_base_clear != NULL + +cdef int traverse_ExtensibleRate(PyObject* raw, visitproc visit, void* arg) except -1: + cdef ExtensibleRate self = raw + cdef int ret = 0 + # If self._rate.use_count() is 1, this ExtensibleRate rate is the only object + # referencing self._rate. To let the GC see the cycle where self._rate references + # self, we tell it to visit self. If self._rate.use_count() is more than one, there + # are other C++ objects referring to self._rate, and we don't want the GC to see a + # cycle, so we skip visiting self. + if self._rate.use_count() == 1: + ret = visit(self, arg) + if ret: + return ret + # Call the original traverser to deal with all other members + ret = extensibleRate_base_traverse(raw, visit, arg) + return ret + +cdef int clear_ExtensibleRate(object obj) except -1: + cdef ExtensibleRate self = obj + # If the GC has called this method, this ExtensibleRate is the only object holding + # a reference to the ReactionRateDelegator, and resetting the shared_ptr will delete + # both that object and its reference to the ExtensibleRate, allowing the ref count + # for the ExtensibleRate to go to zero. + self._rate.reset() + return extensibleRate_base_clear(obj) + +# Assign the augmented garbage collector functions +extensibleRate_t.tp_traverse = traverse_ExtensibleRate +extensibleRate_t.tp_clear = clear_ExtensibleRate + CxxPythonExtensionManager.registerSelf() diff --git a/interfaces/cython/cantera/kinetics.pyx b/interfaces/cython/cantera/kinetics.pyx index 74d4522313..fb1afe1c24 100644 --- a/interfaces/cython/cantera/kinetics.pyx +++ b/interfaces/cython/cantera/kinetics.pyx @@ -443,9 +443,9 @@ cdef class Kinetics(_SolutionBase): def __get__(self): cdef CxxAnyMap settings self.kinetics.getDerivativeSettings(settings) - return anymap_to_dict(settings) + return anymap_to_py(settings) def __set__(self, settings): - self.kinetics.setDerivativeSettings(dict_to_anymap(settings)) + self.kinetics.setDerivativeSettings(py_to_anymap(settings)) property forward_rate_constants_ddT: """ diff --git a/interfaces/cython/cantera/preconditioners.pyx b/interfaces/cython/cantera/preconditioners.pyx index 06b97be32b..ebe5bcc300 100644 --- a/interfaces/cython/cantera/preconditioners.pyx +++ b/interfaces/cython/cantera/preconditioners.pyx @@ -1,7 +1,7 @@ # 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. -from ._utils cimport stringify, pystr, dict_to_anymap, anymap_to_dict +from ._utils cimport stringify, pystr, py_to_anymap, anymap_to_py from .kinetics cimport get_from_sparse cdef class PreconditionerBase: diff --git a/interfaces/cython/cantera/reaction.pxd b/interfaces/cython/cantera/reaction.pxd index 749e029893..2e53e23903 100644 --- a/interfaces/cython/cantera/reaction.pxd +++ b/interfaces/cython/cantera/reaction.pxd @@ -38,6 +38,7 @@ cdef extern from "cantera/kinetics/ReactionRate.h" namespace "Cantera": double eval(double, double) except +translate_exception double eval(double, vector[double]&) except +translate_exception CxxAnyMap parameters() except +translate_exception + CxxUnits conversionUnits() cdef extern from "cantera/kinetics/Arrhenius.h" namespace "Cantera": diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index fb3c638b54..e925a470cf 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -8,6 +8,7 @@ from cython.operator cimport dereference as deref from .kinetics cimport Kinetics from ._utils cimport * +from ._utils import CanteraError from .units cimport * from .delegator cimport * @@ -59,6 +60,21 @@ cdef class ReactionRate: # update global reaction class registry register_subclasses(ReactionRate) + # Check for delegated rate, which will already have a Python wrapper + cdef CxxDelegator* drate = dynamic_cast[CxxDelegatorPtr](rate.get()) + cdef CxxPythonHandle* handle + + if drate != NULL: + handle = dynamic_pointer_cast[CxxPythonHandle, CxxExternalHandle]( + drate.getExternalHandle(stringify("python"))).get() + if handle != NULL and handle.get() != NULL: + py_rate = (handle.get()) + py_rate._rate = rate + return py_rate + else: + raise CanteraError("Internal Error: Delegator does not have a " + "corresponding Python ExtensibleRate object") + # identify class (either subType or type) rate_type = pystr(rate.get().subType()) if rate_type == "": @@ -94,7 +110,7 @@ cdef class ReactionRate: 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, hyphenize=hyphenize) + cdef CxxAnyMap any_map = py_to_anymap(data, hyphenize=hyphenize) cxx_rate = CxxNewReactionRate(any_map) return ReactionRate.wrap(cxx_rate) @@ -129,7 +145,15 @@ cdef class ReactionRate: Get input data for this reaction rate with its current parameter values. """ def __get__(self): - return anymap_to_dict(self.rate.parameters()) + return anymap_to_py(self.rate.parameters()) + + @property + def conversion_units(self) -> Units: + """ + Get the units for converting the leading term in the reaction rate expression + to different unit systems. + """ + return Units.copy(self.rate.conversionUnits()) cdef class ArrheniusRateBase(ReactionRate): @@ -221,8 +245,8 @@ cdef class ArrheniusRate(ArrheniusRateBase): if init: self._cinit(input_data, A=A, b=b, Ea=Ea) - def _from_dict(self, dict input_data): - self._rate.reset(new CxxArrheniusRate(dict_to_anymap(input_data))) + def _from_dict(self, input_data): + self._rate.reset(new CxxArrheniusRate(py_to_anymap(input_data))) def _from_parameters(self, A, b, Ea): self._rate.reset(new CxxArrheniusRate(A, b, Ea)) @@ -248,8 +272,8 @@ cdef class BlowersMaselRate(ArrheniusRateBase): if init: self._cinit(input_data, A=A, b=b, Ea0=Ea0, w=w) - def _from_dict(self, dict input_data): - self._rate.reset(new CxxBlowersMaselRate(dict_to_anymap(input_data))) + def _from_dict(self, input_data): + self._rate.reset(new CxxBlowersMaselRate(py_to_anymap(input_data))) def _from_parameters(self, A, b, Ea0, w): self._rate.reset(new CxxBlowersMaselRate(A, b, Ea0, w)) @@ -321,9 +345,9 @@ cdef class TwoTempPlasmaRate(ArrheniusRateBase): """ return self.rate.eval(temperature, elec_temp) - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset( - new CxxTwoTempPlasmaRate(dict_to_anymap(input_data, hyphenize=True)) + new CxxTwoTempPlasmaRate(py_to_anymap(input_data, hyphenize=True)) ) def _from_parameters(self, A, b, Ea_gas, Ea_electron): @@ -448,9 +472,9 @@ cdef class LindemannRate(FalloffRate): """ _reaction_rate_type = "Lindemann" - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset( - new CxxLindemannRate(dict_to_anymap(input_data, hyphenize=True)) + new CxxLindemannRate(py_to_anymap(input_data, hyphenize=True)) ) cdef set_cxx_object(self): @@ -468,9 +492,9 @@ cdef class TroeRate(FalloffRate): """ _reaction_rate_type = "Troe" - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset( - new CxxTroeRate(dict_to_anymap(input_data, hyphenize=True)) + new CxxTroeRate(py_to_anymap(input_data, hyphenize=True)) ) cdef set_cxx_object(self): @@ -488,9 +512,9 @@ cdef class SriRate(FalloffRate): """ _reaction_rate_type = "SRI" - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset( - new CxxSriRate(dict_to_anymap(input_data, hyphenize=True)) + new CxxSriRate(py_to_anymap(input_data, hyphenize=True)) ) cdef set_cxx_object(self): @@ -504,9 +528,9 @@ cdef class TsangRate(FalloffRate): """ _reaction_rate_type = "Tsang" - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset( - new CxxTsangRate(dict_to_anymap(input_data, hyphenize=True)) + new CxxTsangRate(py_to_anymap(input_data, hyphenize=True)) ) cdef set_cxx_object(self): @@ -528,9 +552,9 @@ cdef class PlogRate(ReactionRate): elif init: if isinstance(input_data, dict): - self._rate.reset(new CxxPlogRate(dict_to_anymap(input_data))) + self._rate.reset(new CxxPlogRate(py_to_anymap(input_data))) elif rates is None: - self._rate.reset(new CxxPlogRate(dict_to_anymap({}))) + self._rate.reset(new CxxPlogRate(py_to_anymap({}))) elif input_data: raise TypeError("Invalid parameter 'input_data'") else: @@ -585,7 +609,7 @@ cdef class ChebyshevRate(ReactionRate): if init: if isinstance(input_data, dict): - self._rate.reset(new CxxChebyshevRate(dict_to_anymap(input_data))) + self._rate.reset(new CxxChebyshevRate(py_to_anymap(input_data))) elif all([arg is not None for arg in [temperature_range, pressure_range, data]]): Tmin = temperature_range[0] @@ -596,7 +620,7 @@ cdef class ChebyshevRate(ReactionRate): new CxxChebyshevRate(Tmin, Tmax, Pmin, Pmax, self._cxxarray2d(data))) elif all([arg is None for arg in [temperature_range, pressure_range, data, input_data]]): - self._rate.reset(new CxxChebyshevRate(dict_to_anymap({}))) + self._rate.reset(new CxxChebyshevRate(py_to_anymap({}))) elif input_data: raise TypeError("Invalid parameter 'input_data'") else: @@ -718,9 +742,9 @@ cdef class ExtensibleRate(ReactionRate): of the rate parameterization and its corresponding data class, and to make these rates constructible through factory functions and input files. - Classes derived from `ExtensibleRate` should implement the `set_parameters` and - `eval` methods, which will be called as delegates from the C++ :ct:`ReactionRate` - class. + Classes derived from `ExtensibleRate` should implement the `set_parameters`, + `get_parameters`, `eval`, and (optionally) `validate` methods, which will be called + as delegates from the C++ :ct:`ReactionRate` class. **Warning:** The delegatable methods defined here are an experimental part of the Cantera API and may change without notice. @@ -732,26 +756,49 @@ cdef class ExtensibleRate(ReactionRate): delegatable_methods = { "eval": ("evalFromStruct", "double(void*)", "replace"), - "set_parameters": ("setParameters", "void(AnyMap&, UnitStack&)", "after") + "set_parameters": ("setParameters", "void(AnyMap&, UnitStack&)", "after"), + "get_parameters": ("getParameters", "void(AnyMap&)", "replace"), + "validate": ("validate", "void(string, void*)", "replace") } + def __cinit__(self, *args, init=True, **kwargs): if init: self._rate.reset(new CxxReactionRateDelegator()) self.set_cxx_object() - def __init__(self, *args, **kwargs): + def __init__(self, *args, input_data=None, **kwargs): if self._rate.get() is not NULL: assign_delegates(self, dynamic_cast[CxxDelegatorPtr](self.rate)) # ReactionRate does not define __init__, so it does not need to be called + if input_data is not None: + self.set_parameters(input_data, UnitStack()) - def set_parameters(self, params: dict, units: Units) -> None: + def set_parameters(self, params: AnyMap, rate_coeff_units: UnitStack) -> None: """ Responsible for setting rate parameters based on the input data. For example, for reactions created from YAML, ``params`` is the YAML reaction entry converted - to a ``dict``. ``units`` specifies the units of the rate coefficient. + to an ``AnyMap``. ``rate_coeff_units`` specifies the units of the rate + coefficient. + + Input values contained in ``params`` may be in non-default unit systems, + specified in the user-provided input file. To convert them to Cantera's native + mks+kmol unit system, use the functions `AnyMap.convert`, + `AnyMap.convert_activation_energy`, and `AnyMap.convert_rate_coeff` as needed. """ raise NotImplementedError(f"{self.__class__.__name__}.set_parameters") + def get_parameters(self, params: AnyMap) -> None: + """ + Responsible for serializing the state of the ExtensibleRate object, using the + same format as a YAML reaction entry. This is the inverse of `set_parameters`. + + Serialization methods may request output in unit systems other than Cantera's + native mks+kmol system. To enable conversions to the user-specified unit system, + dimensional values should be added to ``params`` using the methods + `AnyMap.set_quantity` and `AnyMap.set_activation_energy`. + """ + raise NotImplementedError(f"{self.__class__.__name__}.get_parameters") + def eval(self, data: ExtensibleRateData) -> float: """ Responsible for calculating the forward rate constant based on the current state @@ -760,6 +807,15 @@ cdef class ExtensibleRate(ReactionRate): """ raise NotImplementedError(f"{self.__class__.__name__}.eval") + def validate(self, equation: str, soln: "Solution") -> None: + """ + Responsible for validating that the rate expression is configured with valid + parameters. This may depend on properties of the Solution, for example + temperature ranges over which the rate expression can be evaluated. Raises an + exception if any validation fails. + """ + pass + cdef set_cxx_object(self, CxxReactionRate* rate=NULL): if rate is NULL: self.rate = self._rate.get() @@ -833,9 +889,9 @@ cdef class InterfaceRateBase(ArrheniusRateBase): def __get__(self): cdef CxxAnyMap cxx_deps self.interface.getCoverageDependencies(cxx_deps) - return anymap_to_dict(cxx_deps) - def __set__(self, dict deps): - cdef CxxAnyMap cxx_deps = dict_to_anymap(deps) + return anymap_to_py(cxx_deps) + def __set__(self, deps): + cdef CxxAnyMap cxx_deps = py_to_anymap(deps) self.interface.setCoverageDependencies(cxx_deps) @@ -892,8 +948,8 @@ cdef class InterfaceArrheniusRate(InterfaceRateBase): if init: self._cinit(input_data, A=A, b=b, Ea=Ea) - def _from_dict(self, dict input_data): - self._rate.reset(new CxxInterfaceArrheniusRate(dict_to_anymap(input_data))) + def _from_dict(self, input_data): + self._rate.reset(new CxxInterfaceArrheniusRate(py_to_anymap(input_data))) def _from_parameters(self, A, b, Ea): self._rate.reset(new CxxInterfaceArrheniusRate(A, b, Ea)) @@ -920,8 +976,8 @@ cdef class InterfaceBlowersMaselRate(InterfaceRateBase): if init: self._cinit(input_data, A=A, b=b, Ea0=Ea0, w=w) - def _from_dict(self, dict input_data): - self._rate.reset(new CxxInterfaceBlowersMaselRate(dict_to_anymap(input_data))) + def _from_dict(self, input_data): + self._rate.reset(new CxxInterfaceBlowersMaselRate(py_to_anymap(input_data))) def _from_parameters(self, A, b, Ea0, w): self._rate.reset(new CxxInterfaceBlowersMaselRate(A, b, Ea0, w)) @@ -1030,8 +1086,8 @@ cdef class StickingArrheniusRate(StickRateBase): if init: self._cinit(input_data, A=A, b=b, Ea=Ea) - def _from_dict(self, dict input_data): - self._rate.reset(new CxxStickingArrheniusRate(dict_to_anymap(input_data))) + def _from_dict(self, input_data): + self._rate.reset(new CxxStickingArrheniusRate(py_to_anymap(input_data))) def _from_parameters(self, A, b, Ea): self._rate.reset(new CxxStickingArrheniusRate(A, b, Ea)) @@ -1057,8 +1113,8 @@ cdef class StickingBlowersMaselRate(StickRateBase): if init: self._cinit(input_data, A=A, b=b, Ea0=Ea0, w=w) - def _from_dict(self, dict input_data): - self._rate.reset(new CxxStickingBlowersMaselRate(dict_to_anymap(input_data))) + def _from_dict(self, input_data): + self._rate.reset(new CxxStickingBlowersMaselRate(py_to_anymap(input_data))) def _from_parameters(self, A, b, Ea0, w): self._rate.reset(new CxxStickingBlowersMaselRate(A, b, Ea0, w)) @@ -1354,7 +1410,7 @@ cdef class Reaction: A `Kinetics` object whose associated phase(s) contain the species involved in the reaction. """ - cdef CxxAnyMap any_map = dict_to_anymap(data, hyphenize=hyphenize) + cdef CxxAnyMap any_map = py_to_anymap(data, hyphenize=hyphenize) cxx_reaction = CxxNewReaction(any_map, deref(kinetics.kinetics)) return Reaction.wrap(cxx_reaction) @@ -1549,7 +1605,7 @@ cdef class Reaction: definition. """ def __get__(self): - return anymap_to_dict(self.reaction.parameters(True)) + return anymap_to_py(self.reaction.parameters(True)) def update_user_data(self, data): """ @@ -1557,7 +1613,7 @@ cdef class Reaction: YAML phase definition files with `Solution.write_yaml` or in the data returned by `input_data`. Existing keys with matching names are overwritten. """ - self.reaction.input.update(dict_to_anymap(data), False) + self.reaction.input.update(py_to_anymap(data), False) def clear_user_data(self): """ diff --git a/interfaces/cython/cantera/reactor.pyx b/interfaces/cython/cantera/reactor.pyx index b5a2510a98..44da487693 100644 --- a/interfaces/cython/cantera/reactor.pyx +++ b/interfaces/cython/cantera/reactor.pyx @@ -7,7 +7,7 @@ import numbers as _numbers from cython.operator cimport dereference as deref from .thermo cimport * -from ._utils cimport pystr, stringify, comp_map, dict_to_anymap, anymap_to_dict +from ._utils cimport pystr, stringify, comp_map, py_to_anymap, anymap_to_py from ._utils import * from .delegator cimport * @@ -1593,7 +1593,7 @@ cdef class ReactorNet: def __get__(self): cdef CxxAnyMap stats stats = self.net.solverStats() - return anymap_to_dict(stats) + return anymap_to_py(stats) property derivative_settings: """ @@ -1601,4 +1601,4 @@ cdef class ReactorNet: See also `Kinetics.derivative_settings`. """ def __set__(self, settings): - self.net.setDerivativeSettings(dict_to_anymap(settings)) + self.net.setDerivativeSettings(py_to_anymap(settings)) diff --git a/interfaces/cython/cantera/solutionbase.pxd b/interfaces/cython/cantera/solutionbase.pxd index 0e209feb4b..c8aee47b6d 100644 --- a/interfaces/cython/cantera/solutionbase.pxd +++ b/interfaces/cython/cantera/solutionbase.pxd @@ -8,6 +8,8 @@ import numpy as np cimport numpy as np from .ctcxx cimport * +from .delegator cimport CxxExternalHandle + cdef extern from "cantera/thermo/ThermoFactory.h" namespace "Cantera": cdef cppclass CxxThermoPhase "Cantera::ThermoPhase" @@ -29,14 +31,6 @@ cdef extern from "cantera/base/Interface.h" namespace "Cantera": string, string, vector[string]) except +translate_exception -cdef extern from "cantera/extensions/PythonHandle.h" namespace "Cantera": - cdef cppclass CxxExternalHandle "Cantera::ExternalHandle": - pass - - cdef cppclass CxxPythonHandle "Cantera::PythonHandle" (CxxExternalHandle): - CxxPythonHandle(PyObject*, cbool) - - cdef extern from "cantera/base/Solution.h" namespace "Cantera": cdef cppclass CxxKinetics "Cantera::Kinetics" cdef cppclass CxxTransport "Cantera::Transport" diff --git a/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index 0dfb9f019a..fef09e96a8 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -12,7 +12,7 @@ from .kinetics cimport * from .transport cimport * from .reaction cimport * from ._utils cimport * -from .delegator cimport pyOverride, callback_v +from .delegator cimport pyOverride, callback_v, CxxPythonHandle from .yamlwriter cimport YamlWriter ctypedef CxxSurfPhase* CxxSurfPhasePtr @@ -113,8 +113,8 @@ cdef class _SolutionBase: self.name = name def __dealloc__(self): - if self.base != NULL: - self.base.removeChangedCallback(self) + if self._base: + self._base.get().removeChangedCallback(self) property name: """ @@ -263,7 +263,7 @@ cdef class _SolutionBase: definition. """ def __get__(self): - return anymap_to_dict(self.base.parameters(True)) + return anymap_to_py(self.base.parameters(True)) property input_header: """ @@ -272,15 +272,15 @@ cdef class _SolutionBase: that are not required for the instantiation of Cantera objects. """ def __get__(self): - return anymap_to_dict(self.base.header()) + return anymap_to_py(self.base.header()) - def update_user_data(self, dict data): + def update_user_data(self, data): """ Add the contents of the provided `dict` as additional fields when generating YAML phase definition files with `write_yaml` or in the data returned by `input_data`. Existing keys with matching names are overwritten. """ - self.thermo.input().update(dict_to_anymap(data), False) + self.thermo.input().update(py_to_anymap(data), False) def clear_user_data(self): """ @@ -296,7 +296,7 @@ cdef class _SolutionBase: when generating files with `write_yaml` or in the data returned by `input_header`. Existing keys with matching names are overwritten. """ - self.base.header().update(dict_to_anymap(data), False) + self.base.header().update(py_to_anymap(data), False) def clear_user_header(self): """ @@ -506,7 +506,7 @@ cdef class SolutionArrayBase: size = np.prod(shape) cdef CxxAnyMap cxx_meta if meta is not None: - cxx_meta = dict_to_anymap(meta) + cxx_meta = py_to_anymap(meta) self._base = CxxNewSolutionArray(phase._base, size, cxx_meta) self.base = self._base.get() @@ -574,12 +574,12 @@ cdef class SolutionArrayBase: """ Dictionary holding information describing the `SolutionArrayBase`. """ - return anymap_to_dict(self.base.meta()) + return anymap_to_py(self.base.meta()) @meta.setter def meta(self, meta): if isinstance(meta, dict): - self.base.setMeta(dict_to_anymap(meta)) + self.base.setMeta(py_to_anymap(meta)) else: raise TypeError("Metadata needs to be a dictionary.") @@ -655,18 +655,18 @@ cdef class SolutionArrayBase: def get_auxiliary(self, loc): """ Retrieve auxiliary data for a `SolutionArrayBase` location """ - return anymap_to_dict(self.base.getAuxiliary(loc)) + return anymap_to_py(self.base.getAuxiliary(loc)) def set_auxiliary(self, loc, data): """ Set auxiliary data for a `SolutionArrayBase` location """ - self.base.setAuxiliary(loc, dict_to_anymap(data)) + self.base.setAuxiliary(loc, py_to_anymap(data)) def _append(self, state, extra): """ Append at end of `SolutionArrayBase` """ cdef vector[double] cxx_state for item in state: cxx_state.push_back(item) - self.base.append(cxx_state, dict_to_anymap(extra)) + self.base.append(cxx_state, py_to_anymap(extra)) def _cxx_save(self, filename, name, key, description, overwrite, compression): """ Interface `SolutionArray.save` with C++ core """ @@ -679,4 +679,4 @@ cdef class SolutionArrayBase: cdef CxxAnyMap header header = self.base.restore( stringify(str(filename)), stringify(name), stringify(key)) - return anymap_to_dict(header) + return anymap_to_py(header) diff --git a/interfaces/cython/cantera/speciesthermo.pyx b/interfaces/cython/cantera/speciesthermo.pyx index 7c99538f96..0410427721 100644 --- a/interfaces/cython/cantera/speciesthermo.pyx +++ b/interfaces/cython/cantera/speciesthermo.pyx @@ -97,7 +97,7 @@ cdef class SpeciesThermo: data provided with its input (YAML) definition. """ def __get__(self): - return anymap_to_dict(self.spthermo.parameters(True)) + return anymap_to_py(self.spthermo.parameters(True)) def update_user_data(self, data): """ @@ -105,7 +105,7 @@ cdef class SpeciesThermo: YAML phase definition files with `Solution.write_yaml` or in the data returned by `input_data`. Existing keys with matching names are overwritten. """ - self.spthermo.input().update(dict_to_anymap(data), False) + self.spthermo.input().update(py_to_anymap(data), False) def clear_user_data(self): """ diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 93903c0584..0354b101aa 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -120,7 +120,7 @@ cdef class Species: :param data: A dictionary corresponding to the YAML representation. """ - cdef CxxAnyMap any_map = dict_to_anymap(data) + cdef CxxAnyMap any_map = py_to_anymap(data) cxx_species = CxxNewSpecies(any_map) species = Species(init=False) species._assign(cxx_species) @@ -230,7 +230,7 @@ cdef class Species: """ def __get__(self): cdef CxxThermoPhase* phase = self._phase.thermo if self._phase else NULL - return anymap_to_dict(self.species.parameters(phase)) + return anymap_to_py(self.species.parameters(phase)) def update_user_data(self, data): """ @@ -238,7 +238,7 @@ cdef class Species: YAML phase definition files with `Solution.write_yaml` or in the data returned by `input_data`. Existing keys with matching names are overwritten. """ - self.species.input.update(dict_to_anymap(data), False) + self.species.input.update(py_to_anymap(data), False) def clear_user_data(self): """ diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index b51b4b0aa3..6f9ecb4681 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -80,7 +80,7 @@ cdef class GasTransportData: user-specified data provided with its input (YAML) definition. """ def __get__(self): - return anymap_to_dict(self.data.parameters(True)) + return anymap_to_py(self.data.parameters(True)) def update_user_data(self, data): """ @@ -88,7 +88,7 @@ cdef class GasTransportData: YAML phase definition files with `Solution.write_yaml` or in the data returned by `input_data`. Existing keys with matching names are overwritten. """ - self.data.input.update(dict_to_anymap(data), False) + self.data.input.update(py_to_anymap(data), False) def clear_user_data(self): """ diff --git a/interfaces/cython/cantera/units.pxd b/interfaces/cython/cantera/units.pxd index 04e364fcaa..0fa70d99b0 100644 --- a/interfaces/cython/cantera/units.pxd +++ b/interfaces/cython/cantera/units.pxd @@ -12,7 +12,7 @@ cimport numpy as np cdef extern from "cantera/base/Units.h" namespace "Cantera": cdef cppclass CxxUnits "Cantera::Units": CxxUnits() - CxxUnits(CxxUnits) + CxxUnits(CxxUnits&) CxxUnits(string, cbool) except +translate_exception string str() double factor() @@ -22,16 +22,35 @@ cdef extern from "cantera/base/Units.h" namespace "Cantera": CxxUnitSystem() stdmap[string, string] defaults() void setDefaults(stdmap[string, string]&) except +translate_exception + double convert(CxxAnyValue&, string&) except +translate_exception + double convert(CxxAnyValue&, CxxUnits&) except +translate_exception + double convertTo(double, string&) except +translate_exception + double convertTo(double, CxxUnits&) except +translate_exception + double convertRateCoeff(CxxAnyValue&, CxxUnits&) except +translate_exception + double convertActivationEnergy(CxxAnyValue&, string&) except +translate_exception + double convertActivationEnergyTo(double, string&) except +translate_exception + double convertActivationEnergyTo(double, CxxUnits&) except +translate_exception cdef cppclass CxxUnitStack "Cantera::UnitStack": CxxUnitStack() + CxxUnitStack(CxxUnits&) + CxxUnitStack(CxxUnitStack&) CxxUnits product() + void join(double) except +translate_exception cdef class Units: cdef CxxUnits units @staticmethod - cdef copy(CxxUnits) + cdef Units copy(CxxUnits) + +cdef class UnitStack: + cdef CxxUnitStack stack + @staticmethod + cdef UnitStack copy(const CxxUnitStack&) + cdef class UnitSystem: - cdef CxxUnitSystem unitsystem + cdef _set_unitSystem(self, shared_ptr[CxxUnitSystem] units) + cdef shared_ptr[CxxUnitSystem] _unitsystem + cdef CxxUnitSystem* unitsystem diff --git a/interfaces/cython/cantera/units.pyx b/interfaces/cython/cantera/units.pyx index ae2bb5a3e7..9b590d5fce 100644 --- a/interfaces/cython/cantera/units.pyx +++ b/interfaces/cython/cantera/units.pyx @@ -2,6 +2,9 @@ # at https://cantera.org/license.txt for license and copyright information. from typing import Dict +from collections.abc import Sequence +import numbers +import numpy as np from ._utils cimport * @@ -41,7 +44,7 @@ cdef class Units: return self.units.dimension(stringify(primary)) @property - def dimensions(self) -> Dict[str, str]: + def dimensions(self) -> Dict[str, float]: """A dictionary of the primary unit components to their dimensions. .. versionadded:: 3.0 @@ -58,13 +61,32 @@ cdef class Units: return self.units.factor() @staticmethod - cdef copy(CxxUnits other): + cdef Units copy(CxxUnits other): """Copy a C++ Units object to a Python object.""" cdef Units units = Units() units.units = CxxUnits(other) return units +cdef class UnitStack: + def __cinit__(self): + self.stack = CxxUnitStack(CxxUnits()) + + @staticmethod + cdef UnitStack copy(const CxxUnitStack& other): + """Copy a C++ UnitStack object to a Python object.""" + cdef UnitStack stack = UnitStack() + stack.stack = CxxUnitStack(other) + return stack + + def product(self): + cdef CxxUnits units = self.stack.product() + return Units.copy(units) + + def join(self, exponent): + self.stack.join(exponent) + + cdef class UnitSystem: """ Unit system used for YAML input and output. @@ -74,6 +96,9 @@ cdef class UnitSystem: native unit system, which is SI units except for the use of kmol as the base unit of quantity, that is, kilogram, meter, second, kelvin, ampere, and kmol. + Generally, this class is used indirectly, through methods interacting with `AnyMap` + objects such as `ExtensibleRate.set_parameters` and `ExtensibleRate.get_parameters`. + The default unit system used by Cantera is SI+kmol:: ct.UnitSystem({ @@ -92,15 +117,24 @@ cdef class UnitSystem: the default unit system is retrieved as:: ct.UnitSystem() + """ def __cinit__(self, units=None): - self.unitsystem = CxxUnitSystem() + self._unitsystem.reset(new CxxUnitSystem()) + self.unitsystem = self._unitsystem.get() if units: self.units = units def __repr__(self): return f"" + cdef _set_unitSystem(self, shared_ptr[CxxUnitSystem] units): + self._unitsystem = units + self.unitsystem = self._unitsystem.get() + + def defaults(self): + return self.unitsystem.defaults() + property units: """ Units used by the unit system @@ -114,3 +148,105 @@ cdef class UnitSystem: for dimension, unit in units.items(): cxxunits[stringify(dimension)] = stringify(unit) self.unitsystem.setDefaults(cxxunits) + + def convert_to(self, quantity, dest): + """ + Convert *quantity* to the units defined by *dest*, using this `UnitSystem` to + define the default units of *quantity*. *quantity* can be one of the following: + + - A number, for example ``3.14`` + - A "quantity string" containing a number and a dimension string, separated by + a space. For example, ``"3.14 kmol/m^3"`` + - A NumPy array of either numeric values or quantity strings as described above + - A list, tuple, or other sequence of any shape containing numeric values or + quantity strings, For example ``("3000 mm", 3.14, "12 cm")`` + + *dest* can be a string or `Units` object specifying the destination units. + """ + cdef double value + cdef CxxAnyValue val_units + if isinstance(quantity, str): + val_units = python_to_anyvalue(quantity) + if isinstance(dest, str): + return self.unitsystem.convert(val_units, stringify(dest)) + elif isinstance(dest, Units): + return self.unitsystem.convert(val_units, (dest).units) + else: + raise TypeError("'dest' must be a string or 'Units' object") + elif isinstance(quantity, numbers.Real): + value = quantity + if isinstance(dest, str): + return self.unitsystem.convertTo(value, stringify(dest)) + elif isinstance(dest, Units): + return self.unitsystem.convertTo(value, (dest).units) + else: + raise TypeError("'dest' must be a string or 'Units' object") + elif isinstance(quantity, np.ndarray): + return np.vectorize(lambda item: self.convert_to(item, dest))(quantity) + elif isinstance(quantity, Sequence): + return [self.convert_to(item, dest) for item in quantity] + else: + raise TypeError("'quantity' must be either a string or a number") + + def convert_activation_energy_to(self, quantity, str dest): + """ + Convert *quantity* to the activation energy units defined by *dest*, using this + `UnitSystem` to define the default units of *quantity*. *quantity* can be one of + the following: + + - A number, for example ``3.14`` + - A "quantity string" containing a number and a dimension string, separated by + a space. For example, ``"3.14 J/kmol"`` + - A NumPy array of either numeric values or quantity strings as described above + - A list, tuple, or other sequence of any shape containing numeric values or + quantity strings, For example ``("30 kcal/mol", 3.14, "12000 K")`` + + *dest* can be a string or `Units` object specifying the destination units, which + must be interpretable as unit of energy per unit quantity (for example, J/kmol), + energy (for example, eV) or temperature (K). + """ + cdef double value + cdef CxxAnyValue val_units + if isinstance(quantity, str): + val_units = python_to_anyvalue(quantity) + return self.unitsystem.convertActivationEnergy(val_units, stringify(dest)) + elif isinstance(quantity, numbers.Real): + value = quantity + return self.unitsystem.convertActivationEnergyTo(value, stringify(dest)) + elif isinstance(quantity, np.ndarray): + return np.vectorize( + lambda item: self.convert_activation_energy_to(item, dest))(quantity) + elif isinstance(quantity, Sequence): + return [self.convert_activation_energy_to(item, dest) for item in quantity] + else: + raise TypeError("'quantity' must be either a string or a number") + + def convert_rate_coeff_to(self, quantity, dest): + """ + Convert a *quantity* representing a rate coefficient to the units defined by + *dest*, using this `UnitSystem` to define the default units of *quantity*. + Behaves similar to`convert_to` but with special handling for the case of + standalone rate constants, where the destination units are not actually known, + and where the units may be specified using `Units` or `UnitStack` objects. + """ + cdef Units dest_units + if isinstance(dest, Units): + dest_units = dest + elif isinstance(dest, UnitStack): + dest_units = dest.product() + elif isinstance(dest, str): + dest_units = Units(dest) + else: + raise TypeError("'dest' must be a string, Units, or UnitStack object") + + cdef CxxAnyValue val_units + if isinstance(quantity, (str, numbers.Real)): + val_units = python_to_anyvalue(quantity) + return self.unitsystem.convertRateCoeff(val_units, dest_units.units) + elif isinstance(quantity, np.ndarray): + return np.vectorize( + lambda q: self.convert_rate_coeff_to(q, dest_units))(quantity) + elif isinstance(quantity, Sequence): + return [self.convert_rate_coeff_to(q, dest_units) for q in quantity] + else: + raise TypeError("'quantity' must be either a string or a number") diff --git a/interfaces/cython/cantera/yamlwriter.pxd b/interfaces/cython/cantera/yamlwriter.pxd index bb4bb89aa9..c1cc20148a 100644 --- a/interfaces/cython/cantera/yamlwriter.pxd +++ b/interfaces/cython/cantera/yamlwriter.pxd @@ -24,4 +24,4 @@ cdef class YamlWriter: cdef shared_ptr[CxxYamlWriter] _writer cdef CxxYamlWriter* writer @staticmethod - cdef CxxUnitSystem _get_unitsystem(UnitSystem units) + cdef shared_ptr[CxxUnitSystem] _get_unitsystem(UnitSystem units) diff --git a/interfaces/cython/cantera/yamlwriter.pyx b/interfaces/cython/cantera/yamlwriter.pyx index a4aa0e0ef5..ab9270a59f 100644 --- a/interfaces/cython/cantera/yamlwriter.pyx +++ b/interfaces/cython/cantera/yamlwriter.pyx @@ -3,6 +3,7 @@ from .solutionbase cimport * from ._utils cimport * +from cython.operator import dereference as deref cdef class YamlWriter: """ @@ -64,12 +65,11 @@ cdef class YamlWriter: def __set__(self, units): if not isinstance(units, UnitSystem): units = UnitSystem(units) - cdef CxxUnitSystem cxxunits = YamlWriter._get_unitsystem(units) - self.writer.setUnitSystem(cxxunits) + self.writer.setUnitSystem(deref(YamlWriter._get_unitsystem(units).get())) @staticmethod - cdef CxxUnitSystem _get_unitsystem(UnitSystem units): - return units.unitsystem + cdef shared_ptr[CxxUnitSystem] _get_unitsystem(UnitSystem units): + return units._unitsystem def __reduce__(self): raise NotImplementedError('YamlWriter object is not picklable') diff --git a/samples/python/kinetics/custom_reactions.py b/samples/python/kinetics/custom_reactions.py index af98b40abe..6aefffe351 100644 --- a/samples/python/kinetics/custom_reactions.py +++ b/samples/python/kinetics/custom_reactions.py @@ -52,9 +52,18 @@ def update(self, gas): class ExtensibleArrhenius(ct.ExtensibleRate): __slots__ = ("A", "b", "Ea_R") def set_parameters(self, params, units): - self.A = params["A"] + self.A = params.convert_rate_coeff("A", units) self.b = params["b"] - self.Ea_R = params["Ea_R"] + self.Ea_R = params.convert_activation_energy("Ea", "K") + + def get_parameters(self, params): + params.set_quantity("A", self.A, self.conversion_units) + params["b"] = self.b + params.set_activation_energy("Ea", self.Ea_R, "K") + + def validate(self, equation, soln): + if self.A < 0: + raise ValueError(f"Found negative 'A' for reaction {equation}") def eval(self, data): return self.A * data.T**self.b * exp(-self.Ea_R/data.T) @@ -62,17 +71,19 @@ def eval(self, data): extensible_yaml2 = """ equation: H2 + O <=> H + OH type: extensible-Arrhenius - A: 38.7 + units: {length: cm, quantity: mol, activation-energy: cal/mol} + A: 3.87e+04 b: 2.7 - Ea_R: 3150.1542797022735 + Ea: 6260.0 """ extensible_yaml4 = """ equation: H2O2 + O <=> HO2 + OH type: extensible-Arrhenius - A: 9630.0 + units: {length: cm, quantity: mol, activation-energy: cal/mol} + A: 9.63e+06 b: 2 - Ea_R: 2012.8781339950629 + Ea: 4000 """ extensible_reactions = gas0.reactions() diff --git a/src/base/ExtensionManager.cpp b/src/base/ExtensionManager.cpp index 35865eb011..28a476ce2b 100644 --- a/src/base/ExtensionManager.cpp +++ b/src/base/ExtensionManager.cpp @@ -11,6 +11,7 @@ namespace Cantera map> ExtensionManager::s_ReactionData_linkers = {}; map(shared_ptr)>> ExtensionManager::s_Solution_linkers = {}; +map ExtensionManager::s_userTypeToWrapperType = {}; void ExtensionManager::wrapReactionData(const string& rateName, ReactionDataDelegator& data) @@ -25,9 +26,10 @@ void ExtensionManager::wrapReactionData(const string& rateName, } void ExtensionManager::registerReactionDataLinker(const string& rateName, - function link) + const string& wrapperName, function link) { s_ReactionData_linkers[rateName] = link; + s_userTypeToWrapperType[rateName] = wrapperName; } shared_ptr ExtensionManager::wrapSolution( @@ -48,4 +50,14 @@ void ExtensionManager::registerSolutionLinker(const string& rateName, s_Solution_linkers[rateName] = link; } +string ExtensionManager::getSolutionWrapperType(const string& userType) +{ + if (s_userTypeToWrapperType.count(userType)) { + return s_userTypeToWrapperType[userType]; + } else { + throw CanteraError("ExtensionManager::getSolutionWrapperType", + "No Solution linker for type {} registered", userType); + } +} + } diff --git a/src/base/Solution.cpp b/src/base/Solution.cpp index 51cf89f064..eb5ecffbf4 100644 --- a/src/base/Solution.cpp +++ b/src/base/Solution.cpp @@ -311,7 +311,7 @@ shared_ptr newSolution(const AnyMap& phaseNode, for (size_t i = 0; i < sol->nAdjacent(); i++) { phases.push_back(sol->adjacent(i)->thermo()); } - sol->setKinetics(newKinetics(phases, phaseNode, rootNode)); + sol->setKinetics(newKinetics(phases, phaseNode, rootNode, sol)); // set transport model by name sol->setTransportModel(transport); diff --git a/src/base/Units.cpp b/src/base/Units.cpp index 6f5fc638ee..da5bf9f472 100644 --- a/src/base/Units.cpp +++ b/src/base/Units.cpp @@ -629,6 +629,32 @@ double UnitSystem::convert(const AnyValue& v, const Units& dest) const } } +double UnitSystem::convertRateCoeff(const AnyValue& v, const Units& dest) const +{ + if (dest.factor() != 0) { + // If the destination units are defined, no special handling is required + return convert(v, dest); + } + + auto [value, units] = split_unit(v); + if (units.empty()) { + if (m_length_factor == 1.0 && m_quantity_factor == 1.0) { + // Input is a number in the default mks+kmol system, so no conversion is + // required + return value; + } + } else { + Units sourceUnits(units); + if (fabs(sourceUnits.factor() - 1.0) < 1e-14) { + // Input is explicitly in the mks+kmol system, so no conversion is required + return value; + } + } + throw InputFileError("UnitSystem::convertRateCoeff", v, + "Unable to convert value with non-default units to undefined units,\n" + "likely while creating a standalone ReactionRate object."); +} + vector_fp UnitSystem::convert(const std::vector& vals, const std::string& dest) const { diff --git a/src/extensions/PythonExtensionManager.cpp b/src/extensions/PythonExtensionManager.cpp index b6b1a52446..467714635f 100644 --- a/src/extensions/PythonExtensionManager.cpp +++ b/src/extensions/PythonExtensionManager.cpp @@ -98,8 +98,11 @@ void PythonExtensionManager::registerRateBuilder( //! Call setParameters after the delegated functions have been connected delegator->setParameters(params, units); - // The delegator is responsible for eventually deleting the Python object - delegator->holdExternalHandle(make_shared(extRate, false)); + // The delegator needs to hold a reference to the Python object to prevent + // garbage collection for as long as the delegator exists. Breaking the + // reference cycle is handled on the Python side. + delegator->holdExternalHandle("python", + make_shared(extRate, false)); return delegator.release(); }; ReactionRateFactory::factory()->reg(rateName, builder); @@ -114,7 +117,6 @@ void PythonExtensionManager::registerRateDataBuilder( // object and a Python ExtensibleRateData object of a particular type, and register // this function for making that link auto builder = [moduleName, className](ReactionDataDelegator& delegator) { - delegator.setSolutionWrapperType("python"); PyObject* extData = ct_newPythonExtensibleRateData(&delegator, moduleName, className); if (extData == nullptr) { @@ -124,7 +126,7 @@ void PythonExtensionManager::registerRateDataBuilder( } delegator.setWrapper(make_shared(extData, false)); }; - mgr.registerReactionDataLinker(rateName, builder); + mgr.registerReactionDataLinker(rateName, "python", builder); // Create a function that will link a Python Solution object to the C++ Solution // object that gets passed to the Reaction diff --git a/src/kinetics/Arrhenius.cpp b/src/kinetics/Arrhenius.cpp index 1b2df13585..32cbece710 100644 --- a/src/kinetics/Arrhenius.cpp +++ b/src/kinetics/Arrhenius.cpp @@ -23,6 +23,7 @@ ArrheniusBase::ArrheniusBase(double A, double b, double Ea) ArrheniusBase::ArrheniusBase(const AnyValue& rate, const UnitSystem& units, const UnitStack& rate_units) { + setRateUnits(rate_units); setRateParameters(rate, units, rate_units); } @@ -40,27 +41,14 @@ void ArrheniusBase::setRateParameters( m_A = NAN; m_b = NAN; m_logA = NAN; - m_order = NAN; - m_rate_units = Units(0.); + setRateUnits(Units(0.)); return; } - setRateUnits(rate_units); if (rate.is()) { auto& rate_map = rate.as(); - if (m_rate_units.factor() == 0) { - // A zero rate units factor is used as a sentinel to detect - // stand-alone reaction rate objects - if (rate_map[m_A_str].is()) { - throw InputFileError("ArrheniusBase::setRateParameters", rate_map, - "Specification of units is not supported for pre-exponential " - "factor when\ncreating a standalone 'ReactionRate' object."); - } - m_A = rate_map[m_A_str].asDouble(); - } else { - m_A = units.convert(rate_map[m_A_str], m_rate_units); - } + m_A = units.convertRateCoeff(rate_map[m_A_str], conversionUnits()); m_b = rate_map[m_b_str].asDouble(); if (rate_map.hasKey(m_Ea_str)) { m_Ea_R = units.convertActivationEnergy(rate_map[m_Ea_str], "K"); @@ -70,7 +58,7 @@ void ArrheniusBase::setRateParameters( } } else { auto& rate_vec = rate.asVector(2, 4); - m_A = units.convert(rate_vec[0], m_rate_units); + m_A = units.convertRateCoeff(rate_vec[0], conversionUnits()); m_b = rate_vec[1].asDouble(); if (rate_vec.size() > 2) { m_Ea_R = units.convertActivationEnergy(rate_vec[2], "K"); @@ -92,8 +80,8 @@ void ArrheniusBase::getRateParameters(AnyMap& node) const return; } - if (m_rate_units.factor() != 0.0) { - node[m_A_str].setQuantity(m_A, m_rate_units); + if (conversionUnits().factor() != 0.0) { + node[m_A_str].setQuantity(m_A, conversionUnits()); } else { node[m_A_str] = m_A; // This can't be converted to a different unit system because the dimensions of @@ -130,7 +118,6 @@ void ArrheniusBase::getParameters(AnyMap& node) const { // RateType object is configured node["rate-constant"] = std::move(rateNode); } - node["type"] = type(); } void ArrheniusBase::check(const std::string& equation) diff --git a/src/kinetics/ChebyshevRate.cpp b/src/kinetics/ChebyshevRate.cpp index 71f173b6ea..96cb70b019 100644 --- a/src/kinetics/ChebyshevRate.cpp +++ b/src/kinetics/ChebyshevRate.cpp @@ -63,7 +63,6 @@ ChebyshevRate::ChebyshevRate(const AnyMap& node, const UnitStack& rate_units) void ChebyshevRate::setParameters(const AnyMap& node, const UnitStack& rate_units) { ReactionRate::setParameters(node, rate_units); - m_rate_units = rate_units.product(); const UnitSystem& unit_system = node.units(); Array2D coeffs(0, 0); if (node.hasKey("data")) { @@ -80,9 +79,8 @@ void ChebyshevRate::setParameters(const AnyMap& node, const UnitStack& rate_unit coeffs(i, j) = vcoeffs[i][j]; } } - if (m_rate_units.factor()) { - coeffs(0, 0) += std::log10(unit_system.convertTo(1.0, m_rate_units)); - } + double offset = unit_system.convertRateCoeff(AnyValue(1.0), conversionUnits()); + coeffs(0, 0) += std::log10(offset); setLimits( unit_system.convert(T_range[0], "K"), unit_system.convert(T_range[1], "K"), @@ -127,7 +125,6 @@ void ChebyshevRate::setData(const Array2D& coeffs) void ChebyshevRate::getParameters(AnyMap& rateNode) const { - rateNode["type"] = type(); if (!valid()) { // object not fully set up return; @@ -144,7 +141,7 @@ void ChebyshevRate::getParameters(AnyMap& rateNode) const } // Unit conversions must take place later, after the destination unit system // is known. A lambda function is used here to override the default behavior - Units rate_units2 = m_rate_units; + Units rate_units2 = conversionUnits(); auto converter = [rate_units2](AnyValue& coeffs, const UnitSystem& units) { if (rate_units2.factor() != 0.0) { coeffs.asVector()[0][0] += \ diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index e4bd34e81f..82a6d9a779 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -148,10 +148,9 @@ void FalloffRate::setParameters(const AnyMap& node, const UnitStack& rate_units) UnitStack high_rate_units = rate_units; if (rate_units.size()) { if (m_chemicallyActivated) { - low_rate_units.join(1); - high_rate_units.join(2); - } else { high_rate_units.join(1); + } else { + low_rate_units.join(-1); } } if (node.hasKey("low-P-rate-constant")) { @@ -168,11 +167,6 @@ void FalloffRate::setParameters(const AnyMap& node, const UnitStack& rate_units) void FalloffRate::getParameters(AnyMap& node) const { - if (m_chemicallyActivated) { - node["type"] = "chemically-activated"; - } else { - node["type"] = "falloff"; - } if (m_negativeA_ok) { node["negative-A"] = true; } @@ -348,22 +342,13 @@ void TroeRate::getParameters(AnyMap& node) const FalloffRate::getParameters(node); AnyMap params; - if (!valid()) { - // pass - } else if (m_lowRate.rateUnits().factor() != 0.0) { + if (valid()) { params["A"] = m_a; params["T3"].setQuantity(1.0 / m_rt3, "K"); params["T1"].setQuantity(1.0 / m_rt1, "K"); if (std::abs(m_t2) > SmallNumber) { params["T2"].setQuantity(m_t2, "K"); } - } else { - params["A"] = m_a; - params["T3"] = 1.0 / m_rt3; - params["T1"] = 1.0 / m_rt1; - if (std::abs(m_t2) > SmallNumber) { - params["T2"] = m_t2; - } } params.setFlowStyle(); node["Troe"] = std::move(params); @@ -476,9 +461,7 @@ void SriRate::getParameters(AnyMap& node) const FalloffRate::getParameters(node); AnyMap params; - if (!valid()) { - // pass - } else if (m_lowRate.rateUnits().factor() != 0.0) { + if (valid()) { params["A"] = m_a; params["B"].setQuantity(m_b, "K"); params["C"].setQuantity(m_c, "K"); @@ -486,14 +469,6 @@ void SriRate::getParameters(AnyMap& node) const params["D"] = m_d; params["E"] = m_e; } - } else { - params["A"] = m_a; - params["B"] = m_b; - params["C"] = m_c; - if (m_d != 1.0 || m_e != 0.0) { - params["D"] = m_d; - params["E"] = m_e; - } } params.setFlowStyle(); node["SRI"] = std::move(params); diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index 7867206a74..9d4e9ddd96 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -675,7 +675,7 @@ bool Kinetics::addReaction(shared_ptr r, bool resize) // For reactions created outside the context of a Kinetics object, the units // of the rate coefficient can't be determined in advance. Do that here. if (r->rate_units.factor() == 0) { - r->calculateRateCoeffUnits(*this); + r->rate()->setRateUnits(r->calculateRateCoeffUnits(*this)); } size_t irxn = nReactions(); // index of the new reaction diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index 26c22d2bfa..99aab763c1 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -11,6 +11,7 @@ #include "cantera/kinetics/EdgeKinetics.h" #include "cantera/thermo/ThermoPhase.h" #include "cantera/base/stringUtils.h" +#include "cantera/base/Solution.h" #include @@ -69,7 +70,8 @@ shared_ptr newKinetics(const string& model) shared_ptr newKinetics(const vector>& phases, const AnyMap& phaseNode, - const AnyMap& rootNode) + const AnyMap& rootNode, + shared_ptr soln) { std::string kinType = phaseNode.getString("kinetics", "none"); kinType = KineticsFactory::factory()->canonicalize(kinType); @@ -88,6 +90,9 @@ shared_ptr newKinetics(const vector>& phases, } shared_ptr kin(KineticsFactory::factory()->newKinetics(kinType)); + if (soln) { + soln->setKinetics(kin); + } for (auto& phase : phases) { kin->addThermo(phase); } diff --git a/src/kinetics/PlogRate.cpp b/src/kinetics/PlogRate.cpp index b6ecde731a..6666f13a74 100644 --- a/src/kinetics/PlogRate.cpp +++ b/src/kinetics/PlogRate.cpp @@ -79,7 +79,6 @@ void PlogRate::setParameters(const AnyMap& node, const UnitStack& rate_units) void PlogRate::getParameters(AnyMap& rateNode, const Units& rate_units) const { std::vector rateList; - rateNode["type"] = type(); if (!valid()) { // object not fully set up return; diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 8db9f4fd95..d61ed74c00 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -76,6 +76,11 @@ Reaction::Reaction(const AnyMap& node, const Kinetics& kin) setParameters(node, kin); size_t nDim = kin.thermo(kin.reactionPhaseIndex()).nDim(); + if (!valid()) { + // If the reaction isn't valid (for example, contains undefined species), + // setting up the rate constant won't work + return; + } if (nDim == 3) { if (ba::starts_with(rate_type, "three-body-")) { AnyMap rateNode = node; @@ -134,7 +139,9 @@ void Reaction::check() // Check reaction rate evaluator to ensure changes introduced after object // instantiation are considered. - m_rate->check(equation()); + if (m_rate) { + m_rate->check(equation()); + } } AnyMap Reaction::parameters(bool withInput) const @@ -536,11 +543,12 @@ UnitStack Reaction::calculateRateCoeffUnits(const Kinetics& kin) } } - if (m_third_body) { + if (m_third_body && m_third_body->mass_action) { // Account for third-body collision partner as the last entry rate_units.join(-1); } + Reaction::rate_units = rate_units.product(); return rate_units; } diff --git a/src/kinetics/ReactionRateDelegator.cpp b/src/kinetics/ReactionRateDelegator.cpp index e28823f3c2..17a4fdb299 100644 --- a/src/kinetics/ReactionRateDelegator.cpp +++ b/src/kinetics/ReactionRateDelegator.cpp @@ -23,17 +23,17 @@ ReactionDataDelegator::ReactionDataDelegator() bool ReactionDataDelegator::update(const ThermoPhase& phase, const Kinetics& kin) { if (!m_wrappedSolution) { + auto wrapperType = ExtensionManager::getSolutionWrapperType(m_rateType); auto soln = kin.root(); if (!soln) { throw CanteraError("ReactionDataDelegator::update", "Phase must be instantiated as a Solution to use extensible " "reactions of type '{}'", m_rateType); } - if (soln->getExternalHandle(m_solutionWrapperType)) { - m_wrappedSolution = soln->getExternalHandle(m_solutionWrapperType); + if (soln->getExternalHandle(wrapperType)) { + m_wrappedSolution = soln->getExternalHandle(wrapperType); } else { - m_wrappedSolution = ExtensionManager::wrapSolution(m_solutionWrapperType, - soln); + m_wrappedSolution = ExtensionManager::wrapSolution(wrapperType, soln); } } double needsUpdate = m_update(m_wrappedSolution->get()); @@ -51,6 +51,11 @@ ReactionRateDelegator::ReactionRateDelegator() install("setParameters", m_setParameters, [this](const AnyMap& node, const UnitStack& units) { ReactionRate::setParameters(node, units); }); + install("getParameters", m_getParameters, + [this](AnyMap& node) { ReactionRate::getParameters(node); }); + install("validate", m_validate, + [](const string& equation, void* soln) { + throw NotImplementedError("ReactionRateDelegator::validate"); }); } unique_ptr ReactionRateDelegator::newMultiRate() const @@ -62,4 +67,26 @@ unique_ptr ReactionRateDelegator::newMultiRate() const return multirate; } +void ReactionRateDelegator::validate(const string& equation, const Kinetics& kin) +{ + auto soln = kin.root(); + if (!soln) { + throw CanteraError("ReactionRateDelegator::validate", + "Phase must be instantiated as a Solution to use extensible " + "reactions of type '{}'", m_rateType); + } + auto wrapperType = ExtensionManager::getSolutionWrapperType(m_rateType); + auto wrappedSoln = soln->getExternalHandle(wrapperType); + if (!wrappedSoln) { + wrappedSoln = ExtensionManager::wrapSolution(wrapperType, soln); + } + + try { + m_validate(equation, wrappedSoln->get()); + } catch (CanteraError& err) { + throw InputFileError("'" + m_rateType + "' validate", m_input, + err.getMessage()); + } +} + } diff --git a/src/kinetics/ReactionRateFactory.cpp b/src/kinetics/ReactionRateFactory.cpp index 7969b4e4c1..b39b3f7abf 100644 --- a/src/kinetics/ReactionRateFactory.cpp +++ b/src/kinetics/ReactionRateFactory.cpp @@ -151,15 +151,7 @@ shared_ptr newReactionRate( 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, UnitStack({})); + return newReactionRate(AnyMap(rate_node), UnitStack({})); } } diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index 4ffba13164..77ddad0d82 100644 --- a/test/kinetics/kineticsFromYaml.cpp +++ b/test/kinetics/kineticsFromYaml.cpp @@ -3,6 +3,7 @@ #include "cantera/base/Solution.h" #include "cantera/base/Interface.h" #include "cantera/kinetics/KineticsFactory.h" +#include "cantera/kinetics/ReactionRateFactory.h" #include "cantera/kinetics/GasKinetics.h" #include "cantera/kinetics/Arrhenius.h" #include "cantera/kinetics/ChebyshevRate.h" @@ -32,6 +33,23 @@ TEST(ReactionRate, ModifyArrheniusRate) EXPECT_FALSE(rr->allowNegativePreExponentialFactor()); } +TEST(ReactionRate, ArrheniusUnits) +{ + AnyMap rxn1 = AnyMap::fromYamlString( + "{rate-constant: [2.70000E+13 cm^3/mol/s, 0, 355 cal/mol]}"); + ASSERT_THROW(newReactionRate(rxn1), InputFileError); + + AnyMap rxn2 = AnyMap::fromYamlString( + "{units: {quantity: mol, length: cm},\n" + "nested: {rate-constant: [27.0, 0, 355 cal/mol]}}"); + rxn2.applyUnits(); + ASSERT_THROW(newReactionRate(rxn2["nested"].as()), InputFileError); + + AnyMap rxn3 = AnyMap::fromYamlString( + "{rate-constant: [2.70000E+13 m^3/kmol/s, 0, 355 cal/mol]}"); + auto rate = newReactionRate(rxn3); +} + TEST(Reaction, ElementaryFromYaml) { auto sol = newSolution("gri30.yaml", "", "none"); diff --git a/test/python/test_kinetics.py b/test/python/test_kinetics.py index c9727e1cca..bd5500f484 100644 --- a/test/python/test_kinetics.py +++ b/test/python/test_kinetics.py @@ -1590,7 +1590,7 @@ def test_modify_invalid(self): def test_modify_elementary(self): gas = ct.Solution('h2o2.yaml', transport_model=None) gas.TPX = self.gas.TPX - R = self.gas.reaction(2) + R = gas.reaction(2) A1 = R.rate.pre_exponential_factor b1 = R.rate.temperature_exponent Ta1 = R.rate.activation_energy / ct.gas_constant @@ -1607,7 +1607,7 @@ def test_modify_elementary(self): def test_modify_third_body(self): gas = ct.Solution('h2o2.yaml', transport_model=None) gas.TPX = self.gas.TPX - R = self.gas.reaction(5) + R = gas.reaction(5) A1 = R.rate.pre_exponential_factor b1 = R.rate.temperature_exponent T = gas.T diff --git a/test/python/test_reaction.py b/test/python/test_reaction.py index 39661a0e6b..f22d74f45c 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -213,11 +213,13 @@ def test_roundtrip(self): self.check_rate(rate1) def test_with_units(self): - # test custom units + # test custom units. Sticking coefficients are dimensionless, so this is only + # a concern for other rate types units = "units: {length: cm, quantity: mol}" yaml = f"{textwrap.dedent(self._yaml)}\n{units}" - with self.assertRaisesRegex(Exception, "not supported"): - ct.ReactionRate.from_yaml(yaml) + if "sticking" not in yaml: + with self.assertRaisesRegex(Exception, "undefined units"): + ct.ReactionRate.from_yaml(yaml) @pytest.mark.usefixtures("has_temperature_derivative_warnings") def test_derivative_ddT(self): @@ -275,7 +277,7 @@ def test_negative_A(self): def test_standalone(self): # test creation with unsupported alternative units yaml = "rate-constant: {A: 4.0e+21 cm^6/mol^2/s, b: 0.0, Ea: 1207.72688}" - with self.assertRaisesRegex(Exception, "not supported"): + with self.assertRaisesRegex(Exception, "undefined units"): ct.ReactionRate.from_yaml(yaml) @pytest.mark.usefixtures("has_temperature_derivative_warnings") @@ -646,7 +648,7 @@ def test_standalone(self): - {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"): + with self.assertRaisesRegex(Exception, "undefined units"): ct.ReactionRate.from_yaml(yaml) @@ -940,6 +942,7 @@ class ReactionTests: _index = None # index of reaction in "kineticsfromscratch.yaml" _rate_type = None # name of reaction rate type _yaml = None # YAML parameterization + _rc_units = None # Units of the rate coefficient @classmethod def setUpClass(cls): @@ -1104,6 +1107,14 @@ def test_roundtrip(self): rxn2 = self.from_rate(rate_obj) self.check_rxn(rxn2) + def test_rate_coeff_units(self): + rxn = self.from_yaml() + assert str(rxn.rate_coeff_units) == str(self._rc_units) + + def test_rate_conversion_units(self): + rxn = self.from_yaml() + assert str(rxn.rate.conversion_units) == str(self._rc_units) + def check_equal(self, one, two): # helper function for deprecation tests self.assertEqual(type(one), type(two)) @@ -1128,6 +1139,7 @@ class TestElementary(ReactionTests, utilities.CanteraTest): type: elementary rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol} """ + _rc_units = ct.Units("m^3 / kmol / s") @classmethod def setUpClass(cls): @@ -1148,6 +1160,7 @@ class TestThreeBody(TestElementary): rate-constant: {A: 1.2e+11, b: -1.0, Ea: 0.0 cal/mol} efficiencies: {H2: 2.4, H2O: 15.4, AR: 0.83} """ + _rc_units = ct.Units("m^6 / kmol^2 / s") def test_efficiencies(self): # check efficiencies @@ -1174,6 +1187,7 @@ class TestImplicitThreeBody(TestThreeBody): equation: H + 2 O2 <=> HO2 + O2 rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0} """ + _rc_units = ct.Units("m^6 / kmol^2 / s") def test_efficiencies(self): # overload of default tester @@ -1205,6 +1219,7 @@ class TestTwoTempPlasma(ReactionTests, utilities.CanteraTest): type: two-temperature-plasma rate-constant: {A: 17283, b: -3.1, Ea-gas: -5820 J/mol, Ea-electron: 1081 J/mol} """ + _rc_units = ct.Units("m^3 / kmol / s") def eval_rate(self, rate): return rate(self.soln.T, self.soln.Te) @@ -1230,6 +1245,7 @@ class TestBlowersMasel(ReactionTests, utilities.CanteraTest): type: Blowers-Masel rate-constant: {A: 38700, b: 2.7, Ea0: 2.619184e4 cal/mol, w: 4.184e9 cal/mol} """ + _rc_units = ct.Units("m^3 / kmol / s") def eval_rate(self, rate): rate.delta_enthalpy = self.soln.delta_enthalpy[self._index] @@ -1248,6 +1264,7 @@ class TestThreeBodyBlowersMasel(TestBlowersMasel): type: Blowers-Masel rate-constant: {A: 38700, b: 2.7, Ea0: 2.619184e4 cal/mol, w: 4.184e9 cal/mol} """ + _rc_units = ct.Units("m^6 / kmol^2 / s") class TestTroe(ReactionTests, utilities.CanteraTest): @@ -1272,6 +1289,7 @@ class TestTroe(ReactionTests, utilities.CanteraTest): Troe: {A: 0.7346, T3: 94.0, T1: 1756.0, T2: 5182.0} efficiencies: {AR: 0.7, H2: 2.0, H2O: 6.0} """ + _rc_units = ct.Units("m^3 / kmol / s") @classmethod def setUpClass(cls): @@ -1310,6 +1328,7 @@ class TestLindemann(ReactionTests, utilities.CanteraTest): high-P-rate-constant: {A: 7.4e+10, b: -0.37, Ea: 0.0 cal/mol} efficiencies: {AR: 0.7, H2: 2.0, H2O: 6.0} """ + _rc_units = ct.Units("m^3 / kmol / s") @classmethod def setUpClass(cls): @@ -1344,6 +1363,7 @@ class TestChemicallyActivated(ReactionTests, utilities.CanteraTest): low-P-rate-constant: [282320.078, 1.46878, -3270.56495] high-P-rate-constant: [5.88E-14, 6.721, -3022.227] """ + _rc_units = ct.Units("m^3 / kmol / s") @classmethod def setUpClass(cls): @@ -1383,6 +1403,7 @@ class TestPlog(ReactionTests, utilities.CanteraTest): - {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} """ + _rc_units = ct.Units("m^3 / kmol / s") @classmethod def setUpClass(cls): @@ -1433,6 +1454,7 @@ class TestChebyshev(ReactionTests, utilities.CanteraTest): - [1.9764, 1.0037, 7.2865e-03, -0.030432] - [0.3177, 0.26889, 0.094806, -7.6385e-03] """ + _rc_units = ct.Units("1 / s") def eval_rate(self, rate): return rate(self.soln.T, self.soln.P) @@ -1448,6 +1470,7 @@ class TestCustom(ReactionTests, utilities.CanteraTest): _index = 0 _rate_type = "custom-rate-function" _yaml = None + _rc_units = ct.Units("m^3 / kmol / s") def setUp(self): # need to overwrite rate to ensure correct type ("method" is not compatible with Func1) @@ -1516,18 +1539,30 @@ def update(self, gas): @ct.extension(name="user-rate-1", data=UserRate1Data) class UserRate1(ct.ExtensibleRate): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + # Do default initialization before calling parent init since that init function + # may call set_parameters and we don't want to overwrite those values self.A = np.nan + self.eval_error = False + super().__init__(*args, **kwargs) def set_parameters(self, params, units): self.A = params["A"] + def get_parameters(self, params): + params["A"] = self.A + + def validate(self, equation, soln): + if np.isnan(self.A): + raise ValueError("'A' is NaN") + def eval(self, data): + if self.eval_error: + raise ValueError("Error evaluating rate") return self.A * data.T**2.7 * exp(-3150.15428/data.T) class TestExtensible(ReactionTests, utilities.CanteraTest): - # test Extensible reaction rate + # test general functionality of ExtensibleRate _phase_def = """ phases: - name: gas @@ -1552,6 +1587,7 @@ class TestExtensible(ReactionTests, utilities.CanteraTest): type: user-rate-1 A: 38.7 """ + _rc_units = ct.Units("m^3 / kmol / s") def setUp(self): super().setUp() @@ -1564,27 +1600,37 @@ def eval_rate(self, rate): return gas.forward_rate_constants[0] def test_no_rate(self): - pytest.skip("ExtensibleRate does not yet support validation") - - def test_from_dict(self): - pytest.skip("ExtensibleRate does not yet support serialization") + # Slightly different from the base case since we normally check evaluation via + # a Kinetics object, which will fail validation + rxn = self.from_empty() + with pytest.raises(ct.CanteraError, match="validate"): + self.eval_rate(rxn.rate) - def test_roundtrip(self): - pytest.skip("ExtensibleRate does not yet support roundtrip conversion") + def test_parameter_access(self): + gas = ct.Solution(yaml=self._phase_def) + R = ct.Reaction.from_yaml(self._yaml, gas) + assert R.rate.A == 38.7 def test_set_parameters_error(self): with pytest.raises(KeyError): # Instantiate with no A factor ct.ReactionRate.from_dict({"type": "user-rate-1"}) + def test_standalone_rate(self): + R = ct.ReactionRate.from_dict({"type": "user-rate-1", "A": 101}) + assert R.type == "user-rate-1" + def test_eval_error(self): # Instantiate with non-numeric A factor to cause an exception during evaluation - R = ct.ReactionRate.from_dict({"type": "user-rate-1", "A": "xyz"}) - with pytest.raises(TypeError): + R = ct.ReactionRate.from_dict({"type": "user-rate-1", "A": 12}) + R.eval_error = True + with pytest.raises(ValueError): self.eval_rate(R) class TestExtensible2(utilities.CanteraTest): + # Test handling of ExtensibleRate defined in a separate Python module + _input_template = """ extensions: - type: python @@ -1625,7 +1671,9 @@ def test_memory_management(self): # mixed Python/C++ ownership cycles import user_ext - gc.collect() + for _ in range(3): + gc.collect() + initialRate = user_ext.SquareRate.use_count[0] initialData = user_ext.SquareRateData.use_count[0] @@ -1635,15 +1683,107 @@ def run(): assert user_ext.SquareRate.use_count[0] == initialRate + 1 assert user_ext.SquareRateData.use_count[0] == initialData + 1 + standalone = ct.ReactionRate.from_dict({"type": "square-rate", "A": 101}) + assert user_ext.SquareRate.use_count[0] == initialRate + 2 + assert user_ext.SquareRateData.use_count[0] == initialData + 1 + run() # The number of instances for both classes should go back to its previous value # after deleting the Solution (may not be zero due to other Solution instances) - # held by other test classes - gc.collect() + # held by other test classes. Takes a few GC runs to clean up reference cycles + for _ in range(3): + gc.collect() assert user_ext.SquareRate.use_count[0] == initialRate assert user_ext.SquareRateData.use_count[0] == initialData + +@ct.extension(name="user-rate-2", data=UserRate1Data) +class UserRate2(ct.ExtensibleRate): + def set_parameters(self, params, rc_units): + self.A = params.convert_rate_coeff("A", rc_units) + self.length = params.convert("L", "m") + self.Ta = params.convert_activation_energy("Ea", "K") + + def get_parameters(self, params): + params.set_quantity("A", self.A, self.conversion_units) + params.set_quantity("L", self.length, "m") + params.set_activation_energy("Ea", self.Ta, "K") + + def validate(self, equation, soln): + if self.length < 0: + raise ValueError(f"Negative length found in reaction {equation}") + + def eval(self, data): + return self.A * (self.length / 2.0)**2 * exp(-self.Ta/data.T) + +class TestExtensible3(utilities.CanteraTest): + # Additional ExtensibleRate tests + + def setUp(self): + self.gas = ct.Solution('h2o2.yaml', transport_model=None) + + def test_explicit_units(self): + rxn = """ + equation: H2 + OH = H2O + H + type: user-rate-2 + A: 1000 cm^3/kmol/s + L: 200 cm + Ea: 1000 + """ + rxn = ct.Reaction.from_yaml(rxn, kinetics=self.gas) + assert rxn.rate.length == 2 + assert rxn.rate.Ta == pytest.approx(1000 / ct.gas_constant) + + def test_implicit_units(self): + rxn = """ + equation: H2 + OH = H2O + H + units: {length: cm} + type: user-rate-2 + A: 1000 + L: 200 + Ea: 1000 + """ + rxn = ct.Reaction.from_yaml(rxn, kinetics=self.gas) + assert rxn.rate.length == 2 + assert rxn.rate.Ta == pytest.approx(1000 / ct.gas_constant) + + def test_output_units(self): + rxn = """ + equation: H2 + OH = H2O + H + type: user-rate-2 + A: 1000 + L: 200 + Ea: 50 + """ + rxn = ct.Reaction.from_yaml(rxn, kinetics=self.gas) + N = self.gas.n_reactions + self.gas.add_reaction(rxn) + + self.gas.write_yaml(self.test_work_path / 'user-rate-units.yaml', + units={'length': 'mm', 'activation-energy': 'eV'}) + + yml = utilities.load_yaml(self.test_work_path / 'user-rate-units.yaml') + rxn = yml['reactions'][-1] + assert rxn['type'] == 'user-rate-2' + assert rxn['A'] == pytest.approx(1000 * 1000**3) + assert rxn['L'] == pytest.approx(200 * 1000) + assert rxn['Ea'] == pytest.approx(50 / ct.faraday) + + def test_validate_error(self): + rxn = """ + equation: H2 + OH = H2O + H + type: user-rate-2 + A: 1000 + L: -200 + Ea: 50 + """ + rxn = ct.Reaction.from_yaml(rxn, kinetics=self.gas) + N = self.gas.n_reactions + with pytest.raises(ct.CanteraError, match="Negative"): + self.gas.add_reaction(rxn) + + class InterfaceReactionTests(ReactionTests): # test suite for surface reaction expressions @@ -1714,6 +1854,7 @@ class TestArrheniusInterfaceReaction(InterfaceReactionTests, utilities.CanteraTe rate-constant: {A: 3.7e+20, b: 0, Ea: 11500 J/mol} type: interface-Arrhenius """ + _rc_units = ct.Units("m^2 / kmol / s") _value = 7.9574172975288e+19 @@ -1734,6 +1875,7 @@ class TestArrheniusCoverageReaction(InterfaceReactionTests, utilities.CanteraTes units: {length: cm, quantity: mol, activation-energy: J/mol} type: interface-Arrhenius """ + _rc_units = ct.Units("m^2 / kmol / s") _value = 349029090.19755 @@ -1751,6 +1893,7 @@ class TestBMInterfaceReaction(InterfaceReactionTests, utilities.CanteraTest): units: {length: cm, quantity: mol, activation-energy: J/mol} type: interface-Blowers-Masel """ + _rc_units = ct.Units("m^2 / kmol / s") _value = 1.2891970390741e+14 @@ -1771,6 +1914,7 @@ class TestBMCoverageReaction(InterfaceReactionTests, utilities.CanteraTest): units: {length: cm, quantity: mol, activation-energy: J/mol} type: interface-Blowers-Masel """ + _rc_units = ct.Units("m^2 / kmol / s") _value = 1.7068593925679e+14 @@ -1813,6 +1957,10 @@ def test_site_density(self): self.assertEqual(self.soln.site_density, self.soln.reaction(self._index).rate.site_density) + def test_rate_conversion_units(self): + rxn = self.from_yaml() + assert str(rxn.rate.conversion_units) == str(ct.Units('1')) + class TestArrheniusStickReaction(StickReactionTests, utilities.CanteraTest): # test interface reaction without coverages @@ -1830,6 +1978,7 @@ class TestArrheniusStickReaction(StickReactionTests, utilities.CanteraTest): units: {length: cm, quantity: mol, activation-energy: J/mol} type: sticking-Arrhenius """ + _rc_units = ct.Units("m^3 / kmol / s") _value = 401644856274.97 @@ -1852,6 +2001,7 @@ class TestArrheniusCovStickReaction(StickReactionTests, utilities.CanteraTest): units: {length: cm, quantity: mol, activation-energy: J/mol} type: sticking-Arrhenius """ + _rc_units = ct.Units("m^5 / kmol^2 / s") _value = 1.3792438668539e+19 @@ -1872,6 +2022,7 @@ class TestArrheniusMotzStickReaction(StickReactionTests, utilities.CanteraTest): units: {length: cm, quantity: mol, activation-energy: J/mol} type: sticking-Arrhenius """ + _rc_units = ct.Units("m^3 / kmol / s") _value = 195563866595.97 @@ -1892,4 +2043,5 @@ class TestBlowersMaselStickReaction(StickReactionTests, utilities.CanteraTest): units: {length: cm, quantity: mol, activation-energy: J/mol} type: sticking-Blowers-Masel """ + _rc_units = ct.Units("m^3 / kmol / s") _value = 195563866595.97 diff --git a/test/python/test_utils.py b/test/python/test_utils.py index 62f3354d54..8b9dffd784 100644 --- a/test/python/test_utils.py +++ b/test/python/test_utils.py @@ -1,9 +1,11 @@ import numpy as np +import pytest +from pytest import approx import cantera as ct from . import utilities -from cantera._utils import _py_to_any_to_py +from cantera._utils import _py_to_any_to_py, _py_to_anymap_to_py class TestUnitSystem(utilities.CanteraTest): @@ -61,6 +63,98 @@ def test_raises(self): with self.assertRaisesRegex(ct.CanteraError, "non-unity conversion factor"): ct.UnitSystem({"current": "2 A"}) + def test_convert_to_default(self): + system = ct.UnitSystem() + assert system.convert_to("3 cm", "m") == 0.03 + assert system.convert_to(4, "mm") == 4000.0 + assert system.convert_to("3 cm", ct.Units("m")) == 0.03 + assert system.convert_to(4, ct.Units("m")) == 4 + + def test_convert_activation_energy(self): + system = ct.UnitSystem() + assert system.convert_activation_energy_to("3 J/mol", "J/kmol") == 3000 + assert system.convert_activation_energy_to(4, "J/mol") == 0.004 + + def test_convert_rate_coeff(self): + system = ct.UnitSystem({"length": "cm"}) + assert system.convert_rate_coeff_to(11, ct.Units("m^3/kmol/s")) == approx(11e-6) + assert system.convert_rate_coeff_to("22 m^3/mol/s", "m^3/kmol/s") == approx(22e3) + + def test_convert_to_custom(self): + system = ct.UnitSystem({"length": "cm", "mass": "g"}) + assert system.convert_to(10000, "m^2") == 1.0 + assert system.convert_to(500, "kg") == 0.5 + + def test_convert_to_array(self): + system = ct.UnitSystem({"length": "km"}) + x = np.array(((3, 4), (0.5, 2.0), (1.0, 0.0))) + self.assertArrayNear(system.convert_to(x, "m"), 1000 * x) + + def test_convert_activation_energy_to_array(self): + system = ct.UnitSystem({"activation-energy": "J/mol"}) + x = np.array(((3, 4), (0.5, 2.0), (1.0, 0.0))) + self.assertArrayNear(system.convert_activation_energy_to(x, "J/kmol"), 1000 * x) + + def test_convert_rate_coeff_to_array(self): + system = ct.UnitSystem({"length": "cm"}) + x = np.array(((3, 4), (0.5, 2.0), (1.0, 0.0))) + self.assertArrayNear(system.convert_rate_coeff_to(x, "m^2/kmol/s"), 0.0001 * x) + + def test_convert_to_sequence(self): + system = ct.UnitSystem({"length": "km"}) + x = [("3000 mm", 4), (0.5, 2.0), 1.0] + x_m = system.convert_to(x, "m") + assert x_m[0][0] == 3.0 + assert x_m[1][1] == 2000.0 + assert x_m[2] == 1000.0 + + def test_convert_activation_energy_to_sequence(self): + system = ct.UnitSystem({"activation-energy": "J/mol"}) + x = [("3000 K", 4), (0.5, 2.0), 1.0] + x_m = system.convert_activation_energy_to(x, "J/kmol") + assert x_m[0][0] == approx(3000 * ct.gas_constant) + assert x_m[1][1] == 2000.0 + assert x_m[2] == 1000.0 + + def test_convert_rate_coeff_to_sequence(self): + system = ct.UnitSystem({"length": "cm"}) + x = [("3000 mm^3/kmol/s", 4), (0.5, 2.0), 1.0] + x_m = system.convert_rate_coeff_to(x, ct.Units("m^3/kmol/s")) + assert x_m[0][0] == approx(3000 / 1e9) + assert x_m[1][1] == approx(2 / 1e6) + assert x_m[2] == approx(1e-6) + + def test_convert_errors(self): + system = ct.UnitSystem() + with pytest.raises(ct.CanteraError): + system.convert_to("eggs", "J/kmol") + + with pytest.raises(TypeError): + system.convert_to("5 cm", True) + + with pytest.raises(TypeError): + system.convert_to(None, "J/kmol") + + with pytest.raises(TypeError): + system.convert_to(5, 6) + + with pytest.raises(ct.CanteraError): + system.convert_activation_energy_to(4, "m^3/s") + + with pytest.raises(TypeError): + system.convert_activation_energy_to(5, True) + + with pytest.raises(ct.CanteraError): + system.convert_activation_energy_to("spam spam eggs", "K") + + with pytest.raises(TypeError): + system.convert_activation_energy_to({"spam": 5}, "K") + + with pytest.raises(TypeError): + system.convert_rate_coeff_to(11, ["m^3"]) + + with pytest.raises(TypeError): + system.convert_rate_coeff_to({"spam": 13}, "m^6/kmol^2/s") class TestPyToAnyValue(utilities.CanteraTest): @@ -168,3 +262,63 @@ def test_unconvertible(self): def test_unconvertible2(self): self.check_raises([3+4j, 1-2j], ct.CanteraError, "Unable to convert") + + +class TestAnyMap(utilities.CanteraTest): + @classmethod + def setup_class(cls): + data = { + "units": {"length": "mm", "energy": "kJ"}, + "group1": { + "a": 5000, + "b": "12 MJ", + "c": "8000 K", + "d": [16, "10 cm^2"] + }, + "group2": { + "units": {"mass": "g"}, + "x": 1300 + } + } + cls.data = _py_to_anymap_to_py(data) + + def test_units_simple(self): + assert self.data['group1'].convert('a', 'm') == 5.0 + assert self.data['group1'].convert('b', 'J') == 12e6 + assert self.data['group1'].convert('d', 'm^2') == [16e-6, 10e-4] + + def test_units_activation_energy(self): + assert self.data['group1'].convert_activation_energy('a', 'J/kmol') == 5e6 + assert (self.data['group1'].convert_activation_energy('c', 'J/kmol') + == pytest.approx(8000 * ct.gas_constant)) + + def test_units_nested(self): + assert self.data['group2'].convert('x', 'J/kg') == 1300 * 1e6 + + def test_set_quantity(self): + params = ct.AnyMap() + params.set_quantity('spam', [2, 3, 4], 'Gg') + params.set_quantity('eggs', 10, ct.Units('kg/m^3')) + params.set_activation_energy('beans', 5, 'K') + + converted = _py_to_anymap_to_py(params) + assert converted['spam'] == [2e6, 3e6, 4e6] + assert converted['eggs'] == pytest.approx(10) + assert converted['beans'] == pytest.approx(5 * ct.gas_constant) + + # Unit conversions are deferred + outer = ct.AnyMap() + outer['units'] = {'mass': 'g', 'activation-energy': 'K'} + outer['inner'] = params + converted = _py_to_anymap_to_py(outer) + assert converted['inner']['spam'] == pytest.approx([2e9, 3e9, 4e9]) + assert converted['inner']['eggs'] == pytest.approx(10e3) + assert converted['inner']['beans'] == pytest.approx(5) + + outer.set_quantity('cheese', {'gouda': 5.5}, 'J/kg') + with pytest.raises(ct.CanteraError): + _py_to_anymap_to_py(outer) + + outer.set_activation_energy('cheese', 12, 'V/m') + with pytest.raises(ct.CanteraError): + _py_to_anymap_to_py(outer) diff --git a/test/python/user_ext.py b/test/python/user_ext.py index 4e7228460d..7630b68947 100644 --- a/test/python/user_ext.py +++ b/test/python/user_ext.py @@ -26,6 +26,9 @@ def __init__(self, *args, **kwargs): def set_parameters(self, node, units): self.A = node["A"] + def get_parameters(self, node): + node["A"] = self.A + def eval(self, data): return self.A * data.Tsquared diff --git a/test_problems/.gitignore b/test_problems/.gitignore index 65498da70a..be779ae6b0 100644 --- a/test_problems/.gitignore +++ b/test_problems/.gitignore @@ -26,6 +26,7 @@ cathermo/testWaterPDSS/WaterPDSS cathermo/testWaterTP/WaterSSTP cathermo/VPissp/ISSPTester2 clib_test/clib_test +cxx_samples/flamespeed.h5 diamondSurf/diamondSurf diamondSurf/runDiamond_output.out dustyGasTransport/dustyGasTransport