diff --git a/include/cantera/base/Solution.h b/include/cantera/base/Solution.h index f1c2716e8..df7949306 100644 --- a/include/cantera/base/Solution.h +++ b/include/cantera/base/Solution.h @@ -108,6 +108,22 @@ public: //! Returns a null pointer if the requested handle does not exist. shared_ptr getExternalHandle(const std::string& name) const; + //! Register a function to be called if any of the Solution's thermo, kinetics, + //! or transport objects is replaced. + //! @param id A unique ID corresponding to the object affected by the callback. + //! Typically, this is a pointer to an object that also holds a reference to the + //! Solution object. + //! @param callback The callback function to be called after any component of the + //! Solution is replaced. + //! When the callback becomes invalid (for example, the corresponding object is + //! being deleted, the removeChangedCallback() method must be invoked. + //! @since New in Cantera 3.0 + void registerChangedCallback(void* id, const function& callback); + + //! Remove the callback function associated with the specified object. + //! @since New in Cantera 3.0 + void removeChangedCallback(void* id); + protected: shared_ptr m_thermo; //!< ThermoPhase manager shared_ptr m_kinetics; //!< Kinetics manager @@ -124,6 +140,10 @@ protected: //! Wrappers for this Kinetics object in extension languages, for evaluation //! of user-defined reaction rates std::map> m_externalHandles; + + //! Callback functions that are invoked when the therm, kinetics, or transport + //! members of the Solution are replaced. + map> m_changeCallbacks; }; //! Create and initialize a new Solution from an input file diff --git a/include/cantera/oneD/StFlow.h b/include/cantera/oneD/StFlow.h index 916694947..f0b1de2e9 100644 --- a/include/cantera/oneD/StFlow.h +++ b/include/cantera/oneD/StFlow.h @@ -63,6 +63,8 @@ public: //! @param points initial number of grid points StFlow(shared_ptr sol, const std::string& id="", size_t points=1); + ~StFlow(); + //! @name Problem Specification //! @{ @@ -440,11 +442,6 @@ protected: Kinetics* m_kin; Transport* m_trans; - // Smart pointer preventing garbage collection when the transport model of an - // associated Solution object changes: the transport model of the StFlow object - // will remain unaffected by an external change. - shared_ptr m_trans_shared; - // boundary emissivities for the radiation calculations doublereal m_epsilon_left; doublereal m_epsilon_right; diff --git a/interfaces/cython/cantera/delegator.pxd b/interfaces/cython/cantera/delegator.pxd index f3a93fcfe..960b71d8c 100644 --- a/interfaces/cython/cantera/delegator.pxd +++ b/interfaces/cython/cantera/delegator.pxd @@ -84,3 +84,4 @@ cdef extern from "cantera/base/ExtensionManagerFactory.h" namespace "Cantera": ctypedef CxxDelegator* CxxDelegatorPtr cdef int assign_delegates(object, CxxDelegator*) except -1 +cdef void callback_v(PyFuncInfo& funcInfo) diff --git a/interfaces/cython/cantera/solutionbase.pxd b/interfaces/cython/cantera/solutionbase.pxd index 09297d258..5c18818bd 100644 --- a/interfaces/cython/cantera/solutionbase.pxd +++ b/interfaces/cython/cantera/solutionbase.pxd @@ -59,6 +59,8 @@ cdef extern from "cantera/base/Solution.h" namespace "Cantera": size_t nAdjacent() shared_ptr[CxxSolution] adjacent(size_t) void holdExternalHandle(string&, shared_ptr[CxxExternalHandle]) + void registerChangedCallback(void*, function[void()]) + void removeChangedCallback(void*) cdef shared_ptr[CxxSolution] CxxNewSolution "Cantera::Solution::create" () cdef shared_ptr[CxxSolution] newSolution ( @@ -86,4 +88,5 @@ cdef class _SolutionBase: cdef np.ndarray _selected_species cdef object parent cdef object _adjacent + cdef object _soln_changed_callback cdef public object _references diff --git a/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index dae9e5b7a..f1d776bb6 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -13,6 +13,7 @@ from .kinetics cimport * from .transport cimport * from .reaction cimport * from ._utils cimport * +from .delegator cimport pyOverride, callback_v from .yamlwriter cimport YamlWriter ctypedef CxxSurfPhase* CxxSurfPhasePtr @@ -110,6 +111,10 @@ cdef class _SolutionBase: if name is not None: self.name = name + def __dealloc__(self): + if self.base != NULL: + self.base.removeChangedCallback(self) + property name: """ The name assigned to this object. The default value corresponds @@ -429,11 +434,22 @@ cdef _assign_Solution(_SolutionBase soln, shared_ptr[CxxSolution] cxx_soln, if not weak: # When the main application isn't Python, we should only hold a weak reference # here, since the C++ Solution object owns this Python Solution. + if soln._base.get() != NULL: + soln._base.get().removeChangedCallback((soln)) soln._base = cxx_soln soln.base = cxx_soln.get() - soln.thermo = soln.base.thermo().get() - soln.kinetics = soln.base.kinetics().get() - soln.transport = soln.base.transport().get() + + def assign_pointers(): + soln.thermo = soln.base.thermo().get() + soln.kinetics = soln.base.kinetics().get() + soln.transport = soln.base.transport().get() + assign_pointers() + + soln.base.registerChangedCallback(soln, + pyOverride(assign_pointers, callback_v)) + # PyOverride only holds a weak reference to the function, so this also needs to be + # stored on the Python Solution object to have the right lifetime + soln._soln_changed_callback = assign_pointers cdef shared_ptr[CxxSolution] adj_soln if reset_adjacent: diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index 6a3f77f3d..94824546e 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -195,7 +195,6 @@ cdef class Transport(_SolutionBase): def __set__(self, model): self.base.setTransport(newTransport(self.thermo, stringify(model))) - self.transport = self.base.transport().get() property CK_mode: """Boolean to indicate if the chemkin interpretation is used.""" diff --git a/src/base/Solution.cpp b/src/base/Solution.cpp index 2f2fdc1ca..6b72b36c5 100644 --- a/src/base/Solution.cpp +++ b/src/base/Solution.cpp @@ -42,6 +42,9 @@ void Solution::setName(const std::string& name) { void Solution::setThermo(shared_ptr thermo) { m_thermo = thermo; + for (const auto& [id, callback] : m_changeCallbacks) { + callback(); + } } void Solution::setKinetics(shared_ptr kinetics) { @@ -49,10 +52,16 @@ void Solution::setKinetics(shared_ptr kinetics) { if (m_kinetics) { m_kinetics->setRoot(shared_from_this()); } + for (const auto& [id, callback] : m_changeCallbacks) { + callback(); + } } void Solution::setTransport(shared_ptr transport) { m_transport = transport; + for (const auto& [id, callback] : m_changeCallbacks) { + callback(); + } } void Solution::setTransportModel(const std::string& model) { @@ -152,10 +161,20 @@ shared_ptr Solution::getExternalHandle(const std::string& name) } } -shared_ptr newSolution(const std::string& infile, - const std::string& name, - const std::string& transport, - const std::vector>& adjacent) +void Solution::registerChangedCallback(void *id, const function& callback) +{ + m_changeCallbacks[id] = callback; +} + +void Solution::removeChangedCallback(void* id) +{ + m_changeCallbacks.erase(id); +} + +shared_ptr newSolution(const std::string &infile, + const std::string &name, + const std::string &transport, + const std::vector> &adjacent) { // get file extension size_t dot = infile.find_last_of("."); diff --git a/src/oneD/IonFlow.cpp b/src/oneD/IonFlow.cpp index b5d9eddac..6fdd885e1 100644 --- a/src/oneD/IonFlow.cpp +++ b/src/oneD/IonFlow.cpp @@ -64,8 +64,7 @@ IonFlow::IonFlow(shared_ptr sol, const std::string& id, size_t points) m_solution = sol; m_id = id; m_kin = m_solution->kinetics().get(); - m_trans_shared = m_solution->transport(); - m_trans = m_trans_shared.get(); + m_trans = m_solution->transport().get(); if (m_trans->transportModel() == "None") { // @deprecated warn_deprecated("IonFlow", @@ -74,6 +73,10 @@ IonFlow::IonFlow(shared_ptr sol, const std::string& id, size_t points) "is deprecated and\nwill be removed after Cantera 3.0."); setTransportModel("Ion"); } + m_solution->registerChangedCallback(this, [this]() { + setKinetics(*m_solution->kinetics()); + setTransport(*m_solution->transport()); + }); } void IonFlow::resize(size_t components, size_t points){ diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index 9b6a7b1ae..70e08347f 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -7,6 +7,7 @@ #include "cantera/oneD/StFlow.h" #include "cantera/oneD/refine.h" #include "cantera/transport/Transport.h" +#include "cantera/transport/TransportFactory.h" #include "cantera/numerics/funcs.h" #include "cantera/base/global.h" @@ -113,8 +114,7 @@ StFlow::StFlow(shared_ptr sol, const std::string& id, size_t points) m_solution = sol; m_id = id; m_kin = m_solution->kinetics().get(); - m_trans_shared = m_solution->transport(); - m_trans = m_trans_shared.get(); + m_trans = m_solution->transport().get(); if (m_trans->transportModel() == "None") { // @deprecated warn_deprecated("StFlow", @@ -123,6 +123,17 @@ StFlow::StFlow(shared_ptr sol, const std::string& id, size_t points) "is deprecated and\nwill be removed after Cantera 3.0."); setTransportModel("Mix"); } + m_solution->registerChangedCallback(this, [this]() { + setKinetics(*m_solution->kinetics()); + setTransport(*m_solution->transport()); + }); +} + +StFlow::~StFlow() +{ + if (m_solution) { + m_solution->removeChangedCallback(this); + } } void StFlow::setThermo(IdealGasPhase& th) { @@ -187,9 +198,6 @@ void StFlow::setTransportModel(const std::string& trans) "from a Solution manager: set Transport object directly instead."); } m_solution->setTransportModel(trans); - m_trans_shared = m_solution->transport(); - m_trans = m_trans_shared.get(); - setTransport(*m_trans); } std::string StFlow::transportModel() const { diff --git a/test/python/test_onedim.py b/test/python/test_onedim.py index 07e64ae6a..ed3bc0548 100644 --- a/test/python/test_onedim.py +++ b/test/python/test_onedim.py @@ -128,6 +128,22 @@ class TestOnedim(utilities.CanteraTest): self.assertEqual(rtol_ss, set((5e-3, 3e-4, 7e-7))) self.assertEqual(rtol_ts, set((6e-3, 4e-4, 2e-7))) + def test_switch_transport(self): + gas = ct.Solution('h2o2.yaml') + gas.set_equivalence_ratio(0.9, 'H2', 'O2:0.21, N2:0.79') + flame = ct.FreeFlame(gas, width=0.1) + flame.set_initial_guess() + + assert gas.transport_model == flame.transport_model == 'Mix' + + flame.transport_model = 'UnityLewis' + assert gas.transport_model == flame.transport_model == 'UnityLewis' + Dkm_flame = flame.mix_diff_coeffs + assert all(Dkm_flame[1,:] == Dkm_flame[2,:]) + + gas.transport_model = 'Multi' + assert flame.transport_model == 'Multi' + class TestFreeFlame(utilities.CanteraTest): tol_ss = [1.0e-5, 1.0e-14] # [rtol atol] for steady-state problem