From 7dc74a9909da961bf9befd4863e51c11d5e62cae Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 19 Mar 2023 20:40:48 -0400 Subject: [PATCH 01/26] [Cython] Use built-in declarations for shared_ptr --- interfaces/cython/cantera/ctcxx.pxd | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/interfaces/cython/cantera/ctcxx.pxd b/interfaces/cython/cantera/ctcxx.pxd index 28b77d7ff8..ac772bf658 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 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() From 2ba8ee40b90220aa323d1b77fe4bcee9dc7ba026 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 19 Mar 2023 21:09:51 -0400 Subject: [PATCH 02/26] [Cython] Retain units info when converting AnyMap to Python --- include/cantera/base/AnyMap.h | 3 +++ interfaces/cython/cantera/_utils.pxd | 7 ++++++- interfaces/cython/cantera/_utils.pyx | 24 +++++++++++++++++++----- interfaces/cython/cantera/reaction.pyx | 24 ++++++++++++------------ interfaces/cython/cantera/units.pxd | 4 +++- interfaces/cython/cantera/units.pyx | 10 +++++++++- interfaces/cython/cantera/yamlwriter.pxd | 2 +- interfaces/cython/cantera/yamlwriter.pyx | 8 ++++---- 8 files changed, 57 insertions(+), 25 deletions(-) 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/interfaces/cython/cantera/_utils.pxd b/interfaces/cython/cantera/_utils.pxd index 4918ad98ad..b2b6f31236 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 cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": cdef cppclass CxxAnyValue "Cantera::AnyValue" @@ -37,6 +38,7 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": void update(CxxAnyMap& other, cbool) string keys_str() void applyUnits() + shared_ptr[CxxUnitSystem] unitsShared() cdef cppclass CxxAnyValue "Cantera::AnyValue": CxxAnyValue() @@ -86,6 +88,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,7 +98,7 @@ 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 CxxAnyMap dict_to_anymap(data, cbool hyphenize=*) except * cdef anymap_to_dict(CxxAnyMap& m) cdef CxxAnyValue python_to_anyvalue(item, name=*) except * diff --git a/interfaces/cython/cantera/_utils.pyx b/interfaces/cython/cantera/_utils.pyx index 8c4e3746b2..8c893839fc 100644 --- a/interfaces/cython/cantera/_utils.pyx +++ b/interfaces/cython/cantera/_utils.pyx @@ -153,6 +153,18 @@ class CanteraError(RuntimeError): cdef public PyObject* pyCanteraError = CanteraError + +cdef class AnyMap(dict): + 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() + + cdef anyvalue_to_python(string name, CxxAnyValue& v): cdef CxxAnyMap a cdef CxxAnyValue b @@ -207,12 +219,14 @@ cdef anyvalue_to_python(string name, CxxAnyValue& v): cdef anymap_to_dict(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 CxxAnyMap dict_to_anymap(dict data, cbool hyphenize=False) except *: +cdef CxxAnyMap dict_to_anymap(data, cbool hyphenize=False) except *: cdef CxxAnyMap m if hyphenize: # replace "_" by "-": while Python dictionaries typically use "_" in key names, diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index fb3c638b54..f5cdac2907 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -221,7 +221,7 @@ cdef class ArrheniusRate(ArrheniusRateBase): if init: self._cinit(input_data, A=A, b=b, Ea=Ea) - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset(new CxxArrheniusRate(dict_to_anymap(input_data))) def _from_parameters(self, A, b, Ea): @@ -248,7 +248,7 @@ 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): + def _from_dict(self, input_data): self._rate.reset(new CxxBlowersMaselRate(dict_to_anymap(input_data))) def _from_parameters(self, A, b, Ea0, w): @@ -321,7 +321,7 @@ 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)) ) @@ -448,7 +448,7 @@ 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)) ) @@ -468,7 +468,7 @@ 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)) ) @@ -488,7 +488,7 @@ 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)) ) @@ -504,7 +504,7 @@ 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)) ) @@ -834,7 +834,7 @@ cdef class InterfaceRateBase(ArrheniusRateBase): cdef CxxAnyMap cxx_deps self.interface.getCoverageDependencies(cxx_deps) return anymap_to_dict(cxx_deps) - def __set__(self, dict deps): + def __set__(self, deps): cdef CxxAnyMap cxx_deps = dict_to_anymap(deps) self.interface.setCoverageDependencies(cxx_deps) @@ -892,7 +892,7 @@ cdef class InterfaceArrheniusRate(InterfaceRateBase): if init: self._cinit(input_data, A=A, b=b, Ea=Ea) - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset(new CxxInterfaceArrheniusRate(dict_to_anymap(input_data))) def _from_parameters(self, A, b, Ea): @@ -920,7 +920,7 @@ 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): + def _from_dict(self, input_data): self._rate.reset(new CxxInterfaceBlowersMaselRate(dict_to_anymap(input_data))) def _from_parameters(self, A, b, Ea0, w): @@ -1030,7 +1030,7 @@ cdef class StickingArrheniusRate(StickRateBase): if init: self._cinit(input_data, A=A, b=b, Ea=Ea) - def _from_dict(self, dict input_data): + def _from_dict(self, input_data): self._rate.reset(new CxxStickingArrheniusRate(dict_to_anymap(input_data))) def _from_parameters(self, A, b, Ea): @@ -1057,7 +1057,7 @@ 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): + def _from_dict(self, input_data): self._rate.reset(new CxxStickingBlowersMaselRate(dict_to_anymap(input_data))) def _from_parameters(self, A, b, Ea0, w): diff --git a/interfaces/cython/cantera/units.pxd b/interfaces/cython/cantera/units.pxd index 04e364fcaa..85e096f409 100644 --- a/interfaces/cython/cantera/units.pxd +++ b/interfaces/cython/cantera/units.pxd @@ -34,4 +34,6 @@ cdef class Units: cdef copy(CxxUnits) 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..29fae9add6 100644 --- a/interfaces/cython/cantera/units.pyx +++ b/interfaces/cython/cantera/units.pyx @@ -94,13 +94,21 @@ cdef class UnitSystem: 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 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') From a8c1c5059666df3100d075cfbb9642399393af4e Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 19 Mar 2023 21:18:37 -0400 Subject: [PATCH 03/26] [Cython] Update function names for AnyMap conversions --- interfaces/cython/cantera/_onedim.pyx | 6 +-- interfaces/cython/cantera/_utils.pxd | 4 +- interfaces/cython/cantera/_utils.pyx | 12 ++--- interfaces/cython/cantera/delegator.pyx | 4 +- interfaces/cython/cantera/kinetics.pyx | 4 +- interfaces/cython/cantera/preconditioners.pyx | 2 +- interfaces/cython/cantera/reaction.pyx | 44 +++++++++---------- interfaces/cython/cantera/reactor.pyx | 6 +-- interfaces/cython/cantera/solutionbase.pyx | 22 +++++----- interfaces/cython/cantera/speciesthermo.pyx | 4 +- interfaces/cython/cantera/thermo.pyx | 6 +-- interfaces/cython/cantera/transport.pyx | 4 +- 12 files changed, 59 insertions(+), 59 deletions(-) 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 b2b6f31236..b616c4cc87 100644 --- a/interfaces/cython/cantera/_utils.pxd +++ b/interfaces/cython/cantera/_utils.pxd @@ -98,8 +98,8 @@ cdef pystr(string x) cdef comp_map_to_dict(Composition m) cdef Composition comp_map(X) except * -cdef CxxAnyMap dict_to_anymap(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 8c893839fc..165e30fa56 100644 --- a/interfaces/cython/cantera/_utils.pyx +++ b/interfaces/cython/cantera/_utils.pyx @@ -187,9 +187,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]](): @@ -216,7 +216,7 @@ 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() cdef AnyMap out = AnyMap() @@ -226,7 +226,7 @@ cdef anymap_to_dict(CxxAnyMap& m): return out -cdef CxxAnyMap dict_to_anymap(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, @@ -304,7 +304,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: @@ -398,7 +398,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): diff --git a/interfaces/cython/cantera/delegator.pyx b/interfaces/cython/cantera/delegator.pyx index d7e4b8a99b..96f0731484 100644 --- a/interfaces/cython/cantera/delegator.pyx +++ b/interfaces/cython/cantera/delegator.pyx @@ -9,7 +9,7 @@ from libc.stdlib cimport malloc from libc.string cimport strcpy from ._utils import CanteraError -from ._utils cimport stringify, pystr, anymap_to_dict +from ._utils cimport stringify, pystr, anymap_to_py from .units cimport Units # from .reaction import ExtensibleRate, ExtensibleRateData from .reaction cimport (ExtensibleRate, ExtensibleRateData, CxxReaction, @@ -115,7 +115,7 @@ cdef void callback_v_b(PyFuncInfo& funcInfo, cbool arg) noexcept: cdef void callback_v_cAMr_cUSr(PyFuncInfo& funcInfo, const CxxAnyMap& arg1, const CxxUnitStack& arg2) noexcept: - pyArg1 = anymap_to_dict(arg1) # cast away constness + pyArg1 = anymap_to_py(arg1) # cast away constness pyArg2 = Units.copy(arg2.product()) try: (funcInfo.func())(pyArg1, pyArg2) 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.pyx b/interfaces/cython/cantera/reaction.pyx index f5cdac2907..d68436a3bb 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -94,7 +94,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 +129,7 @@ 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()) cdef class ArrheniusRateBase(ReactionRate): @@ -222,7 +222,7 @@ cdef class ArrheniusRate(ArrheniusRateBase): self._cinit(input_data, A=A, b=b, Ea=Ea) def _from_dict(self, input_data): - self._rate.reset(new CxxArrheniusRate(dict_to_anymap(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)) @@ -249,7 +249,7 @@ cdef class BlowersMaselRate(ArrheniusRateBase): self._cinit(input_data, A=A, b=b, Ea0=Ea0, w=w) def _from_dict(self, input_data): - self._rate.reset(new CxxBlowersMaselRate(dict_to_anymap(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)) @@ -323,7 +323,7 @@ cdef class TwoTempPlasmaRate(ArrheniusRateBase): 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): @@ -450,7 +450,7 @@ cdef class LindemannRate(FalloffRate): 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): @@ -470,7 +470,7 @@ cdef class TroeRate(FalloffRate): 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): @@ -490,7 +490,7 @@ cdef class SriRate(FalloffRate): 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): @@ -506,7 +506,7 @@ cdef class TsangRate(FalloffRate): 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 +528,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 +585,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 +596,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: @@ -833,9 +833,9 @@ cdef class InterfaceRateBase(ArrheniusRateBase): def __get__(self): cdef CxxAnyMap cxx_deps self.interface.getCoverageDependencies(cxx_deps) - return anymap_to_dict(cxx_deps) + return anymap_to_py(cxx_deps) def __set__(self, deps): - cdef CxxAnyMap cxx_deps = dict_to_anymap(deps) + cdef CxxAnyMap cxx_deps = py_to_anymap(deps) self.interface.setCoverageDependencies(cxx_deps) @@ -893,7 +893,7 @@ cdef class InterfaceArrheniusRate(InterfaceRateBase): self._cinit(input_data, A=A, b=b, Ea=Ea) def _from_dict(self, input_data): - self._rate.reset(new CxxInterfaceArrheniusRate(dict_to_anymap(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)) @@ -921,7 +921,7 @@ cdef class InterfaceBlowersMaselRate(InterfaceRateBase): self._cinit(input_data, A=A, b=b, Ea0=Ea0, w=w) def _from_dict(self, input_data): - self._rate.reset(new CxxInterfaceBlowersMaselRate(dict_to_anymap(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)) @@ -1031,7 +1031,7 @@ cdef class StickingArrheniusRate(StickRateBase): self._cinit(input_data, A=A, b=b, Ea=Ea) def _from_dict(self, input_data): - self._rate.reset(new CxxStickingArrheniusRate(dict_to_anymap(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)) @@ -1058,7 +1058,7 @@ cdef class StickingBlowersMaselRate(StickRateBase): self._cinit(input_data, A=A, b=b, Ea0=Ea0, w=w) def _from_dict(self, input_data): - self._rate.reset(new CxxStickingBlowersMaselRate(dict_to_anymap(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 +1354,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 +1549,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 +1557,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.pyx b/interfaces/cython/cantera/solutionbase.pyx index 0dfb9f019a..1eb140f1df 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -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,7 +272,7 @@ 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): """ @@ -280,7 +280,7 @@ cdef class _SolutionBase: 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): """ From 038a5296ddc1a73a6c58616a27cd62be57e79470 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 25 Mar 2023 20:01:26 -0400 Subject: [PATCH 04/26] [Python] Add UnitSystem.convert_to / convert_activation_energy_to --- interfaces/cython/cantera/units.pxd | 7 +++ interfaces/cython/cantera/units.pyx | 76 +++++++++++++++++++++++++++++ test/python/test_utils.py | 71 +++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) diff --git a/interfaces/cython/cantera/units.pxd b/interfaces/cython/cantera/units.pxd index 85e096f409..96fc86f0db 100644 --- a/interfaces/cython/cantera/units.pxd +++ b/interfaces/cython/cantera/units.pxd @@ -22,6 +22,13 @@ 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 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() diff --git a/interfaces/cython/cantera/units.pyx b/interfaces/cython/cantera/units.pyx index 29fae9add6..88c7123d7a 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 * @@ -92,6 +95,7 @@ cdef class UnitSystem: the default unit system is retrieved as:: ct.UnitSystem() + """ def __cinit__(self, units=None): self._unitsystem.reset(new CxxUnitSystem()) @@ -122,3 +126,75 @@ 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") diff --git a/test/python/test_utils.py b/test/python/test_utils.py index 62f3354d54..6b8b9ba942 100644 --- a/test/python/test_utils.py +++ b/test/python/test_utils.py @@ -1,4 +1,6 @@ import numpy as np +import pytest +from pytest import approx import cantera as ct from . import utilities @@ -61,6 +63,75 @@ 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_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_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_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") + class TestPyToAnyValue(utilities.CanteraTest): From a54b1d10c891d3a2d5647674aa8dd8ebb3fce89a Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 29 Mar 2023 21:01:05 -0400 Subject: [PATCH 05/26] [Python] Add unit handling to AnyMap --- interfaces/cython/cantera/_utils.pyx | 26 +++++++++++++++++++++ test/python/test_utils.py | 34 +++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/_utils.pyx b/interfaces/cython/cantera/_utils.pyx index 165e30fa56..241b7fcb76 100644 --- a/interfaces/cython/cantera/_utils.pyx +++ b/interfaces/cython/cantera/_utils.pyx @@ -164,6 +164,26 @@ cdef class AnyMap(dict): 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) + cdef anyvalue_to_python(string name, CxxAnyValue& v): cdef CxxAnyMap a @@ -240,6 +260,7 @@ cdef CxxAnyMap py_to_anymap(data, cbool hyphenize=False) except *: for k, v in data.items(): m[stringify(k)] = python_to_anyvalue(v, k) + m.applyUnits() return m cdef get_types(item): @@ -446,3 +467,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/test/python/test_utils.py b/test/python/test_utils.py index 6b8b9ba942..0683c83bae 100644 --- a/test/python/test_utils.py +++ b/test/python/test_utils.py @@ -5,7 +5,7 @@ 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): @@ -239,3 +239,35 @@ 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 From 380a510f2aed1fd7896323a439224ab439908ae8 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Thu, 30 Mar 2023 21:48:07 -0400 Subject: [PATCH 06/26] Add holdExternalHandle to Delegator --- include/cantera/base/Delegator.h | 24 ++++++++++++++++++---- include/cantera/base/Solution.h | 2 +- interfaces/cython/cantera/delegator.pxd | 11 ++++++++++ interfaces/cython/cantera/solutionbase.pxd | 10 ++------- interfaces/cython/cantera/solutionbase.pyx | 2 +- src/extensions/PythonExtensionManager.cpp | 3 ++- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/include/cantera/base/Delegator.h b/include/cantera/base/Delegator.h index 9aee98729e..2d1ac9e997 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 @@ -254,8 +255,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: @@ -496,8 +510,10 @@ class Delegator std::function*> 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/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/interfaces/cython/cantera/delegator.pxd b/interfaces/cython/cantera/delegator.pxd index 39011a392d..9a1034999f 100644 --- a/interfaces/cython/cantera/delegator.pxd +++ b/interfaces/cython/cantera/delegator.pxd @@ -22,11 +22,22 @@ 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 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 1eb140f1df..65eb3fca86 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 diff --git a/src/extensions/PythonExtensionManager.cpp b/src/extensions/PythonExtensionManager.cpp index b6b1a52446..6dc9c36cba 100644 --- a/src/extensions/PythonExtensionManager.cpp +++ b/src/extensions/PythonExtensionManager.cpp @@ -99,7 +99,8 @@ void PythonExtensionManager::registerRateBuilder( delegator->setParameters(params, units); // The delegator is responsible for eventually deleting the Python object - delegator->holdExternalHandle(make_shared(extRate, false)); + delegator->holdExternalHandle("python", + make_shared(extRate, false)); return delegator.release(); }; ReactionRateFactory::factory()->reg(rateName, builder); From dc684e17c4c34e28da1643e1acb0b821618f7a88 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Thu, 30 Mar 2023 23:31:26 -0400 Subject: [PATCH 07/26] [Python] Improve access to ExtensibleRate objects Previously, accessing an ExtensibleRate object from a Reaction object would create a new wrapper object rather than providing access to the "original" object that is used by the ReactionRateDelegator, and this wrapper object would not behave the same as the original object except for methods that passed through via the Delegator. Now, we return the underlying object, ensuring consistency. This requires more complex memory management because the ExtensibleRate/Delegator pair form a reference cycle that can be attached to other objects either from the C++ or the Python side. --- interfaces/cython/cantera/ctcxx.pxd | 2 +- interfaces/cython/cantera/delegator.pyx | 44 +++++++++++++++++++++++ interfaces/cython/cantera/reaction.pyx | 16 +++++++++ src/extensions/PythonExtensionManager.cpp | 4 ++- test/python/test_reaction.py | 22 ++++++++++-- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/interfaces/cython/cantera/ctcxx.pxd b/interfaces/cython/cantera/ctcxx.pxd index ac772bf658..e79645d7ca 100644 --- a/interfaces/cython/cantera/ctcxx.pxd +++ b/interfaces/cython/cantera/ctcxx.pxd @@ -11,7 +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 +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 diff --git a/interfaces/cython/cantera/delegator.pyx b/interfaces/cython/cantera/delegator.pyx index 96f0731484..73597488dc 100644 --- a/interfaces/cython/cantera/delegator.pyx +++ b/interfaces/cython/cantera/delegator.pyx @@ -17,6 +17,8 @@ from .reaction cimport (ExtensibleRate, ExtensibleRateData, CxxReaction, 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 @@ -405,6 +407,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/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index d68436a3bb..7288e32a8c 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 == "": diff --git a/src/extensions/PythonExtensionManager.cpp b/src/extensions/PythonExtensionManager.cpp index 6dc9c36cba..4ef0e1a05d 100644 --- a/src/extensions/PythonExtensionManager.cpp +++ b/src/extensions/PythonExtensionManager.cpp @@ -98,7 +98,9 @@ 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 + // 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(); diff --git a/test/python/test_reaction.py b/test/python/test_reaction.py index 39661a0e6b..d390922484 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -1572,11 +1572,20 @@ def test_from_dict(self): 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"}) @@ -1625,7 +1634,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,12 +1646,17 @@ 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 From 63c58999d38bcea9c5f49d1a42d351b695e1f020 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 2 Apr 2023 17:58:53 -0400 Subject: [PATCH 08/26] Move undefined units handling into UnitSystem Handling this centrally simplifies the implementation of user-defined rate functions --- include/cantera/base/Units.h | 7 +++++++ src/base/Units.cpp | 26 ++++++++++++++++++++++++++ src/kinetics/Arrhenius.cpp | 15 ++------------- src/kinetics/ChebyshevRate.cpp | 5 ++--- src/kinetics/Reaction.cpp | 9 ++++++++- src/kinetics/ReactionRateFactory.cpp | 10 +--------- test/kinetics/kineticsFromYaml.cpp | 18 ++++++++++++++++++ test/python/test_reaction.py | 12 +++++++----- 8 files changed, 71 insertions(+), 31 deletions(-) diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h index 22485cf0f7..86c6c360e4 100644 --- a/include/cantera/base/Units.h +++ b/include/cantera/base/Units.h @@ -220,6 +220,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/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/kinetics/Arrhenius.cpp b/src/kinetics/Arrhenius.cpp index 1b2df13585..fd7fdc05ad 100644 --- a/src/kinetics/Arrhenius.cpp +++ b/src/kinetics/Arrhenius.cpp @@ -49,18 +49,7 @@ void ArrheniusBase::setRateParameters( 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], m_rate_units); 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 +59,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], m_rate_units); m_b = rate_vec[1].asDouble(); if (rate_vec.size() > 2) { m_Ea_R = units.convertActivationEnergy(rate_vec[2], "K"); diff --git a/src/kinetics/ChebyshevRate.cpp b/src/kinetics/ChebyshevRate.cpp index 71f173b6ea..088488a1f4 100644 --- a/src/kinetics/ChebyshevRate.cpp +++ b/src/kinetics/ChebyshevRate.cpp @@ -80,9 +80,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), m_rate_units); + coeffs(0, 0) += std::log10(offset); setLimits( unit_system.convert(T_range[0], "K"), unit_system.convert(T_range[1], "K"), diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 8db9f4fd95..e12ddff7c8 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 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_reaction.py b/test/python/test_reaction.py index d390922484..4b8c2fa1d0 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) From 7c908d5f4b03ece89e34b1e8e71caabd3b3e6c80 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 2 Apr 2023 19:58:09 -0400 Subject: [PATCH 09/26] [Python] Add more complete handling of UnitStack in ExtensibleRate --- include/cantera/base/Units.h | 2 + interfaces/cython/cantera/_utils.pyx | 8 ++++ interfaces/cython/cantera/delegator.pyx | 4 +- interfaces/cython/cantera/reaction.pyx | 5 ++- interfaces/cython/cantera/units.pxd | 14 ++++++- interfaces/cython/cantera/units.pyx | 51 ++++++++++++++++++++++++- test/python/test_reaction.py | 40 ++++++++++++++++++- test/python/test_utils.py | 23 +++++++++++ 8 files changed, 139 insertions(+), 8 deletions(-) diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h index 86c6c360e4..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(); } diff --git a/interfaces/cython/cantera/_utils.pyx b/interfaces/cython/cantera/_utils.pyx index 241b7fcb76..b386159904 100644 --- a/interfaces/cython/cantera/_utils.pyx +++ b/interfaces/cython/cantera/_utils.pyx @@ -184,6 +184,14 @@ cdef class AnyMap(dict): """ 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) + cdef anyvalue_to_python(string name, CxxAnyValue& v): cdef CxxAnyMap a diff --git a/interfaces/cython/cantera/delegator.pyx b/interfaces/cython/cantera/delegator.pyx index 73597488dc..8ca26b4389 100644 --- a/interfaces/cython/cantera/delegator.pyx +++ b/interfaces/cython/cantera/delegator.pyx @@ -10,7 +10,7 @@ from libc.string cimport strcpy from ._utils import CanteraError from ._utils cimport stringify, pystr, anymap_to_py -from .units cimport Units +from .units cimport Units, UnitStack # from .reaction import ExtensibleRate, ExtensibleRateData from .reaction cimport (ExtensibleRate, ExtensibleRateData, CxxReaction, CxxReactionRateDelegator, CxxReactionDataDelegator) @@ -118,7 +118,7 @@ cdef void callback_v_cAMr_cUSr(PyFuncInfo& funcInfo, const CxxAnyMap& arg1, const CxxUnitStack& arg2) noexcept: pyArg1 = anymap_to_py(arg1) # cast away constness - pyArg2 = Units.copy(arg2.product()) + pyArg2 = UnitStack.copy(arg2) try: (funcInfo.func())(pyArg1, pyArg2) except BaseException as e: diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index 7288e32a8c..3fec4a0f61 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -760,11 +760,12 @@ cdef class ExtensibleRate(ReactionRate): assign_delegates(self, dynamic_cast[CxxDelegatorPtr](self.rate)) # ReactionRate does not define __init__, so it does not need to be called - 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. """ raise NotImplementedError(f"{self.__class__.__name__}.set_parameters") diff --git a/interfaces/cython/cantera/units.pxd b/interfaces/cython/cantera/units.pxd index 96fc86f0db..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() @@ -26,19 +26,29 @@ cdef extern from "cantera/base/Units.h" namespace "Cantera": 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 _set_unitSystem(self, shared_ptr[CxxUnitSystem] units) diff --git a/interfaces/cython/cantera/units.pyx b/interfaces/cython/cantera/units.pyx index 88c7123d7a..7a58664422 100644 --- a/interfaces/cython/cantera/units.pyx +++ b/interfaces/cython/cantera/units.pyx @@ -61,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. @@ -198,3 +217,33 @@ cdef class UnitSystem: 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/test/python/test_reaction.py b/test/python/test_reaction.py index 4b8c2fa1d0..06df398f0e 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -1529,7 +1529,7 @@ def eval(self, data): class TestExtensible(ReactionTests, utilities.CanteraTest): - # test Extensible reaction rate + # test general functionality of ExtensibleRate _phase_def = """ phases: - name: gas @@ -1596,6 +1596,8 @@ def test_eval_error(self): class TestExtensible2(utilities.CanteraTest): + # Test handling of ExtensibleRate defined in a separate Python module + _input_template = """ extensions: - type: python @@ -1662,6 +1664,42 @@ def run(): 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 __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.A = np.nan + self.Ta = np.nan + self.length = np.nan + + 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 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) + + class InterfaceReactionTests(ReactionTests): # test suite for surface reaction expressions diff --git a/test/python/test_utils.py b/test/python/test_utils.py index 0683c83bae..03e08a3642 100644 --- a/test/python/test_utils.py +++ b/test/python/test_utils.py @@ -75,6 +75,11 @@ def test_convert_activation_energy(self): 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 @@ -90,6 +95,11 @@ def test_convert_activation_energy_to_array(self): 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] @@ -106,6 +116,14 @@ def test_convert_activation_energy_to_sequence(self): 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): @@ -132,6 +150,11 @@ def test_convert_errors(self): 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): From 3e6db4b956665d08d37bae51b2f0794ccce3c753 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 2 Apr 2023 21:08:16 -0400 Subject: [PATCH 10/26] Centralize serialization of ReactionRate 'type' attribute --- include/cantera/kinetics/ReactionRate.h | 1 + src/kinetics/Arrhenius.cpp | 1 - src/kinetics/ChebyshevRate.cpp | 1 - src/kinetics/Falloff.cpp | 5 ----- src/kinetics/PlogRate.cpp | 1 - 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/include/cantera/kinetics/ReactionRate.h b/include/cantera/kinetics/ReactionRate.h index 1dd5610274..33e66208a8 100644 --- a/include/cantera/kinetics/ReactionRate.h +++ b/include/cantera/kinetics/ReactionRate.h @@ -102,6 +102,7 @@ class ReactionRate //! handled by the getParameters() method. AnyMap parameters() const { AnyMap out; + out["type"] = type(); getParameters(out); return out; } diff --git a/src/kinetics/Arrhenius.cpp b/src/kinetics/Arrhenius.cpp index fd7fdc05ad..4ded086ad5 100644 --- a/src/kinetics/Arrhenius.cpp +++ b/src/kinetics/Arrhenius.cpp @@ -119,7 +119,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 088488a1f4..4fdcdbe5e7 100644 --- a/src/kinetics/ChebyshevRate.cpp +++ b/src/kinetics/ChebyshevRate.cpp @@ -126,7 +126,6 @@ void ChebyshevRate::setData(const Array2D& coeffs) void ChebyshevRate::getParameters(AnyMap& rateNode) const { - rateNode["type"] = type(); if (!valid()) { // object not fully set up return; diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index e4bd34e81f..dadbea1f96 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -168,11 +168,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; } 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; From 22f12fff08410c60a6082496b5871607c10d95b8 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 2 Apr 2023 21:56:10 -0400 Subject: [PATCH 11/26] [Python] Add basic serialization support for ExtensibleRate --- include/cantera/base/Delegator.h | 22 +++++++++++++++-- .../cantera/kinetics/ReactionRateDelegator.h | 11 ++++++--- interfaces/cython/cantera/delegator.pxd | 2 ++ interfaces/cython/cantera/delegator.pyx | 18 +++++++++++++- interfaces/cython/cantera/reaction.pyx | 15 ++++++++++-- src/kinetics/ReactionRateDelegator.cpp | 2 ++ test/python/test_reaction.py | 24 +++++++------------ test/python/user_ext.py | 3 +++ 8 files changed, 73 insertions(+), 24 deletions(-) diff --git a/include/cantera/base/Delegator.h b/include/cantera/base/Delegator.h index 2d1ac9e997..7dad72eb8e 100644 --- a/include/cantera/base/Delegator.h +++ b/include/cantera/base/Delegator.h @@ -146,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, @@ -297,6 +308,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, @@ -307,8 +326,6 @@ class Delegator m_funcs_v_cAMr_cUSr[name] = ⌖ } - - //! Install a function with the signature `void(double*)` as being delegatable void install(const std::string& name, std::function, double*)>& target, @@ -484,6 +501,7 @@ 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; std::map m_evalFromStruct; + function m_evalFromStruct; - std::function m_setParameters; + function m_setParameters; + function m_getParameters; }; } diff --git a/interfaces/cython/cantera/delegator.pxd b/interfaces/cython/cantera/delegator.pxd index 9a1034999f..aed42e48d2 100644 --- a/interfaces/cython/cantera/delegator.pxd +++ b/interfaces/cython/cantera/delegator.pxd @@ -42,6 +42,7 @@ cdef extern from "cantera/base/Delegator.h" namespace "Cantera": 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(size_array1, double*)], string&) except +translate_exception void setDelegate(string&, function[void(size_array1, double, double*)], string&) except +translate_exception @@ -58,6 +59,7 @@ 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(size_array1, double*)] pyOverride( diff --git a/interfaces/cython/cantera/delegator.pyx b/interfaces/cython/cantera/delegator.pyx index 8ca26b4389..6ab8962882 100644 --- a/interfaces/cython/cantera/delegator.pyx +++ b/interfaces/cython/cantera/delegator.pyx @@ -9,7 +9,7 @@ from libc.stdlib cimport malloc from libc.string cimport strcpy from ._utils import CanteraError -from ._utils cimport stringify, pystr, anymap_to_py +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, @@ -113,6 +113,19 @@ 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: @@ -310,6 +323,9 @@ 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) diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index 3fec4a0f61..5807a95d94 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -748,17 +748,21 @@ 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"), } + 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: AnyMap, rate_coeff_units: UnitStack) -> None: """ @@ -769,6 +773,13 @@ cdef class ExtensibleRate(ReactionRate): """ 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`. + """ + 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 diff --git a/src/kinetics/ReactionRateDelegator.cpp b/src/kinetics/ReactionRateDelegator.cpp index e28823f3c2..bde44e8c67 100644 --- a/src/kinetics/ReactionRateDelegator.cpp +++ b/src/kinetics/ReactionRateDelegator.cpp @@ -51,6 +51,8 @@ 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); }); } unique_ptr ReactionRateDelegator::newMultiRate() const diff --git a/test/python/test_reaction.py b/test/python/test_reaction.py index 06df398f0e..f3ce9bbda1 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -1517,13 +1517,12 @@ 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) - self.A = np.nan - def set_parameters(self, params, units): self.A = params["A"] + def get_parameters(self, params): + params["A"] = self.A + def eval(self, data): return self.A * data.T**2.7 * exp(-3150.15428/data.T) @@ -1568,12 +1567,6 @@ def eval_rate(self, rate): 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") - - 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) @@ -1667,17 +1660,16 @@ def run(): @ct.extension(name="user-rate-2", data=UserRate1Data) class UserRate2(ct.ExtensibleRate): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.A = np.nan - self.Ta = np.nan - self.length = np.nan - 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["A"] = self.A + params["L"] = self.length + params["Ea"] = self.Ta + def eval(self, data): return self.A * (self.length / 2.0)**2 * exp(-self.Ta/data.T) 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 From 6ebe8dd72d6b81e2ca796997aa3789934d4f8191 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 9 Apr 2023 23:22:41 -0400 Subject: [PATCH 12/26] Include AnyMap in Python docs --- doc/sphinx/cython/utilities.rst | 9 +++++++-- doc/sphinx/yaml/general.rst | 2 ++ interfaces/cython/cantera/_utils.pyx | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) 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/interfaces/cython/cantera/_utils.pyx b/interfaces/cython/cantera/_utils.pyx index b386159904..4c61febe29 100644 --- a/interfaces/cython/cantera/_utils.pyx +++ b/interfaces/cython/cantera/_utils.pyx @@ -155,6 +155,13 @@ 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() From 2a31cd7084ce793c511f0aa33d8ecb628f97b99e Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 11 Apr 2023 11:21:23 -0400 Subject: [PATCH 13/26] Fix calculateRateCoeffUnits for falloff reactions The forward rate constants for falloff and chemically activated reactions already incorporate the third-body dependency, so the units should only reflect the explicit reactants. This was being compensated for when setting the units of the high/low pressure rate expressions, so it did not affect any results. --- src/kinetics/Falloff.cpp | 5 ++--- src/kinetics/Reaction.cpp | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index dadbea1f96..592842708a 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")) { diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index e12ddff7c8..476b190507 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -543,7 +543,7 @@ 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); } From e80e7c5de9b4024607758f34bd65aefa6e73db5f Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 11 Apr 2023 11:25:40 -0400 Subject: [PATCH 14/26] Fix setting of Reaction.rate_units --- src/kinetics/Reaction.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 476b190507..d61ed74c00 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -548,6 +548,7 @@ UnitStack Reaction::calculateRateCoeffUnits(const Kinetics& kin) rate_units.join(-1); } + Reaction::rate_units = rate_units.product(); return rate_units; } From fa7797b60aa5455163d19acb420633db0057dfb3 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 11 Apr 2023 11:54:45 -0400 Subject: [PATCH 15/26] Add tests for rate coefficient units --- interfaces/cython/cantera/units.pyx | 2 +- test/python/test_reaction.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/units.pyx b/interfaces/cython/cantera/units.pyx index 7a58664422..ef78d76c6a 100644 --- a/interfaces/cython/cantera/units.pyx +++ b/interfaces/cython/cantera/units.pyx @@ -44,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 diff --git a/test/python/test_reaction.py b/test/python/test_reaction.py index f3ce9bbda1..37bd9f8f00 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -942,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): @@ -1106,6 +1107,10 @@ 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 check_equal(self, one, two): # helper function for deprecation tests self.assertEqual(type(one), type(two)) @@ -1130,6 +1135,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): @@ -1150,6 +1156,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 @@ -1176,6 +1183,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 @@ -1207,6 +1215,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) @@ -1232,6 +1241,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] @@ -1250,6 +1260,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): @@ -1274,6 +1285,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): @@ -1312,6 +1324,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): @@ -1346,6 +1359,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): @@ -1385,6 +1399,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): @@ -1435,6 +1450,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) @@ -1450,6 +1466,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) @@ -1553,6 +1570,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() @@ -1762,6 +1780,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 @@ -1782,6 +1801,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 @@ -1799,6 +1819,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 @@ -1819,6 +1840,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 @@ -1878,6 +1900,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 @@ -1900,6 +1923,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 @@ -1920,6 +1944,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 @@ -1940,4 +1965,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 From 953dafb3a1957a856f5ef93ff97ad929a55f53e4 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 11 Apr 2023 22:13:21 -0400 Subject: [PATCH 16/26] [Test] Avoid modifying shared Solution object --- test/python/test_kinetics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From e02bedf9aa7c9c8a1427abfac41160dfd41fb488 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 9 Apr 2023 23:40:53 -0400 Subject: [PATCH 17/26] Distinguish between rate units and units in rate parameterization These differ specifically in the case of sticking reactions, where the rate coefficient has units like m^3/kmol/s, depending on the reactant orders while the sticking coefficient itself is dimensionless. --- include/cantera/kinetics/Arrhenius.h | 13 +++------- include/cantera/kinetics/ChebyshevRate.h | 2 -- include/cantera/kinetics/InterfaceRate.h | 12 ++++++---- include/cantera/kinetics/ReactionRate.h | 30 ++++++++++++++++++++++++ interfaces/cython/cantera/reaction.pxd | 1 + interfaces/cython/cantera/reaction.pyx | 8 +++++++ src/kinetics/Arrhenius.cpp | 13 +++++----- src/kinetics/ChebyshevRate.cpp | 5 ++-- src/kinetics/Falloff.cpp | 4 ++-- src/kinetics/Kinetics.cpp | 2 +- test/python/test_reaction.py | 8 +++++++ 11 files changed, 69 insertions(+), 29 deletions(-) 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/ReactionRate.h b/include/cantera/kinetics/ReactionRate.h index 33e66208a8..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; } @@ -107,6 +110,30 @@ class ReactionRate 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) {} @@ -225,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/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 5807a95d94..dbe483d4b3 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -147,6 +147,14 @@ cdef class ReactionRate: def __get__(self): 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): """ diff --git a/src/kinetics/Arrhenius.cpp b/src/kinetics/Arrhenius.cpp index 4ded086ad5..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,16 +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(); - m_A = units.convertRateCoeff(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"); @@ -59,7 +58,7 @@ void ArrheniusBase::setRateParameters( } } else { auto& rate_vec = rate.asVector(2, 4); - m_A = units.convertRateCoeff(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"); @@ -81,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 diff --git a/src/kinetics/ChebyshevRate.cpp b/src/kinetics/ChebyshevRate.cpp index 4fdcdbe5e7..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,7 +79,7 @@ void ChebyshevRate::setParameters(const AnyMap& node, const UnitStack& rate_unit coeffs(i, j) = vcoeffs[i][j]; } } - double offset = unit_system.convertRateCoeff(AnyValue(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"), @@ -142,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 592842708a..a8a354902a 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -344,7 +344,7 @@ void TroeRate::getParameters(AnyMap& node) const AnyMap params; if (!valid()) { // pass - } else if (m_lowRate.rateUnits().factor() != 0.0) { + } else if (m_lowRate.conversionUnits().factor() != 0.0) { params["A"] = m_a; params["T3"].setQuantity(1.0 / m_rt3, "K"); params["T1"].setQuantity(1.0 / m_rt1, "K"); @@ -472,7 +472,7 @@ void SriRate::getParameters(AnyMap& node) const AnyMap params; if (!valid()) { // pass - } else if (m_lowRate.rateUnits().factor() != 0.0) { + } else if (m_lowRate.conversionUnits().factor() != 0.0) { params["A"] = m_a; params["B"].setQuantity(m_b, "K"); params["C"].setQuantity(m_c, "K"); 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/test/python/test_reaction.py b/test/python/test_reaction.py index 37bd9f8f00..97ef43f239 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -1111,6 +1111,10 @@ 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)) @@ -1883,6 +1887,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 From b653767efbc22bb37601f135fe9c53d36219de1d Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 12 Apr 2023 00:09:28 -0400 Subject: [PATCH 18/26] [Kinetics] Simplify falloff serialization --- src/kinetics/Falloff.cpp | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index a8a354902a..82a6d9a779 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -342,22 +342,13 @@ void TroeRate::getParameters(AnyMap& node) const FalloffRate::getParameters(node); AnyMap params; - if (!valid()) { - // pass - } else if (m_lowRate.conversionUnits().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); @@ -470,9 +461,7 @@ void SriRate::getParameters(AnyMap& node) const FalloffRate::getParameters(node); AnyMap params; - if (!valid()) { - // pass - } else if (m_lowRate.conversionUnits().factor() != 0.0) { + if (valid()) { params["A"] = m_a; params["B"].setQuantity(m_b, "K"); params["C"].setQuantity(m_c, "K"); @@ -480,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); From 83cf2727cd0621f05868c3ac3b7cece0a3250bf1 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 14 Apr 2023 22:37:03 -0400 Subject: [PATCH 19/26] [Python] Implement output unit conversions for AnyMap --- interfaces/cython/cantera/_utils.pxd | 8 +++- interfaces/cython/cantera/_utils.pyx | 44 +++++++++++++++++++++- interfaces/cython/cantera/solutionbase.pyx | 2 +- test/python/test_reaction.py | 41 ++++++++++++++++++-- test/python/test_utils.py | 28 ++++++++++++++ 5 files changed, 115 insertions(+), 8 deletions(-) diff --git a/interfaces/cython/cantera/_utils.pxd b/interfaces/cython/cantera/_utils.pxd index b616c4cc87..c27d66595a 100644 --- a/interfaces/cython/cantera/_utils.pxd +++ b/interfaces/cython/cantera/_utils.pxd @@ -7,7 +7,7 @@ from libcpp.unordered_map cimport unordered_map from .ctcxx cimport * -from .units cimport UnitSystem +from .units cimport UnitSystem, CxxUnits cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": cdef cppclass CxxAnyValue "Cantera::AnyValue" @@ -37,7 +37,7 @@ 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": @@ -51,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]() diff --git a/interfaces/cython/cantera/_utils.pyx b/interfaces/cython/cantera/_utils.pyx index 4c61febe29..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,6 +153,10 @@ 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 @@ -199,6 +205,22 @@ cdef class AnyMap(dict): """ 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 @@ -261,6 +283,22 @@ cdef anymap_to_py(CxxAnyMap& m): 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 py_to_anymap(data, cbool hyphenize=False) except *: cdef CxxAnyMap m if hyphenize: @@ -274,8 +312,10 @@ cdef CxxAnyMap py_to_anymap(data, cbool hyphenize=False) except *: data = _hyphenize(data) for k, v in data.items(): - m[stringify(k)] = python_to_anyvalue(v, k) - m.applyUnits() + if isinstance(v, _DimensionalValue): + setQuantity(m, k, v) + else: + m[stringify(k)] = python_to_anyvalue(v, k) return m cdef get_types(item): diff --git a/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index 65eb3fca86..c0c2f6bbdc 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -274,7 +274,7 @@ cdef class _SolutionBase: def __get__(self): 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 diff --git a/test/python/test_reaction.py b/test/python/test_reaction.py index 97ef43f239..e9a2b817e3 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -1688,9 +1688,9 @@ def set_parameters(self, params, rc_units): self.Ta = params.convert_activation_energy("Ea", "K") def get_parameters(self, params): - params["A"] = self.A - params["L"] = self.length - params["Ea"] = self.Ta + 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 eval(self, data): return self.A * (self.length / 2.0)**2 * exp(-self.Ta/data.T) @@ -1713,6 +1713,41 @@ def test_explicit_units(self): 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) + class InterfaceReactionTests(ReactionTests): # test suite for surface reaction expressions diff --git a/test/python/test_utils.py b/test/python/test_utils.py index 03e08a3642..8b9dffd784 100644 --- a/test/python/test_utils.py +++ b/test/python/test_utils.py @@ -294,3 +294,31 @@ def test_units_activation_energy(self): 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) From d2b0e9c1859e8e1d46c23e942962109d7a116766 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 14 Apr 2023 22:45:05 -0400 Subject: [PATCH 20/26] Update gitignore --- test_problems/.gitignore | 1 + 1 file changed, 1 insertion(+) 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 From c1136f986ded9d0397924c9336049bc47bc25e52 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 16 Apr 2023 14:10:36 -0400 Subject: [PATCH 21/26] Make functions with signature void(string&, void*) delegatable --- include/cantera/base/Delegator.h | 23 +++++++++++++++++++++++ interfaces/cython/cantera/delegator.pxd | 3 +++ interfaces/cython/cantera/delegator.pyx | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/include/cantera/base/Delegator.h b/include/cantera/base/Delegator.h index 7dad72eb8e..3b38588fe8 100644 --- a/include/cantera/base/Delegator.h +++ b/include/cantera/base/Delegator.h @@ -172,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, @@ -326,6 +339,15 @@ 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, std::function, double*)>& target, @@ -504,6 +526,7 @@ class Delegator 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::mapexc_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 @@ -329,6 +339,9 @@ cdef int assign_delegates(obj, CxxDelegator* delegator) except -1: 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) From 16f3682788b0ae12156024d686454edf5cae2695 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 16 Apr 2023 22:47:38 -0400 Subject: [PATCH 22/26] [Delegator] Move handling of Solution wrapper name to ExtensionManager --- include/cantera/base/ExtensionManager.h | 14 ++++++++++++-- include/cantera/kinetics/ReactionRateDelegator.h | 10 ---------- src/base/ExtensionManager.cpp | 14 +++++++++++++- src/extensions/PythonExtensionManager.cpp | 3 +-- src/kinetics/ReactionRateDelegator.cpp | 8 ++++---- 5 files changed, 30 insertions(+), 19 deletions(-) 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/kinetics/ReactionRateDelegator.h b/include/cantera/kinetics/ReactionRateDelegator.h index 437561c2d7..ef535fa391 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; 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/extensions/PythonExtensionManager.cpp b/src/extensions/PythonExtensionManager.cpp index 4ef0e1a05d..467714635f 100644 --- a/src/extensions/PythonExtensionManager.cpp +++ b/src/extensions/PythonExtensionManager.cpp @@ -117,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) { @@ -127,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/ReactionRateDelegator.cpp b/src/kinetics/ReactionRateDelegator.cpp index bde44e8c67..31273791e6 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()); From 20de305c957e5ba3f2dfe276c4d3304fe0a7b118 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 17 Apr 2023 14:06:19 -0400 Subject: [PATCH 23/26] [Kinetics] Make Solution object available while adding reactions --- include/cantera/kinetics/KineticsFactory.h | 4 +++- interfaces/cython/cantera/solutionbase.pyx | 4 ++-- src/base/Solution.cpp | 2 +- src/kinetics/KineticsFactory.cpp | 7 ++++++- 4 files changed, 12 insertions(+), 5 deletions(-) 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/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index c0c2f6bbdc..fef09e96a8 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -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: """ 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/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); } From f3542ff5e6acbaec3bee94d2a4ee70550256fea2 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 17 Apr 2023 14:08:35 -0400 Subject: [PATCH 24/26] Implement ExtensibleRate.validate --- .../cantera/kinetics/ReactionRateDelegator.h | 3 ++ interfaces/cython/cantera/reaction.pyx | 10 +++++ src/kinetics/ReactionRateDelegator.cpp | 25 +++++++++++ test/python/test_reaction.py | 41 +++++++++++++++++-- 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/include/cantera/kinetics/ReactionRateDelegator.h b/include/cantera/kinetics/ReactionRateDelegator.h index ef535fa391..a8e716d06d 100644 --- a/include/cantera/kinetics/ReactionRateDelegator.h +++ b/include/cantera/kinetics/ReactionRateDelegator.h @@ -97,6 +97,8 @@ class ReactionRateDelegator : public Delegator, public ReactionRate m_getParameters(node); } + void validate(const string& equation, const Kinetics& kin) override; + private: //! The name of the reaction rate type string m_rateType; @@ -105,6 +107,7 @@ class ReactionRateDelegator : public Delegator, public ReactionRate //! ReactionData wrapper object function m_evalFromStruct; + function m_validate; function m_setParameters; function m_getParameters; }; diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index dbe483d4b3..4a2a98b0d3 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -758,6 +758,7 @@ cdef class ExtensibleRate(ReactionRate): "eval": ("evalFromStruct", "double(void*)", "replace"), "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): @@ -796,6 +797,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() diff --git a/src/kinetics/ReactionRateDelegator.cpp b/src/kinetics/ReactionRateDelegator.cpp index 31273791e6..17a4fdb299 100644 --- a/src/kinetics/ReactionRateDelegator.cpp +++ b/src/kinetics/ReactionRateDelegator.cpp @@ -53,6 +53,9 @@ ReactionRateDelegator::ReactionRateDelegator() 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 @@ -64,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/test/python/test_reaction.py b/test/python/test_reaction.py index e9a2b817e3..f22d74f45c 100644 --- a/test/python/test_reaction.py +++ b/test/python/test_reaction.py @@ -1538,13 +1538,26 @@ def update(self, gas): @ct.extension(name="user-rate-1", data=UserRate1Data) class UserRate1(ct.ExtensibleRate): + def __init__(self, *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) @@ -1587,7 +1600,11 @@ def eval_rate(self, rate): return gas.forward_rate_constants[0] def test_no_rate(self): - pytest.skip("ExtensibleRate does not yet support validation") + # 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_parameter_access(self): gas = ct.Solution(yaml=self._phase_def) @@ -1605,8 +1622,9 @@ def test_standalone_rate(self): 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) @@ -1692,6 +1710,10 @@ def get_parameters(self, params): 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) @@ -1748,6 +1770,19 @@ def test_output_units(self): 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 From dffa5cbe518bf776b48fe9e6cd2daaa3827d7d7b Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 16 Apr 2023 22:23:27 -0400 Subject: [PATCH 25/26] Update ExtensibleReaction example to model additional methods --- samples/python/kinetics/custom_reactions.py | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) 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() From 8b27a52a298a124724a426a102af4548755fb2a1 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 18 Apr 2023 09:52:30 -0400 Subject: [PATCH 26/26] [Python] Clarify use of unit conversion functions --- interfaces/cython/cantera/reaction.pyx | 16 +++++++++++++--- interfaces/cython/cantera/units.pyx | 3 +++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index 4a2a98b0d3..e925a470cf 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -742,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. @@ -779,6 +779,11 @@ cdef class ExtensibleRate(ReactionRate): for reactions created from YAML, ``params`` is the YAML reaction entry converted 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") @@ -786,6 +791,11 @@ cdef class ExtensibleRate(ReactionRate): """ 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") diff --git a/interfaces/cython/cantera/units.pyx b/interfaces/cython/cantera/units.pyx index ef78d76c6a..9b590d5fce 100644 --- a/interfaces/cython/cantera/units.pyx +++ b/interfaces/cython/cantera/units.pyx @@ -96,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({