diff --git a/doc/sphinx/reactors.rst b/doc/sphinx/reactors.rst index 0fa10f1446..1e63645fc7 100644 --- a/doc/sphinx/reactors.rst +++ b/doc/sphinx/reactors.rst @@ -382,6 +382,13 @@ by two methods: Internally, several ``step()`` calls are typically performed to reach the accurate state at time `t_{\rm new}`. +- ``advance_to_steady_state(max_steps, residual_threshold, atol, + write_residuals)`` [Python interface only]: If the steady state solution of a + reactor network is of interest, this method can be used. Internally, the + steady state is approached by time stepping. The network is considered to be + at steady state if the feature-scaled residual of the state vector is below a + given threshold value (which by default is 10 times the time step rtol). + The use of the ``advance`` method in a loop has the advantage that it produces results corresponding to a predefined time series. These are associated with a predefined memory consumption and well comparable between simulation runs with diff --git a/include/cantera/kinetics/ImplicitSurfChem.h b/include/cantera/kinetics/ImplicitSurfChem.h index 1447f64795..2b28f71ca0 100644 --- a/include/cantera/kinetics/ImplicitSurfChem.h +++ b/include/cantera/kinetics/ImplicitSurfChem.h @@ -134,6 +134,8 @@ class ImplicitSurfChem : public FuncEval //! Set the initial conditions for the solution vector /*! + * Essentially calls getState() + * * @param t0 Initial time * @param leny Length of the solution vector * @param y Value of the solution vector to be used. @@ -143,6 +145,14 @@ class ImplicitSurfChem : public FuncEval virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + //! Get the current state of the solution vector + /*! + * @param y Value of the solution vector to be used. + * On output, this contains the initial value + * of the solution. + */ + virtual void getState(doublereal* y); + /*! * Get the specifications for the problem from the values * in the ThermoPhase objects for all phases. diff --git a/include/cantera/zeroD/ConstPressureReactor.h b/include/cantera/zeroD/ConstPressureReactor.h index c5b5c86ec7..5582b4d37c 100644 --- a/include/cantera/zeroD/ConstPressureReactor.h +++ b/include/cantera/zeroD/ConstPressureReactor.h @@ -31,6 +31,7 @@ class ConstPressureReactor : public Reactor virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + virtual void getState(doublereal* y); virtual void initialize(doublereal t0 = 0.0); virtual void evalEqs(doublereal t, doublereal* y, diff --git a/include/cantera/zeroD/FlowReactor.h b/include/cantera/zeroD/FlowReactor.h index fa1f055376..3d059a2b77 100644 --- a/include/cantera/zeroD/FlowReactor.h +++ b/include/cantera/zeroD/FlowReactor.h @@ -26,6 +26,7 @@ class FlowReactor : public Reactor virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + virtual void getState(doublereal* y); virtual void initialize(doublereal t0 = 0.0); virtual void evalEqs(doublereal t, doublereal* y, diff --git a/include/cantera/zeroD/IdealGasConstPressureReactor.h b/include/cantera/zeroD/IdealGasConstPressureReactor.h index 2546b8f4bc..f69aceb7b1 100644 --- a/include/cantera/zeroD/IdealGasConstPressureReactor.h +++ b/include/cantera/zeroD/IdealGasConstPressureReactor.h @@ -33,6 +33,7 @@ class IdealGasConstPressureReactor : public ConstPressureReactor virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + virtual void getState(doublereal* y); virtual void initialize(doublereal t0 = 0.0); virtual void evalEqs(doublereal t, doublereal* y, diff --git a/include/cantera/zeroD/IdealGasReactor.h b/include/cantera/zeroD/IdealGasReactor.h index ada758cda9..9f2304ffdc 100644 --- a/include/cantera/zeroD/IdealGasReactor.h +++ b/include/cantera/zeroD/IdealGasReactor.h @@ -30,6 +30,7 @@ class IdealGasReactor : public Reactor virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + virtual void getState(doublereal* y); virtual void initialize(doublereal t0 = 0.0); diff --git a/include/cantera/zeroD/Reactor.h b/include/cantera/zeroD/Reactor.h index 7eb2c00589..124bb2e6f3 100644 --- a/include/cantera/zeroD/Reactor.h +++ b/include/cantera/zeroD/Reactor.h @@ -95,6 +95,8 @@ class Reactor : public ReactorBase //! Called by ReactorNet to get the initial conditions. /*! + * Essentially calls function getState() + * * @param[in] t0 Time at which initial conditions are determined * @param[in] leny Length of *y* (unused) * @param[out] y state vector representing the initial state of the reactor @@ -102,6 +104,12 @@ class Reactor : public ReactorBase virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + //! Get the the current state of the reactor. + /*! + * @param[out] y state vector representing the initial state of the reactor + */ + virtual void getState(doublereal* y); + virtual void initialize(doublereal t0 = 0.0); /*! diff --git a/include/cantera/zeroD/ReactorNet.h b/include/cantera/zeroD/ReactorNet.h index 011fad4899..b7616a8b88 100644 --- a/include/cantera/zeroD/ReactorNet.h +++ b/include/cantera/zeroD/ReactorNet.h @@ -195,6 +195,8 @@ class ReactorNet : public FuncEval doublereal* ydot, doublereal* p); virtual void getInitialConditions(doublereal t0, size_t leny, doublereal* y); + virtual void getState(doublereal* y); + virtual size_t nparams() { return m_ntotpar; } diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 69e709026b..404196be99 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -475,6 +475,8 @@ cdef extern from "cantera/zeroD/Reactor.h": void setEnergy(int) cbool energyEnabled() size_t componentIndex(string&) + size_t neq() + void getState(double*) void addSensitivityReaction(size_t) except + size_t nSensParams() @@ -550,6 +552,7 @@ cdef extern from "cantera/zeroD/ReactorNet.h": cbool verbose() void setVerbose(cbool) size_t neq() + void getState(double*) void setSensitivityTolerances(double, double) double rtolSensitivity() diff --git a/interfaces/cython/cantera/examples/reactors/mix1.py b/interfaces/cython/cantera/examples/reactors/mix1.py index 8e32dfacce..0d91df4225 100644 --- a/interfaces/cython/cantera/examples/reactors/mix1.py +++ b/interfaces/cython/cantera/examples/reactors/mix1.py @@ -59,17 +59,8 @@ sim = ct.ReactorNet([mixer]) # Since the mixer is a reactor, we need to integrate in time to reach steady -# state. A few residence times should be enough. -print('{0:>14s} {1:>14s} {2:>14s} {3:>14s} {4:>14s}'.format( - 't [s]', 'T [K]', 'h [J/kg]', 'P [Pa]', 'X_CH4')) - -t = 0.0 -for n in range(30): - tres = mixer.mass/(mfc1.mdot(t) + mfc2.mdot(t)) - t += 0.5*tres - sim.advance(t) - print('{0:14.5g} {1:14.5g} {2:14.5g} {3:14.5g} {4:14.5g}'.format( - t, mixer.T, mixer.thermo.h, mixer.thermo.P, mixer.thermo['CH4'].X[0])) +# state +sim.advance_to_steady_state() # view the state of the gas in the mixer print(mixer.thermo.report()) diff --git a/interfaces/cython/cantera/examples/reactors/pfr.py b/interfaces/cython/cantera/examples/reactors/pfr.py index 75d96fcdd8..1ec1100097 100644 --- a/interfaces/cython/cantera/examples/reactors/pfr.py +++ b/interfaces/cython/cantera/examples/reactors/pfr.py @@ -125,19 +125,8 @@ gas2.TDY = r2.thermo.TDY upstream.syncState() # integrate the reactor forward in time until steady state is reached - sim2.set_initial_time(0) # forces reinitialization - time = 0 - all_done = False - # determine steady state from H2 mole fraction - X_H2_previous = r2.thermo['H2'].X - while not all_done: - time += dt - sim2.advance(time) - if np.abs(r2.thermo['H2'].X - X_H2_previous) < 1.e-10: - # check whether surface coverages are in steady state. - all_done = True - else: - X_H2_previous = r2.thermo['H2'].X + sim2.reinitialize() + sim2.advance_to_steady_state() # compute velocity and transform into time u2[n] = mass_flow_rate2 / area / r2.thermo.density t_r2[n] = r2.mass / mass_flow_rate2 # residence time in this reactor diff --git a/interfaces/cython/cantera/examples/reactors/surf_pfr.py b/interfaces/cython/cantera/examples/reactors/surf_pfr.py index 26405f95f2..552c4204aa 100644 --- a/interfaces/cython/cantera/examples/reactors/surf_pfr.py +++ b/interfaces/cython/cantera/examples/reactors/surf_pfr.py @@ -110,33 +110,8 @@ # Set the state of the reservoir to match that of the previous reactor gas.TDY = r.thermo.TDY upstream.syncState() - - time = 0 - all_done = False - sim.set_initial_time(0) # forces reinitialization - while not all_done: - time += dt - sim.advance(time) - - if time > 10 * dt: - # check whether surface coverages are in steady state. This will be - # the case if the creation and destruction rates for a surface (but - # not gas) species are equal. - all_done = True - - # Note: netProduction = creation - destruction. By supplying the - # surface object as an argument, only the values for the surface - # species are returned by these methods - sdot = surf.get_net_production_rates(surf) - cdot = surf.get_creation_rates(surf) - ddot = surf.get_destruction_rates(surf) - - for ks in range(surf.n_species): - ratio = abs(sdot[ks]/(cdot[ks] + ddot[ks])) - if ratio > 1.0e-9: - all_done = False - break - + sim.reinitialize() + sim.advance_to_steady_state() dist = n * rlen * 1.0e3 # distance in mm if not n % 10: diff --git a/interfaces/cython/cantera/reactor.pyx b/interfaces/cython/cantera/reactor.pyx index 323254ab5e..24fb4e012b 100644 --- a/interfaces/cython/cantera/reactor.pyx +++ b/interfaces/cython/cantera/reactor.pyx @@ -235,6 +235,48 @@ cdef class Reactor(ReactorBase): raise IndexError('No such component: {!r}'.format(name)) return k + property n_vars: + """ + The number of state variables in the reactor. + Equal to: + + `Reactor` and `IdealGasReactor`: `n_species` + 3 (mass, volume, + internal energy or temperature). + + `ConstPressureReactor` and `IdealGasConstPressureReactor`: + `n_species` + 2 (mass, enthalpy or temperature). + """ + def __get__(self): + return self.reactor.neq() + + def get_state(self): + """ + Get the state vector of the reactor. + + The order of the variables (i.e. rows) is: + + `Reactor` or `IdealGasReactor`: + + - 0 - mass + - 1 - volume + - 2 - internal energy or temperature + - 3+ - mass fractions of the species + + `ConstPressureReactor` or `IdealGasConstPressureReactor`: + + - 0 - mass + - 1 - enthalpy or temperature + - 2+ - mass fractions of the species + + You can use the function `component_index` to determine the location + of a specific component + """ + if not self.n_vars: + raise Exception('Reactor empty or network not initialized.') + cdef np.ndarray[np.double_t, ndim=1] y = np.zeros(self.n_vars) + self.reactor.getState(&y[0]) + return y + cdef class Reservoir(ReactorBase): """ @@ -941,6 +983,80 @@ cdef class ReactorNet: def __get__(self): return self.net.neq() + def get_state(self): + """ + Get the combined state vector of the reactor network. + + The combined state vector consists of the concatenated state vectors of + all entities contained. + """ + if not self.n_vars: + raise Exception('ReactorNet empty or not initialized.') + cdef np.ndarray[np.double_t, ndim=1] y = np.zeros(self.n_vars) + self.net.getState(&y[0]) + return y + + def advance_to_steady_state(self, int max_steps=10000, + double residual_threshold=0., double atol=0., + pybool return_residuals=False): + r""" + Advance the reactor network in time until steady state is reached. + + The steady state is defined by requiring that the state of the system + only changes below a certain threshold. The residual is computed using + feature scaling: + + .. math:: r = \left| \frac{x(t + \Delta t) - x(t)}{\text{max}(x) + \text{atol}} \right| \cdot \frac{1}{n_x} + + :param max_steps: + Maximum number of steps to be taken + :param residual_threshold: + Threshold below which the feature-scaled residual r should drop such + that the network is defines as steady state. By default, + residual_threshold is 10 times the solver rtol. + :param atol: + The smallest expected value of interest. Used for feature scaling. + By default, this atol is identical to the solver atol. + :param return_residuals: + If set to `True`, this function returns the residual time series + as a vector with length `max_steps`. + + """ + # get default tolerances: + if not atol: + atol = self.rtol + if not residual_threshold: + residual_threshold = 10. * self.rtol + if residual_threshold <= self.rtol: + raise Exception('Residual threshold (' + str(residual_threshold) + + ') should be below solver rtol (' + + str(self.rtol) + ')') + if return_residuals: + residuals = np.empty(max_steps) + # check if system is initialized + if not self.n_vars: + self.reinitialize() + max_state_values = self.get_state() # denominator for feature scaling + for step in range(max_steps): + previous_state = self.get_state() + # take 10 steps (just to increase speed) + for n1 in range(10): + self.step() + state = self.get_state() + max_state_values = np.maximum(max_state_values, state) + # determine feature_scaled residual + residual = np.linalg.norm((state - previous_state) + / (max_state_values + atol)) / np.sqrt(self.n_vars) + if return_residuals: + residuals[step] = residual + if residual < residual_threshold: + break + if step == max_steps - 1: + raise Exception('Maximum number of steps reached before convergence' + ' below maximum residual') + if return_residuals: + return residuals[:step + 1] + def __reduce__(self): raise NotImplementedError('ReactorNet object is not picklable') diff --git a/interfaces/cython/cantera/test/test_reactor.py b/interfaces/cython/cantera/test/test_reactor.py index 4f3ebdbe20..61db7919bd 100644 --- a/interfaces/cython/cantera/test/test_reactor.py +++ b/interfaces/cython/cantera/test/test_reactor.py @@ -668,6 +668,16 @@ def test_ignition3(self): t,T = self.integrate(100.0) self.assertTrue(T[-1] < 910) # mixture did not ignite + def test_steady_state(self): + self.setup(900.0, 10*ct.one_atm, 1.0, 20.0) + residuals = self.net.advance_to_steady_state(return_residuals=True) + # test if steady state is reached + self.assertTrue(residuals[-1] < 10. * self.net.rtol) + # regression test; no external basis for these results + self.assertNear(self.combustor.T, 2486.14, 1e-5) + self.assertNear(self.combustor.thermo['H2O'].Y[0], 0.103804, 1e-5) + self.assertNear(self.combustor.thermo['HO2'].Y[0], 7.71296e-06, 1e-5) + class TestConstPressureReactor(utilities.CanteraTest): """ diff --git a/src/kinetics/ImplicitSurfChem.cpp b/src/kinetics/ImplicitSurfChem.cpp index ed1685af83..773a657e55 100644 --- a/src/kinetics/ImplicitSurfChem.cpp +++ b/src/kinetics/ImplicitSurfChem.cpp @@ -101,6 +101,11 @@ int ImplicitSurfChem::checkMatch(std::vector m_vec, ThermoPhase* t void ImplicitSurfChem::getInitialConditions(doublereal t0, size_t lenc, doublereal* c) +{ + getState(c); +} + +void ImplicitSurfChem::getState(doublereal* c) { size_t loc = 0; for (size_t n = 0; n < m_nsurf; n++) { diff --git a/src/zeroD/ConstPressureReactor.cpp b/src/zeroD/ConstPressureReactor.cpp index cd237114af..1e1438c0ac 100644 --- a/src/zeroD/ConstPressureReactor.cpp +++ b/src/zeroD/ConstPressureReactor.cpp @@ -13,10 +13,16 @@ using namespace std; namespace Cantera { -void ConstPressureReactor::getInitialConditions(double t0, size_t leny, double* y) +void ConstPressureReactor::getInitialConditions(double t0, size_t leny, + double* y) +{ + getState(y); +} + +void ConstPressureReactor::getState(double* y) { if (m_thermo == 0) { - throw CanteraError("getInitialConditions", + throw CanteraError("getState", "Error: reactor is empty."); } m_thermo->restoreState(m_state); diff --git a/src/zeroD/FlowReactor.cpp b/src/zeroD/FlowReactor.cpp index 69790fe506..5c68a0543e 100644 --- a/src/zeroD/FlowReactor.cpp +++ b/src/zeroD/FlowReactor.cpp @@ -25,10 +25,15 @@ FlowReactor::FlowReactor() : } void FlowReactor::getInitialConditions(double t0, size_t leny, double* y) +{ + getState(y); +} + +void FlowReactor::getState(double* y) { if (m_thermo == 0) { - writelog("Error: reactor is empty.\n"); - return; + throw CanteraError("getState", + "Error: reactor is empty."); } m_thermo->restoreState(m_state); m_thermo->getMassFractions(y+2); diff --git a/src/zeroD/IdealGasConstPressureReactor.cpp b/src/zeroD/IdealGasConstPressureReactor.cpp index 2454c4150e..4187e4316d 100644 --- a/src/zeroD/IdealGasConstPressureReactor.cpp +++ b/src/zeroD/IdealGasConstPressureReactor.cpp @@ -26,10 +26,15 @@ void IdealGasConstPressureReactor::setThermoMgr(ThermoPhase& thermo) void IdealGasConstPressureReactor::getInitialConditions(double t0, size_t leny, - double* y) + double* y) +{ + getState(y); +} + +void IdealGasConstPressureReactor::getState(double* y) { if (m_thermo == 0) { - throw CanteraError("getInitialConditions", + throw CanteraError("getState", "Error: reactor is empty."); } m_thermo->restoreState(m_state); diff --git a/src/zeroD/IdealGasReactor.cpp b/src/zeroD/IdealGasReactor.cpp index 88852372ba..12e9bbbb14 100644 --- a/src/zeroD/IdealGasReactor.cpp +++ b/src/zeroD/IdealGasReactor.cpp @@ -23,10 +23,15 @@ void IdealGasReactor::setThermoMgr(ThermoPhase& thermo) } void IdealGasReactor::getInitialConditions(double t0, size_t leny, double* y) +{ + getState(y); +} + +void IdealGasReactor::getState(double* y) { if (m_thermo == 0) { - cout << "Error: reactor is empty." << endl; - return; + throw CanteraError("getState", + "Error: reactor is empty."); } m_thermo->restoreState(m_state); diff --git a/src/zeroD/Reactor.cpp b/src/zeroD/Reactor.cpp index 711aa67092..1f2927afda 100644 --- a/src/zeroD/Reactor.cpp +++ b/src/zeroD/Reactor.cpp @@ -28,10 +28,15 @@ Reactor::Reactor() : {} void Reactor::getInitialConditions(double t0, size_t leny, double* y) +{ + getState(y); +} + +void Reactor::getState(double* y) { if (m_thermo == 0) { - cout << "Error: reactor is empty." << endl; - return; + throw CanteraError("getState", + "Error: reactor is empty."); } m_thermo->restoreState(m_state); diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index 9cfdc139dd..4d1854f8e2 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -175,12 +175,15 @@ void ReactorNet::updateState(doublereal* y) } } -void ReactorNet::getInitialConditions(doublereal t0, - size_t leny, doublereal* y) +void ReactorNet::getInitialConditions(double t0, size_t leny, double* y) +{ + getState(y); +} + +void ReactorNet::getState(double* y) { for (size_t n = 0; n < m_reactors.size(); n++) { - m_reactors[n]->getInitialConditions(t0, m_start[n+1]-m_start[n], - y + m_start[n]); + m_reactors[n]->getState(y + m_start[n]); } }