From 02baaee02e507d17bad172a8956fdb8a28180159 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 22:13:24 -0600 Subject: [PATCH 01/14] [Python] Implement string output for Solution.write_yaml() --- interfaces/cython/cantera/base.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 4ff9337ef38..1333705c1cc 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -283,14 +283,14 @@ cdef class _SolutionBase: """ self.base.header().clear() - def write_yaml(self, filename, phases=None, units=None, precision=None, + def write_yaml(self, filename=None, phases=None, units=None, precision=None, skip_user_defined=None, header=True): """ Write the definition for this phase, any additional phases specified, and their species and reactions to the specified file. :param filename: - The name of the output file + The name of the output file; if ``None``, a YAML string is returned :param phases: Additional ThermoPhase / Solution objects to be included in the output file @@ -328,6 +328,8 @@ cdef class _SolutionBase: Y.precision = precision if skip_user_defined is not None: Y.skip_user_defined = skip_user_defined + if filename is None: + return Y.to_string() Y.to_file(filename) def __getitem__(self, selection): From 3052468ac93515d5d646951ad4c4fe9bf30030f9 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 13 Aug 2019 10:33:02 -0500 Subject: [PATCH 02/14] [Input] Implement pickle serialization for Solution objects --- interfaces/cython/cantera/base.pyx | 26 +++++++++++++++++-- interfaces/cython/cantera/test/test_thermo.py | 5 ---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 1333705c1cc..21ff9f795d5 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -9,6 +9,18 @@ cdef class _SolutionBase: source=None, yaml=None, thermo=None, species=(), kinetics=None, reactions=(), **kwargs): + # run instantiation only if valid sources are specified + if origin or infile or source or yaml or (thermo and species): + + self._cinit(infile=infile, name=name, adjacent=adjacent, + origin=origin, source=source, yaml=yaml, + thermo=thermo, species=species, kinetics=kinetics, + reactions=reactions, **kwargs) + + def _cinit(self, infile='', name='', adjacent=(), origin=None, + source=None, yaml=None, thermo=None, species=(), + kinetics=None, reactions=(), **kwargs): + if 'phaseid' in kwargs: if name is not '': raise AttributeError('duplicate specification of phase name') @@ -351,8 +363,18 @@ cdef class _SolutionBase: for i,spec in enumerate(species): self._selected_species[i] = self.species_index(spec) - def __reduce__(self): - raise NotImplementedError('Solution object is not picklable') + def __getstate__(self): + """Save complete information of the current phase for pickling.""" + if self.kinetics.nTotalSpecies() > self.thermo.nSpecies(): + raise NotImplementedError( + "Pickling of Interface objects is not implemented.") + return self.write_yaml(), self.state + + def __setstate__(self, pkl): + """Restore Solution from pickled information.""" + yml, state = pkl + self._cinit(yaml=yml) + self.state = state def __copy__(self): raise NotImplementedError('Solution object is not copyable') diff --git a/interfaces/cython/cantera/test/test_thermo.py b/interfaces/cython/cantera/test/test_thermo.py index 43f32bf55f8..a2189bf241c 100644 --- a/interfaces/cython/cantera/test/test_thermo.py +++ b/interfaces/cython/cantera/test/test_thermo.py @@ -740,11 +740,6 @@ def test_ref_info(self): self.assertNear(self.phase.min_temp, 300.0) self.assertNear(self.phase.max_temp, 3500.0) - def test_unpicklable(self): - import pickle - with self.assertRaises(NotImplementedError): - pickle.dumps(self.phase) - def test_uncopyable(self): import copy with self.assertRaises(NotImplementedError): From f50b6393ae8659101748152bbf412d75136001fa Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Fri, 16 Apr 2021 12:49:02 -0500 Subject: [PATCH 03/14] [Python] Mark SolutionArray as not picklable --- interfaces/cython/cantera/composite.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 1d0caa0861c..fbb658fdb39 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -1375,6 +1375,12 @@ def strip_ext(source): return root_attrs + def __reduce__(self): + raise NotImplementedError('SolutionArray object is not picklable') + + def __copy__(self): + raise NotImplementedError('SolutionArray object is not copyable') + def _state2_prop(name, doc_source): # Factory for creating properties which consist of a tuple of two variables, From 711d8b7ddd1a08ed75f8899d6740ca8d49189f39 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 19:59:45 -0600 Subject: [PATCH 04/14] [Kinetics] Clarify base Kinetics::kineticsType as 'None' --- include/cantera/kinetics/Kinetics.h | 2 +- src/kinetics/KineticsFactory.cpp | 5 +++-- test/kinetics/kineticsFromYaml.cpp | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/include/cantera/kinetics/Kinetics.h b/include/cantera/kinetics/Kinetics.h index 8975dd1eb05..7647509e369 100644 --- a/include/cantera/kinetics/Kinetics.h +++ b/include/cantera/kinetics/Kinetics.h @@ -130,7 +130,7 @@ class Kinetics //! Each class derived from Kinetics should override this method to return //! a meaningful identifier. virtual std::string kineticsType() const { - return "Kinetics"; + return "None"; } //! Finalize Kinetics object and associated objects diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index 72e61b3fc43..c551b427820 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -42,6 +42,7 @@ Kinetics* KineticsFactory::newKinetics(XML_Node& phaseData, KineticsFactory::KineticsFactory() { reg("none", []() { return new Kinetics(); }); addAlias("none", "Kinetics"); + addAlias("none", "None"); reg("gas", []() { return new GasKinetics(); }); addAlias("gas", "gaskinetics"); addAlias("gas", "Gas"); @@ -106,7 +107,7 @@ void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode vector sections, rules; if (phaseNode.hasKey("reactions")) { - if (kin.kineticsType() == "Kinetics") { + if (kin.kineticsType() == "None") { throw InputFileError("addReactions", phaseNode["reactions"], "Phase entry includes a 'reactions' field but does not " "specify a kinetics model."); @@ -137,7 +138,7 @@ void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode rules.push_back(item.begin()->second.asString()); } } - } else if (kin.kineticsType() != "Kinetics") { + } else if (kin.kineticsType() != "None") { if (rootNode.hasKey("reactions")) { // Default behavior is to add all reactions from the 'reactions' // section, if a 'kinetics' model has been specified diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index a3d0440a08a..97de849cf60 100644 --- a/test/kinetics/kineticsFromYaml.cpp +++ b/test/kinetics/kineticsFromYaml.cpp @@ -404,7 +404,7 @@ TEST(KineticsFromYaml, NoKineticsModelOrReactionsField1) { auto soln = newSolution("phase-reaction-spec1.yaml", "nokinetics-noreactions"); - EXPECT_EQ(soln->kinetics()->kineticsType(), "Kinetics"); + EXPECT_EQ(soln->kinetics()->kineticsType(), "None"); EXPECT_EQ(soln->kinetics()->nReactions(), (size_t) 0); } @@ -412,7 +412,7 @@ TEST(KineticsFromYaml, NoKineticsModelOrReactionsField2) { auto soln = newSolution("phase-reaction-spec2.yaml", "nokinetics-noreactions"); - EXPECT_EQ(soln->kinetics()->kineticsType(), "Kinetics"); + EXPECT_EQ(soln->kinetics()->kineticsType(), "None"); EXPECT_EQ(soln->kinetics()->nReactions(), (size_t) 0); } From c6c2aa2ea7443672b1bfa4bda6e08e1f2c6c4d83 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 20:00:15 -0600 Subject: [PATCH 05/14] [Thermo] Clarify base ThermoPhase::type as 'None' --- include/cantera/thermo/ThermoPhase.h | 2 +- src/thermo/ThermoFactory.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index 1c5762a9462..52d15a4dcb5 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -111,7 +111,7 @@ class ThermoPhase : public Phase //! @{ virtual std::string type() const { - return "ThermoPhase"; + return "None"; } //! String indicating the mechanical phase of the matter in this Phase. diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index 6275397e923..b1828f3de74 100644 --- a/src/thermo/ThermoFactory.cpp +++ b/src/thermo/ThermoFactory.cpp @@ -48,6 +48,9 @@ std::mutex ThermoFactory::thermo_mutex; ThermoFactory::ThermoFactory() { + reg("none", []() { return new ThermoPhase(); }); + addAlias("none", "ThermoPhase"); + addAlias("none", "None"); reg("ideal-gas", []() { return new IdealGasPhase(); }); addAlias("ideal-gas", "IdealGas"); reg("ideal-surface", []() { return new SurfPhase(); }); From 1dfa531eb3db0c260fbd6f2ee65d78652ad9e0e0 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 21:23:53 -0600 Subject: [PATCH 06/14] [Transport] Register base model as 'none' in TransportFactory --- src/transport/TransportFactory.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/transport/TransportFactory.cpp b/src/transport/TransportFactory.cpp index 6fe71260c48..f5a19bb9c91 100644 --- a/src/transport/TransportFactory.cpp +++ b/src/transport/TransportFactory.cpp @@ -29,10 +29,10 @@ std::mutex TransportFactory::transport_mutex; TransportFactory::TransportFactory() { - reg("", []() { return new Transport(); }); - addAlias("", "Transport"); - addAlias("", "None"); - addAlias("", "none"); + reg("none", []() { return new Transport(); }); + addAlias("none", "Transport"); + addAlias("none", "None"); + addAlias("none", ""); reg("unity-Lewis-number", []() { return new UnityLewisTransport(); }); addAlias("unity-Lewis-number", "UnityLewis"); reg("mixture-averaged", []() { return new MixTransport(); }); @@ -63,7 +63,7 @@ void TransportFactory::deleteFactory() Transport* TransportFactory::newTransport(const std::string& transportModel, ThermoPhase* phase, int log_level) { - if (transportModel != "DustyGas" && canonicalize(transportModel) == "") { + if (transportModel != "DustyGas" && canonicalize(transportModel) == "none") { return create(transportModel); } if (!phase) { From 189d7260c53701ae369495fb6f2565e04a568ef3 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 20:01:46 -0600 Subject: [PATCH 07/14] [Python] Enable safe handling of empty Solution objects --- interfaces/cython/cantera/base.pyx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 21ff9f795d5..686adeb740a 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -16,6 +16,19 @@ cdef class _SolutionBase: origin=origin, source=source, yaml=yaml, thermo=thermo, species=species, kinetics=kinetics, reactions=reactions, **kwargs) + return + + self._base = CxxNewSolution() + self.base = self._base.get() + self.base.setSource(stringify("none")) + + self.base.setThermo(newThermo(stringify("none"))) + self.thermo = self.base.thermo().get() + self.base.setKinetics(newKinetics(stringify("none"))) + self.kinetics = self.base.kinetics().get() + self.base.setTransport(newTransport(NULL, stringify("none"))) + self.transport = self.base.transport().get() + self._selected_species = np.ndarray(0, dtype=np.uint64) def _cinit(self, infile='', name='', adjacent=(), origin=None, source=None, yaml=None, thermo=None, species=(), From 088d17f5a6d771e8190e255671f02431fa0c7e84 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 20:14:02 -0600 Subject: [PATCH 08/14] [Thermo] Make error messages more meaningful --- include/cantera/thermo/Phase.h | 6 ++++-- src/thermo/ThermoPhase.cpp | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/include/cantera/thermo/Phase.h b/include/cantera/thermo/Phase.h index f80bbccc3db..7457fac1f6f 100644 --- a/include/cantera/thermo/Phase.h +++ b/include/cantera/thermo/Phase.h @@ -664,7 +664,8 @@ class Phase * \rho, Y_1, \dots, Y_K) \f$. Alternatively, it returns a stored value. */ virtual double pressure() const { - throw NotImplementedError("Phase::pressure"); + throw NotImplementedError("Phase::pressure", + "Not implemented for thermo model '{}'", type()); } //! Density (kg/m^3). @@ -703,7 +704,8 @@ class Phase * @param p input Pressure (Pa) */ virtual void setPressure(double p) { - throw NotImplementedError("Phase::setPressure"); + throw NotImplementedError("Phase::setPressure", + "Not implemented for thermo model '{}'", type()); } //! Set the internally stored temperature of the phase (K). diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 778f30ae66c..e46c47c934e 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1403,6 +1403,11 @@ void ThermoPhase::getdlnActCoeffdlnN_numderiv(const size_t ld, doublereal* const std::string ThermoPhase::report(bool show_thermo, doublereal threshold) const { + if (type() == "None") { + throw NotImplementedError("ThermoPhase::report", + "Not implemented for thermo model 'None'"); + } + fmt::memory_buffer b; // This is the width of the first column of names in the report. int name_width = 18; From 288111117d21c18b4a4c4c8e5fb3fd245f2b39b5 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 20:45:58 -0600 Subject: [PATCH 09/14] [Python] Accommodate empty Kinetics objects --- interfaces/cython/cantera/kinetics.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interfaces/cython/cantera/kinetics.pyx b/interfaces/cython/cantera/kinetics.pyx index 94b24f01f38..acf5ed1d9de 100644 --- a/interfaces/cython/cantera/kinetics.pyx +++ b/interfaces/cython/cantera/kinetics.pyx @@ -8,6 +8,8 @@ from ctypes import c_int # e.g. class Solution. [Cython 0.16] cdef np.ndarray get_species_array(Kinetics kin, kineticsMethod1d method): cdef np.ndarray[np.double_t, ndim=1] data = np.empty(kin.n_total_species) + if kin.n_total_species == 0: + return data method(kin.kinetics, &data[0]) # @todo: Fix _selected_species to work with interface kinetics if kin._selected_species.size: @@ -17,6 +19,8 @@ cdef np.ndarray get_species_array(Kinetics kin, kineticsMethod1d method): cdef np.ndarray get_reaction_array(Kinetics kin, kineticsMethod1d method): cdef np.ndarray[np.double_t, ndim=1] data = np.empty(kin.n_reactions) + if kin.n_reactions == 0: + return data method(kin.kinetics, &data[0]) return data From 0cdee6f3582c139a456f252c1e6ca57cf710d9db Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 8 Dec 2021 19:03:15 -0600 Subject: [PATCH 10/14] [Python] Ensure transport handler is always properly set --- interfaces/cython/cantera/base.pyx | 11 ++++++++--- interfaces/cython/cantera/transport.pyx | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 686adeb740a..346cf639965 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -177,6 +177,9 @@ cdef class _SolutionBase: self.base.setKinetics(newKinetics(stringify("none"))) self.kinetics = self.base.kinetics().get() + # Transport + self.transport = self.base.transport().get() + def _init_cti_xml(self, infile, name, adjacent, source): """ Instantiate a set of new Cantera C++ objects from a CTI or XML @@ -381,12 +384,14 @@ cdef class _SolutionBase: if self.kinetics.nTotalSpecies() > self.thermo.nSpecies(): raise NotImplementedError( "Pickling of Interface objects is not implemented.") - return self.write_yaml(), self.state + transport_model = pystr(self.transport.transportType()) + return self.write_yaml(), self.state, transport_model def __setstate__(self, pkl): """Restore Solution from pickled information.""" - yml, state = pkl - self._cinit(yaml=yml) + yml, state, transport_model = pkl + self._cinit(yaml=yml, transport_model=transport_model) + self.state = state def __copy__(self): diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index d87a452803e..751e73c9ca1 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -168,6 +168,7 @@ cdef class Transport(_SolutionBase): # The signature of this function causes warnings for Sphinx documentation def __init__(self, *args, **kwargs): if self.transport == NULL: + # @todo ... after removal of CTI/XML, this should be handled by base.pyx if 'transport_model' not in kwargs: self.base.setTransport(newTransport(self.thermo, stringify("default"))) else: From aef96df09d8d4be5c428cade37d4fb8c610bc852 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 14 Dec 2021 08:53:25 -0600 Subject: [PATCH 11/14] [Python] Simplify pickle and update instantiation logic --- interfaces/cython/cantera/base.pyx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 346cf639965..7c70fb14128 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -384,15 +384,12 @@ cdef class _SolutionBase: if self.kinetics.nTotalSpecies() > self.thermo.nSpecies(): raise NotImplementedError( "Pickling of Interface objects is not implemented.") - transport_model = pystr(self.transport.transportType()) - return self.write_yaml(), self.state, transport_model + return self.write_yaml() def __setstate__(self, pkl): """Restore Solution from pickled information.""" - yml, state, transport_model = pkl - self._cinit(yaml=yml, transport_model=transport_model) - - self.state = state + yml = pkl + self._cinit(yaml=yml) def __copy__(self): raise NotImplementedError('Solution object is not copyable') From 24a7d3c0f4e4421a34daf12bcaa1af0fb4ff57c7 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 14 Dec 2021 09:45:47 -0600 Subject: [PATCH 12/14] [Python] Ensure that ThermoPhase constructor is always called first --- interfaces/cython/cantera/_cantera.pxd | 2 +- interfaces/cython/cantera/base.pyx | 1 + interfaces/cython/cantera/composite.py | 6 +++--- interfaces/cython/cantera/liquidvapor.py | 2 +- interfaces/cython/cantera/thermo.pyx | 3 +++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 30223c9798d..1e624510666 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -1246,6 +1246,7 @@ cdef class _SolutionBase: cdef int thermo_basis cdef np.ndarray _selected_species cdef object parent + cdef public object _references cdef class Species: cdef shared_ptr[CxxSpecies] _species @@ -1261,7 +1262,6 @@ cdef class ThermoPhase(_SolutionBase): cpdef int species_index(self, species) except * cdef np.ndarray _getArray1(self, thermoMethod1d method) cdef void _setArray1(self, thermoMethod1d method, values) except * - cdef public object _references cdef class InterfacePhase(ThermoPhase): cdef CxxSurfPhase* surf diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 7c70fb14128..d8a5cfacafa 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -10,6 +10,7 @@ cdef class _SolutionBase: kinetics=None, reactions=(), **kwargs): # run instantiation only if valid sources are specified + self._references = None if origin or infile or source or yaml or (thermo and species): self._cinit(infile=infile, name=name, adjacent=adjacent, diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index fbb658fdb39..4fa8ff51592 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -25,7 +25,7 @@ import pandas as _pandas -class Solution(ThermoPhase, Kinetics, Transport): +class Solution(Transport, Kinetics, ThermoPhase): """ A class for chemically-reacting solutions. Instances can be created to represent any type of solution -- a mixture of gases, a liquid solution, or @@ -101,7 +101,7 @@ class Solution(ThermoPhase, Kinetics, Transport): __slots__ = () -class Interface(InterfacePhase, InterfaceKinetics): +class Interface(InterfaceKinetics, InterfacePhase): """ Two-dimensional interfaces. @@ -121,7 +121,7 @@ class Interface(InterfacePhase, InterfaceKinetics): __slots__ = ('_phase_indices',) -class DustyGas(ThermoPhase, Kinetics, DustyGasTransport): +class DustyGas(DustyGasTransport, Kinetics, ThermoPhase): """ A composite class which models a gas in a stationary, solid, porous medium. diff --git a/interfaces/cython/cantera/liquidvapor.py b/interfaces/cython/cantera/liquidvapor.py index 8bf6a17d834..48a5837263c 100644 --- a/interfaces/cython/cantera/liquidvapor.py +++ b/interfaces/cython/cantera/liquidvapor.py @@ -41,7 +41,7 @@ def Water(backend='Reynolds'): :ct:WaterSSTP and :ct:WaterTransport in the Cantera C++ source code documentation. """ - class WaterWithTransport(PureFluid, _cantera.Transport): + class WaterWithTransport(_cantera.Transport, PureFluid): __slots__ = () if backend == 'Reynolds': diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 9499bbd7ee0..9a4e5de2f55 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -330,6 +330,9 @@ cdef class ThermoPhase(_SolutionBase): super().__init__(*args, **kwargs) if 'source' not in kwargs: self.thermo_basis = mass_basis + # In composite objects, the ThermoPhase constructor needs to be called first + # to prevent instantiation of stand-alone 'Kinetics' or 'Transport' objects. + # The following is used as a sentinel. self._references = weakref.WeakKeyDictionary() property thermo_model: From c31e757e7e1683ffb860f1c98f8bf6218d84385e Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 14 Dec 2021 09:47:55 -0600 Subject: [PATCH 13/14] [Python] Prevent stand-alone Kinetics and Transport objects Also, catch edge cases that provide insufficient information for phase instantiation. --- interfaces/cython/cantera/base.pyx | 7 ++++--- interfaces/cython/cantera/kinetics.pyx | 7 +++++++ interfaces/cython/cantera/transport.pyx | 5 +++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index d8a5cfacafa..563f546dfee 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -18,6 +18,9 @@ cdef class _SolutionBase: thermo=thermo, species=species, kinetics=kinetics, reactions=reactions, **kwargs) return + elif any([infile, source, adjacent, origin, source, yaml, + thermo, species, kinetics, reactions, kwargs]): + raise ValueError("Arguments are insufficient to define a phase") self._base = CxxNewSolution() self.base = self._base.get() @@ -89,10 +92,8 @@ cdef class _SolutionBase: # Parse inputs if infile or source: self._init_cti_xml(infile, name, adjacent, source) - elif thermo and species: - self._init_parts(thermo, species, kinetics, adjacent, reactions) else: - raise ValueError("Arguments are insufficient to define a phase") + self._init_parts(thermo, species, kinetics, adjacent, reactions) self._selected_species = np.ndarray(0, dtype=np.uint64) diff --git a/interfaces/cython/cantera/kinetics.pyx b/interfaces/cython/cantera/kinetics.pyx index acf5ed1d9de..d213f56e32f 100644 --- a/interfaces/cython/cantera/kinetics.pyx +++ b/interfaces/cython/cantera/kinetics.pyx @@ -64,6 +64,13 @@ cdef class Kinetics(_SolutionBase): of progress, species production rates, and other quantities pertaining to a reaction mechanism. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self._references is None: + raise ValueError( + "Cannot instantiate stand-alone 'Kinetics' object as it requires an " + "associated thermo phase.\nAll 'Kinetics' methods should be accessed " + "from a 'Solution' object.") property kinetics_model: """ diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index 751e73c9ca1..3f1874b1a98 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -179,6 +179,11 @@ cdef class Transport(_SolutionBase): self.transport = self.base.transport().get() super().__init__(*args, **kwargs) + if self._references is None: + raise ValueError( + "Cannot instantiate stand-alone 'Transport' object as it requires an " + "associated thermo phase.\nAll 'Transport' methods should be accessed " + "from a 'Solution' object.") property transport_model: """ From e3a7e289afb53271b788801be1de47f361215157 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sat, 27 Nov 2021 20:46:38 -0600 Subject: [PATCH 14/14] [UnitTests] Add tests for pickling and empty objects --- .../cython/cantera/test/test_composite.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index 49de22a99ef..04a52782b56 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -2,6 +2,7 @@ import numpy as np from collections import OrderedDict +import pickle import cantera as ct from cantera.composite import _h5py, _pandas @@ -116,6 +117,94 @@ def check(a, b): raise TypeError(msg) from inst +class TestPickle(utilities.CanteraTest): + + def test_pickle_gas(self): + gas = ct.Solution("h2o2.yaml", transport_model=None) + gas.TPX = 500, 500000, "H2:.75,O2:.25" + with open("gas.pkl", "wb") as pkl: + pickle.dump(gas, pkl) + + with open("gas.pkl", "rb") as pkl: + gas2 = pickle.load(pkl) + self.assertNear(gas.T, gas2.T) + self.assertNear(gas.P, gas2.P) + self.assertArrayNear(gas.X, gas2.X) + + self.assertEqual(gas2.transport_model, "None") + + def test_pickle_gas_with_transport(self): + gas = ct.Solution("h2o2.yaml") + gas.TPX = 500, 500000, "H2:.75,O2:.25" + gas.transport_model = "Multi" + with open("gas.pkl", "wb") as pkl: + pickle.dump(gas, pkl) + + with open("gas.pkl", "rb") as pkl: + gas2 = pickle.load(pkl) + self.assertNear(gas.T, gas2.T) + self.assertNear(gas.P, gas2.P) + self.assertArrayNear(gas.X, gas2.X) + + self.assertEqual(gas2.transport_model, "Multi") + + def test_pickle_interface(self): + gas = ct.Solution("diamond.yaml", "gas") + solid = ct.Solution("diamond.yaml", "diamond") + interface = ct.Interface("diamond.yaml", "diamond_100", (gas, solid)) + + with self.assertRaises(NotImplementedError): + with open("interface.pkl", "wb") as pkl: + pickle.dump(interface, pkl) + + +class TestEmptyThermoPhase(utilities.CanteraTest): + """ Test empty Solution object """ + @classmethod + def setUpClass(cls): + utilities.CanteraTest.setUpClass() + cls.gas = ct.ThermoPhase() + + def test_empty_report(self): + with self.assertRaisesRegex(ct.CanteraError, "NotImplementedError"): + self.gas() + + def test_empty_TP(self): + with self.assertRaisesRegex(ct.CanteraError, "NotImplementedError"): + self.gas.TP = 300, ct.one_atm + + def test_empty_equilibrate(self): + with self.assertRaisesRegex(ct.CanteraError, "NotImplementedError"): + self.gas.equilibrate("TP") + + +class TestEmptySolution(TestEmptyThermoPhase): + """ Test empty Solution object """ + @classmethod + def setUpClass(cls): + utilities.CanteraTest.setUpClass() + cls.gas = ct.Solution() + + def test_empty_composite(self): + self.assertEqual(self.gas.thermo_model, "None") + self.assertEqual(self.gas.composite, ("None", "None", "None")) + + +class TestEmptyEdgeCases(utilities.CanteraTest): + """ Test for edge cases where constructors are not allowed """ + def test_empty_phase(self): + with self.assertRaisesRegex(ValueError, "Arguments are insufficient to define a phase"): + ct.ThermoPhase(thermo="ideal-gas") + + def test_empty_kinetics(self): + with self.assertRaisesRegex(ValueError, "Cannot instantiate"): + ct.Kinetics() + + def test_empty_transport(self): + with self.assertRaisesRegex(ValueError, "Cannot instantiate"): + ct.Transport() + + class TestSolutionArrayIO(utilities.CanteraTest): """ Test SolutionArray file IO """ @classmethod