Add callbacks for synchronizing with C++ Solution members

Objects that need to hold pointers to the members of a C++ Solution,
like the Python Solution object or StFlow, can register a callback
with the Solution that will be called if any of the thermo/kinetics/transport
objects change.

Fixes #1409
This commit is contained in:
Ray Speth 2023-02-26 22:58:27 -05:00 committed by Ingmar Schoegl
parent 6392724bd0
commit c54e429472
10 changed files with 102 additions and 20 deletions

View File

@ -108,6 +108,22 @@ public:
//! Returns a null pointer if the requested handle does not exist.
shared_ptr<ExternalHandle> 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<void()>& callback);
//! Remove the callback function associated with the specified object.
//! @since New in Cantera 3.0
void removeChangedCallback(void* id);
protected:
shared_ptr<ThermoPhase> m_thermo; //!< ThermoPhase manager
shared_ptr<Kinetics> 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<std::string, shared_ptr<ExternalHandle>> m_externalHandles;
//! Callback functions that are invoked when the therm, kinetics, or transport
//! members of the Solution are replaced.
map<void*, function<void()>> m_changeCallbacks;
};
//! Create and initialize a new Solution from an input file

View File

@ -63,6 +63,8 @@ public:
//! @param points initial number of grid points
StFlow(shared_ptr<Solution> 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<Transport> m_trans_shared;
// boundary emissivities for the radiation calculations
doublereal m_epsilon_left;
doublereal m_epsilon_right;

View File

@ -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)

View File

@ -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

View File

@ -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(<PyObject*>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(<PyObject*>(soln))
soln._base = cxx_soln
soln.base = cxx_soln.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(<PyObject*>soln,
pyOverride(<PyObject*>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:

View File

@ -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."""

View File

@ -42,6 +42,9 @@ void Solution::setName(const std::string& name) {
void Solution::setThermo(shared_ptr<ThermoPhase> thermo) {
m_thermo = thermo;
for (const auto& [id, callback] : m_changeCallbacks) {
callback();
}
}
void Solution::setKinetics(shared_ptr<Kinetics> kinetics) {
@ -49,10 +52,16 @@ void Solution::setKinetics(shared_ptr<Kinetics> kinetics) {
if (m_kinetics) {
m_kinetics->setRoot(shared_from_this());
}
for (const auto& [id, callback] : m_changeCallbacks) {
callback();
}
}
void Solution::setTransport(shared_ptr<Transport> 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<ExternalHandle> Solution::getExternalHandle(const std::string& name)
}
}
shared_ptr<Solution> newSolution(const std::string& infile,
const std::string& name,
const std::string& transport,
const std::vector<shared_ptr<Solution>>& adjacent)
void Solution::registerChangedCallback(void *id, const function<void()>& callback)
{
m_changeCallbacks[id] = callback;
}
void Solution::removeChangedCallback(void* id)
{
m_changeCallbacks.erase(id);
}
shared_ptr<Solution> newSolution(const std::string &infile,
const std::string &name,
const std::string &transport,
const std::vector<shared_ptr<Solution>> &adjacent)
{
// get file extension
size_t dot = infile.find_last_of(".");

View File

@ -64,8 +64,7 @@ IonFlow::IonFlow(shared_ptr<Solution> 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<Solution> 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){

View File

@ -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<Solution> 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<Solution> 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 {

View File

@ -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