From 378342d0b0b2da6157f28cb9228334f7c26acc31 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 26 Feb 2021 16:34:38 -0500 Subject: [PATCH 1/9] Add initial steady state solver to zeroD --- include/cantera/numerics/NonLinSol.h | 101 +++++++++++++++++++++++++ include/cantera/zeroD/Reactor.h | 9 +++ include/cantera/zeroD/ReactorNet.h | 18 ++++- interfaces/cython/cantera/_cantera.pxd | 1 + interfaces/cython/cantera/reactor.pyx | 7 ++ src/zeroD/Reactor.cpp | 26 +++++++ src/zeroD/ReactorNet.cpp | 47 ++++++++++++ 7 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 include/cantera/numerics/NonLinSol.h diff --git a/include/cantera/numerics/NonLinSol.h b/include/cantera/numerics/NonLinSol.h new file mode 100644 index 0000000000..1175703050 --- /dev/null +++ b/include/cantera/numerics/NonLinSol.h @@ -0,0 +1,101 @@ +/** + * @file NonLinSol.h + * A nonlinear algebraic system solver, built upon Cantera's 1D multi-domain damped newton solver. + * + * NonLinSol is a simplified interface to the Cantera solver, designed for solving standard + * systems of nonlinear algebraic equations. Systems are solved by Cantera as single-domain, + * single-point problems. + */ + +#include "cantera/onedim.h" + +class NonLinSol : public Cantera::Domain1D +{ +public: +/* TO USE THIS SOLVER: + * 1. Include this header file in your code. + * #include "path/to/NonLinSol.h" + * 2. Create a NonLinSol child class, where you'll set up your problem. + * class YourClass : public NonLinSol + * 3. In the child class, provide implementations for problem-specific functions. + * void nonlinsol_residFunction(args) { ... } + * doublereal nonlinsol_initialValue(size_t i, size_t j) { ... } + * size_t nonlinsol_neq() { ... } + * 4. Initialize your class and call its inherited solve() function. + * YourClassObject.solve(); + * 5. (Optional) Reconfigure solver settings. + * YourClassObject.reconfigure(neq, lowerBound, upperBound, rtol, atol); + */ + +/// IMPLEMENT THESE FUNCTIONS: + + // Specify the residual function for the system + // sol - iteration solution vector (input) + // rsd - residual vector (output) + virtual void nonlinsol_residFunction(double *sol, double *rsd) = 0; + + // Specify guesses for the initial values. + // Note: called during Sim1D initialization + virtual doublereal nonlinsol_initialValue(size_t i) = 0; + + // Number of equations (state variables) for this reactor + virtual size_t nonlinsol_nEqs() = 0; + +/// CALLABLE FUNCTIONS: + +//TODO: need per component reconfigure here + void reconfigure(int neq, double lowerBound = -1.0e-3, double upperBound = 1.01, + double rtol = 1.0e-4, double atol = 1.0e-9) + { + Domain1D::resize(neq, 1); + for (int i = 0; i < neq; i++) + { + Domain1D::setBounds(i, lowerBound, upperBound); + Domain1D::setSteadyTolerances(rtol, atol, i); + Domain1D::setTransientTolerances(rtol, atol, i); + Domain1D::setComponentName(i, std::to_string(i)); + } + //TODO: per component reconfigure + Domain1D::setBounds(0, 0, 100); + Domain1D::setBounds(1, 0, 5); + Domain1D::setBounds(2, -10000000, 10000000); + } + + /** + * Solve the nonlinear algebraic system. + * @param loglevel controls amount of diagnostic output. + */ + void solve(int loglevel = 0) + { + if (Domain1D::nComponents() != nonlinsol_nEqs()) + reconfigure(nonlinsol_nEqs()); + + std::vector domains{this}; + Cantera::Sim1D(domains).solve(loglevel); + } + +private: +/// INTERNAL FUNCTIONS: + + // Implementing the residual function for the Cantera solver to use. Handles time integration, calls subclass residFunction for residuals. + void eval(size_t jg, double *sol, double *rsd, int *timeintMask, double rdt) + { + nonlinsol_residFunction(sol, rsd); // call subclass residFunction() implementation to update rsd + + if (rdt == 0) + return; // rdt is the reciprocal of the time step "dt"; rdt != 0 for time integration... + + // -------------------- TIME INTEGRATION -------------------------- + for (int i = 0; i < nonlinsol_nEqs(); i++) + { + rsd[i] -= rdt * (sol[i] - prevSoln(i, 0)); // backward euler method (result will be appropriately extracted from the residual) + timeintMask[i] = 1; // enable time stepping for this solution component (automatically resets each iteration) + } + } + + // Implementing the initial value function for the Cantera solver. Grid point j is always 0 (the only point in the single-point simulation), and thus unneeded + doublereal initialValue(size_t n, size_t j) + { + return nonlinsol_initialValue(n); + } +}; diff --git a/include/cantera/zeroD/Reactor.h b/include/cantera/zeroD/Reactor.h index ae81a7a241..c6af9eb99e 100644 --- a/include/cantera/zeroD/Reactor.h +++ b/include/cantera/zeroD/Reactor.h @@ -172,6 +172,15 @@ class Reactor : public ReactorBase //! @param limit value for step size limit void setAdvanceLimit(const std::string& nm, const double limit); + // Specify the residual function for the system + // sol - iteration solution vector (input) + // rsd - residual vector (output) + virtual void residFunction(double *sol, double *rsd); + + // Specify guesses for the initial values. + // Note: called during Sim1D initialization + virtual doublereal initialValue(size_t i); + //! Set reaction rate multipliers based on the sensitivity variables in //! *params*. virtual void applySensitivity(double* params); diff --git a/include/cantera/zeroD/ReactorNet.h b/include/cantera/zeroD/ReactorNet.h index 75e81daae5..4408e53760 100644 --- a/include/cantera/zeroD/ReactorNet.h +++ b/include/cantera/zeroD/ReactorNet.h @@ -8,6 +8,7 @@ #include "Reactor.h" #include "cantera/numerics/FuncEval.h" +#include "cantera/numerics/NonLinSol.h" namespace Cantera { @@ -23,7 +24,7 @@ class Integrator; * * @ingroup ZeroD */ -class ReactorNet : public FuncEval +class ReactorNet : public FuncEval, public NonLinSol { public: ReactorNet(); @@ -253,6 +254,21 @@ class ReactorNet : public FuncEval //! Retrieve absolute step size limits during advance bool getAdvanceLimits(double* limits); + //! Advance the reactor network to steady state. + double solveSteady(); + + // Specify the residual function for the system + // sol - iteration solution vector (input) + // rsd - residual vector (output) + virtual void nonlinsol_residFunction(double *sol, double *rsd); + + // Specify guesses for the initial values. + // Note: called during Sim1D initialization + virtual doublereal nonlinsol_initialValue(size_t i); + + // Number of equations (state variables) for this reactor + virtual size_t nonlinsol_nEqs(); + protected: //! Estimate a future state based on current derivatives. diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 4c80d496e6..c1e9ca9626 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -987,6 +987,7 @@ cdef extern from "cantera/zerodim.h" namespace "Cantera": double sensitivity(string&, size_t, int) except +translate_exception size_t nparams() string sensitivityParameterName(size_t) except +translate_exception + double solveSteady() except +translate_exception cdef extern from "cantera/zeroD/ReactorDelegator.h" namespace "Cantera": cdef cppclass CxxReactorAccessor "Cantera::ReactorAccessor": diff --git a/interfaces/cython/cantera/reactor.pyx b/interfaces/cython/cantera/reactor.pyx index a18a107992..00beba56e9 100644 --- a/interfaces/cython/cantera/reactor.pyx +++ b/interfaces/cython/cantera/reactor.pyx @@ -1463,6 +1463,13 @@ cdef class ReactorNet: if return_residuals: return residuals[:step + 1] + def solve_steady(self): + """ + Advance the reactor network to steady state via direct solution of + the ODE governing equations. + """ + return self.net.solveSteady() + def __reduce__(self): raise NotImplementedError('ReactorNet object is not picklable') diff --git a/src/zeroD/Reactor.cpp b/src/zeroD/Reactor.cpp index 6da06213b1..0e31b55ec0 100644 --- a/src/zeroD/Reactor.cpp +++ b/src/zeroD/Reactor.cpp @@ -499,4 +499,30 @@ void Reactor::setAdvanceLimit(const string& nm, const double limit) } } +// Specify the residual function for the system +// sol - iteration solution vector (input) +// rsd - residual vector (output) +void Reactor::residFunction(double *sol, double *rsd) +{ + evalEqs(0, sol, rsd, 0); // evaluate ODE system at y = sol. store result (ydot) in rsd + rsd[1] = 1.0 - sol[1]; // constant volume + //TODO: allow volume other than 1.0 +} + +// Specify guesses for the initial values. +// Note: called during Sim1D initialization +doublereal Reactor::initialValue(size_t i) { + m_thermo->restoreState(m_state); + switch (i) + { + case 0: + return m_thermo->density() * m_vol; + case 1: + return m_vol; + case 2: + return m_thermo->intEnergy_mass() * m_thermo->density() * m_vol; + } + return m_thermo->massFraction(i-3); +} + } diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index 16c183c26b..25d3e0a7fe 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -430,4 +430,51 @@ size_t ReactorNet::registerSensitivityParameter( return m_sens_params.size() - 1; } +// ----------- 0DSS ----------- +double ReactorNet::solveSteady() +{ + if (m_reactors.empty()) { + throw CanteraError("ReactorNet::solveSteady", + "no reactors in network!"); + } + + //initialize + m_nv = 0; + m_start.assign(1, 0); + for (size_t n = 0; n < m_reactors.size(); n++) { + Reactor& r = *m_reactors[n]; + if (r.nWalls()) { + throw CanteraError("ReactorNet::solveSteady", + "Wall detected in network - cannot solve for" + "steady state with transient components."); + } + //TODO: add check for constant mass flow rate + r.initialize(m_time); + m_nv += r.neq(); + m_start.push_back(m_nv); + } + //solve + NonLinSol::solve(); +} + +doublereal ReactorNet::nonlinsol_initialValue(size_t i) +{ + for (size_t n = m_reactors.size() - 1; n >= 0; n--) + if (i >= m_start[n]) + return m_reactors[n]->initialValue(i - m_start[n]); + return -1; +} + +void ReactorNet::nonlinsol_residFunction(double *sol, double *rsd) +{ + updateState(sol); + for (size_t n = 0; n < m_reactors.size(); n++) + m_reactors[n]->residFunction(sol + m_start[n], rsd + m_start[n]); +} + +size_t ReactorNet::nonlinsol_nEqs() +{ + return m_nv; +} + } From 083df6081cc33d09abc68cde49c28fa14c17633c Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 10 Mar 2021 17:10:22 -0500 Subject: [PATCH 2/9] Add FuncEval-based Newton solver for steadystate --- include/cantera/numerics/Jacobian.h | 100 ++++++++++ include/cantera/numerics/Newton.h | 100 ++++++++++ include/cantera/zeroD/ReactorNet.h | 15 +- src/numerics/Jacobian.cpp | 85 ++++++++ src/numerics/Newton.cpp | 290 ++++++++++++++++++++++++++++ src/zeroD/ReactorNet.cpp | 35 ++-- 6 files changed, 592 insertions(+), 33 deletions(-) create mode 100644 include/cantera/numerics/Jacobian.h create mode 100644 include/cantera/numerics/Newton.h create mode 100644 src/numerics/Jacobian.cpp create mode 100644 src/numerics/Newton.cpp diff --git a/include/cantera/numerics/Jacobian.h b/include/cantera/numerics/Jacobian.h new file mode 100644 index 0000000000..ed0603b1df --- /dev/null +++ b/include/cantera/numerics/Jacobian.h @@ -0,0 +1,100 @@ +//! @file MultiJac.h + +// 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. + +#ifndef CT_JACOBIAN_H +#define CT_JACOBIAN_H + +// #include "BandMatrix.h" +#include "DenseMatrix.h" +#include "FuncEval.h" + +namespace Cantera +{ + +/** + * Class MultiJac evaluates the Jacobian of a system of equations defined by a + * residual function supplied by an instance of class OneDim. The residual + * function may consist of several linked 1D domains, with different variables + * in each domain. + * @ingroup onedim + */ +class Jacobian : public DenseMatrix +// class Jacobian : public BandMatrix +{ +public: + Jacobian(FuncEval& func); + + /** + * Evaluate the Jacobian at x0. The unperturbed residual function is resid0, + * which must be supplied on input. The third parameter 'rdt' is the + * reciprocal of the time step. If zero, the steady-state Jacobian is + * evaluated. + */ + void eval(doublereal* x0, doublereal* resid0, double rdt); + + //wrapper for DenseMatrix solve + void compute(doublereal* step){ + solve(*this, step); + } + + //wrapper for DenseMatrix multiply + void mult(doublereal* b, doublereal* result){ + multiply(*this, b, result); + } + + //! Elapsed CPU time spent computing the Jacobian. + doublereal elapsedTime() const { + return m_elapsed; + } + + //! Number of Jacobian evaluations. + int nEvals() const { + return m_nevals; + } + + //! Number of times 'incrementAge' has been called since the last evaluation + int age() const { + return m_age; + } + + //! Increment the Jacobian age. + void incrementAge() { + m_age++; + } + + void updateTransient(doublereal rdt, integer* mask); + + //! Set the Jacobian age. + void setAge(int age) { + m_age = age; + } + + vector_int& transientMask() { + return m_mask; + } + + void incrementDiagonal(int j, doublereal d); + +protected: + //! Residual evaluator for this Jacobian + /*! + * This is a pointer to the residual evaluator. This object isn't owned by + * this Jacobian object. + */ + FuncEval* m_residfunc; + + vector_fp m_r1; + doublereal m_rtol, m_atol; + doublereal m_elapsed; + vector_fp m_ssdiag; + vector_int m_mask; + int m_nevals; + int m_age; + size_t m_size; + size_t m_points; +}; +} + +#endif diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h new file mode 100644 index 0000000000..14c0a5a36a --- /dev/null +++ b/include/cantera/numerics/Newton.h @@ -0,0 +1,100 @@ +//! @file Newton.h + +// 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. + +#ifndef CT_NEWTON_H +#define CT_NEWTON_H + +#include "Jacobian.h" +#include "FuncEval.h" + +namespace Cantera +{ + +/** + * A Newton solver. + */ +class Newton +{ +public: + Newton(FuncEval& func, Jacobian& jac); + virtual ~Newton() {}; + Newton(const Newton&) = delete; + Newton& operator=(const Newton&) = delete; + + size_t size() { + return m_nv; + } + + //! Compute the undamped Newton step. The residual function is evaluated + //! at `x`, but the Jacobian is not recomputed. + void step(doublereal* x, doublereal* step, int loglevel); + + /** + * Return the factor by which the undamped Newton step 'step0' + * must be multiplied in order to keep all solution components in + * all domains between their specified lower and upper bounds. + */ + doublereal boundStep(const doublereal* x0, const doublereal* step0, int loglevel); + + /** + * On entry, step0 must contain an undamped Newton step for the solution x0. + * This method attempts to find a damping coefficient such that the next + * undamped step would have a norm smaller than that of step0. If + * successful, the new solution after taking the damped step is returned in + * x1, and the undamped step at x1 is returned in step1. + */ + int dampStep(const doublereal* x0, const doublereal* step0, + doublereal* x1, doublereal* step1, doublereal& s1, + int loglevel, bool writetitle); + + //! Compute the weighted 2-norm of `step`. + doublereal weightedNorm(const doublereal* x, const doublereal* step) const; + + /** + * Find the solution to F(X) = 0 by damped Newton iteration. + */ + int solve(int loglevel=0); + + /// Set options. + void setOptions(int maxJacAge = 5) { + m_maxAge = maxJacAge; + } + + //TODO: implement get methods + //nice implementation for steady vs transient below + //! Relative tolerance of the nth component. + // doublereal rtol(size_t n) { + // return (m_rdt == 0.0 ? m_rtol_ss[n] : m_rtol_ts[n]); + // } + + //! Set upper and lower bounds on the nth component + void setBounds(size_t n, doublereal lower, doublereal upper) { + m_min[n] = lower; + m_max[n] = upper; + } + +protected: + doublereal m_rdt = 0.0; + + FuncEval* m_residfunc; + Jacobian* m_jac; + + //! Work arrays of size #m_nv used in solve(). + vector_fp m_x, m_x1, m_stp, m_stp1; + + vector_fp m_max, m_min; + vector_fp m_rtol_ss, m_rtol_ts; + vector_fp m_atol_ss, m_atol_ts; + + int m_maxAge; + + //! number of variables + size_t m_nv; + + doublereal m_elapsed; +}; +} + +#endif diff --git a/include/cantera/zeroD/ReactorNet.h b/include/cantera/zeroD/ReactorNet.h index 4408e53760..cdb0dc0fd0 100644 --- a/include/cantera/zeroD/ReactorNet.h +++ b/include/cantera/zeroD/ReactorNet.h @@ -8,7 +8,6 @@ #include "Reactor.h" #include "cantera/numerics/FuncEval.h" -#include "cantera/numerics/NonLinSol.h" namespace Cantera { @@ -24,7 +23,7 @@ class Integrator; * * @ingroup ZeroD */ -class ReactorNet : public FuncEval, public NonLinSol +class ReactorNet : public FuncEval { public: ReactorNet(); @@ -257,18 +256,6 @@ class ReactorNet : public FuncEval, public NonLinSol //! Advance the reactor network to steady state. double solveSteady(); - // Specify the residual function for the system - // sol - iteration solution vector (input) - // rsd - residual vector (output) - virtual void nonlinsol_residFunction(double *sol, double *rsd); - - // Specify guesses for the initial values. - // Note: called during Sim1D initialization - virtual doublereal nonlinsol_initialValue(size_t i); - - // Number of equations (state variables) for this reactor - virtual size_t nonlinsol_nEqs(); - protected: //! Estimate a future state based on current derivatives. diff --git a/src/numerics/Jacobian.cpp b/src/numerics/Jacobian.cpp new file mode 100644 index 0000000000..03b46e5e0d --- /dev/null +++ b/src/numerics/Jacobian.cpp @@ -0,0 +1,85 @@ +//! @file MultiJac.cpp Implementation file for class MultiJac + +// 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. + +#include "cantera/numerics/Jacobian.h" +#include "cantera/base/utilities.h" + +#include + +using namespace std; + +namespace Cantera +{ + +// MultiJac::MultiJac(OneDim& r) +Jacobian::Jacobian(FuncEval& func) + : DenseMatrix((&func)->neq(),(&func)->neq()) + // : BandMatrix((&func)->neq(), 2*(&func)->neq()-1,2*(&func)->neq()-1) +{ + m_residfunc = &func; + m_size = m_residfunc->neq(); + m_r1.resize(m_size); + m_ssdiag.resize(m_size); + m_mask.resize(m_size); + m_elapsed = 0.0; + m_nevals = 0; + m_age = 100000; + m_atol = sqrt(std::numeric_limits::epsilon()); + m_rtol = 1.0e-5; +} + +void Jacobian::updateTransient(doublereal rdt, integer* mask) +{ + for (size_t n = 0; n < m_size; n++) { + value(n,n) = m_ssdiag[n] - mask[n]*rdt; + } +} + +void Jacobian::incrementDiagonal(int j, doublereal d) +{ + m_ssdiag[j] += d; + value(j,j) = m_ssdiag[j]; +} + +void Jacobian::eval(doublereal* x0, doublereal* resid0, doublereal rdt) +{ + m_nevals++; + clock_t t0 = clock(); + //bfill(0.0); + + for (size_t n = 0; n < m_size; n++) { + // perturb x(n); preserve sign(x(n)) + double xsave = x0[n]; + double dx; + if (xsave >= 0) { + dx = xsave*m_rtol + m_atol; + } else { + dx = xsave*m_rtol - m_atol; + } + x0[n] = xsave + dx; + dx = x0[n] - xsave; + double rdx = 1.0/dx; + + // calculate perturbed residual + fill(m_r1.begin(), m_r1.end(), 0.0); + m_residfunc->eval(0, x0, m_r1.data(), 0); + + // compute nth column of Jacobian + for (size_t m = 0; m < m_size; m++) { + value(m,n) = (m_r1[m] - resid0[m])*rdx; + } + + x0[n] = xsave; + } + + for (size_t n = 0; n < m_size; n++) { + m_ssdiag[n] = value(n,n); + } + + m_elapsed += double(clock() - t0)/CLOCKS_PER_SEC; + m_age = 0; +} + +} // namespace diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp new file mode 100644 index 0000000000..f86f648037 --- /dev/null +++ b/src/numerics/Newton.cpp @@ -0,0 +1,290 @@ +//! @file Newton.cpp damped Newton solver + +// 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. + +#include "cantera/numerics/Newton.h" +#include "cantera/base/utilities.h" + +#include + +using namespace std; + +namespace Cantera +{ + +// constants +const doublereal DampFactor = sqrt(2.0); +const size_t NDAMP = 7; + +Newton::Newton(FuncEval& func, Jacobian& jac) + : m_maxAge(5) +{ + m_residfunc = &func; + m_jac = &jac; + m_nv = m_residfunc->neq(); + m_x.resize(m_nv); + m_x1.resize(m_nv); + m_stp.resize(m_nv); + m_stp1.resize(m_nv); + m_max.resize(m_nv, 0.0); + m_min.resize(m_nv, 0.0); + m_rtol_ss.resize(m_nv, 1.0e-4); + m_atol_ss.resize(m_nv, 1.0e-9); + m_rtol_ts.resize(m_nv, 1.0e-4); + m_atol_ts.resize(m_nv, 1.0e-11); + + m_elapsed = 0.0; +} + +doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) const +{ + double sum = 0.0; + for (size_t n = 0; n < m_nv; n++) { + double weight = m_rtol_ss[n]*fabs(x[n]) + m_atol_ss[n]; //TODO: add transient tolerances if rdt!=0 + double f = step[n]/weight; + sum += f*f; + } + return sqrt(sum/m_nv); +} + +void Newton::step(doublereal* x, doublereal* step, int loglevel) +{ + fill(step, step + m_nv, 0.0); + m_residfunc->eval(0, x, step, 0); + + //DenseMatrix overwrites itself with LU factored version on solve() calls. + //Temporary fix: recompute jac before every solve call + m_jac->eval(x, step, 0.0); + + for (size_t n = 0; n < m_nv; n++) { + step[n] = -step[n]; + } + + try { + m_jac->compute(step); + } catch (CanteraError&) { + // int iok = m_jac->info() - 1; + int iok = -1; //TODO: enable error info + if (iok >= 0) { + throw CanteraError("Newton::step", + "Jacobian is singular for component {} (Matrix row {})", + iok, iok); //TODO: add component name + } else { + throw; + } + } +} + +doublereal Newton::boundStep(const doublereal* x, const doublereal* step, int loglevel) +{ + doublereal boundFactor = 1.0; + bool wroteTitle = false; + for (size_t n = 0; n < m_nv; n++) { + double upperBound = m_max[n]; + double lowerBound = m_min[n]; + double val = x[n]; + if (loglevel > 0 && (val > upperBound + 1.0e-12 || val < lowerBound - 1.0e-12)) { + writelog("\nERROR: solution out of bounds.\n"); + writelog("Component {}: {:10.3e} with bounds ({:10.3e}, {:10.3e})\n", + n, val, lowerBound, upperBound); + // writelog("domain {:d}: {:>20s}({:d}) = {:10.3e} ({:10.3e}, {:10.3e})\n", + // r.domainIndex(), r.componentName(m), j, val, below, above); + } + double newval = val + step[n]; + + if (newval > upperBound) { + boundFactor = std::max(0.0, std::min(boundFactor, (upperBound - val)/(newval - val))); + } else if (newval < lowerBound) { + boundFactor = std::min(boundFactor, (val - lowerBound)/(val - newval)); + } + if (loglevel > 1 && (newval > upperBound || newval < lowerBound)) { + if (!wroteTitle) { + writelog("\nNewton step takes solution out of bounds.\n\n"); + // writelog(" {:>12s} {:>12s} {:>4s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + // "domain","component","pt","value","step","min","max"); + wroteTitle = true; + } + writelog("Component {}: {:10.3e} with bounds ({:10.3e}, {:10.3e}), step = {:10.3e}\n", + n, val, lowerBound, upperBound, step[n]); + // writelog(" {:4d} {:>12s} {:4d} {:10.3e} {:10.3e} {:10.3e} {:10.3e}\n", + // r.domainIndex(), r.componentName(m), j, + // val, step[index(m,j)], below, above); + } + } + return boundFactor; +} + +int Newton::dampStep(const doublereal* x0, const doublereal* step0, + doublereal* x1, doublereal* step1, doublereal& s1, + int loglevel, bool writetitle) +{ + // write header + if (loglevel > 0 && writetitle) { + writelog("\n\nDamped Newton iteration:\n"); + writeline('-', 65, false); + + writelog("\n{} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s} {:>5s} {:>5s}\n", + "m","F_damp","F_bound","log10(ss)", + "log10(s0)","log10(s1)","N_jac","Age"); + writeline('-', 65); + } + + // compute the weighted norm of the undamped step size step0 + doublereal s0 = weightedNorm(x0, step0); + + // compute the multiplier to keep all components in bounds + doublereal boundFactor = boundStep(x0, step0, loglevel-1); + + // if bound factor is very small, then x0 is already close to the boundary and + // step0 points out of the allowed domain. In this case, the Newton + // algorithm fails, so return an error condition. + if (boundFactor < 1.e-10) { + debuglog("\nAt limits.\n", loglevel); + return -3; + } + + // ---------- Attempt damped step ---------- + + // damping coefficient starts at 1.0 + doublereal damp = 1.0; + size_t m; + for (m = 0; m < NDAMP; m++) { + double ff = boundFactor*damp; + + // step the solution by the damped step size + for (size_t j = 0; j < m_nv; j++) { + x1[j] = ff*step0[j] + x0[j]; + } + + // compute the next undamped step that would result if x1 is accepted + step(x1, step1, loglevel-1); + + // compute the weighted norm of step1 + s1 = weightedNorm(x1, step1); + + // write log information + if (loglevel > 0) { + doublereal ss = weightedNorm(x1,step1); + writelog("\n{:d} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:4d} {:d}/{:d}", + m, damp, boundFactor, log10(ss+SmallNumber), + log10(s0+SmallNumber), log10(s1+SmallNumber), + m_jac->nEvals(), m_jac->age(), m_maxAge); + } + + // if the norm of s1 is less than the norm of s0, then accept this + // damping coefficient. Also accept it if this step would result in a + // converged solution. Otherwise, decrease the damping coefficient and + // try again. + if (s1 < 1.0 || s1 < s0) { + break; + } + damp /= DampFactor; + } + + // If a damping coefficient was found, return 1 if the solution after + // stepping by the damped step would represent a converged solution, and + // return 0 otherwise. If no damping coefficient could be found, return -2. + if (m < NDAMP) { + if (s1 > 1.0) { + return 0; + } else { + return 1; + } + } else { + return -2; + } +} + +int Newton::solve(int loglevel) +{ + // clock_t t0 = clock(); + int m = 0; + bool forceNewJac = false; + doublereal s1=1.e30; + + m_residfunc->getState(m_x.data()); + + bool frst = true; + // doublereal rdt = r.rdt(); + int j0 = m_jac->nEvals(); + int nJacReeval = 0; + + while (true) { + // Check whether the Jacobian should be re-evaluated. + if (m_jac->age() > m_maxAge) { + if (loglevel > 0) { + writelog("\nMaximum Jacobian age reached ({})\n", m_maxAge); + } + forceNewJac = true; + } + + if (forceNewJac) { + fill(m_stp.begin(), m_stp.end(), 0.0); + m_residfunc->eval(0, &m_x[0], &m_stp[0], 0); + m_jac->eval(&m_x[0], &m_stp[0], 0.0); + // jac.updateTransient(rdt, r.transientMask().data()); + forceNewJac = false; + } + + // compute the undamped Newton step + step(&m_x[0], &m_stp[0], loglevel-1); + + // increment the Jacobian age + m_jac->incrementAge(); + + // damp the Newton step + m = dampStep(&m_x[0], &m_stp[0], &m_x1[0], &m_stp1[0], s1, loglevel-1, frst); + if (loglevel == 1 && m >= 0) { + if (frst) { + writelog("\n\n {:>10s} {:>10s} {:>5s}", + "log10(ss)","log10(s1)","N_jac"); + writelog("\n ------------------------------------"); + } + doublereal ss = weightedNorm(&m_x[0], &m_stp[0]); + writelog("\n {:10.4f} {:10.4f} {:d}", + log10(ss),log10(s1),m_jac->nEvals()); + } + frst = false; + + // Successful step, but not converged yet. Take the damped step, and try + // again. + if (m == 0) { + copy(m_x1.begin(), m_x1.end(), m_x.begin()); + } else if (m == 1) { + // convergence + // if (rdt == 0) { + // jac.setAge(0); // for efficient sensitivity analysis + // } + break; + } else if (m < 0) { + // If dampStep fails, first try a new Jacobian if an old one was + // being used. If it was a new Jacobian, then return -1 to signify + // failure. + if (m_jac->age() > 1) { + forceNewJac = true; + if (nJacReeval > 3) { + break; + } + nJacReeval++; + debuglog("\nRe-evaluating Jacobian, since no damping " + "coefficient\ncould be found with this Jacobian.\n", + loglevel); + } else { + break; + } + } + } + + if (m < 0) { + //TODO: add get solution method? vs copy into provided vector + //copy(m_x.begin(), m_x.end(), x1); + } + if (m > 0 && m_jac->nEvals() == j0) { + m = 100; + } + // m_elapsed += (clock() - t0)/(1.0*CLOCKS_PER_SEC); + return m; +} + +} // end namespace Cantera diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index 25d3e0a7fe..d347633917 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -9,6 +9,7 @@ #include "cantera/base/utilities.h" #include "cantera/base/Array.h" #include "cantera/numerics/Integrator.h" +#include "cantera/numerics/Newton.h" using namespace std; @@ -437,7 +438,7 @@ double ReactorNet::solveSteady() throw CanteraError("ReactorNet::solveSteady", "no reactors in network!"); } - + //initialize m_nv = 0; m_start.assign(1, 0); @@ -453,28 +454,24 @@ double ReactorNet::solveSteady() m_nv += r.neq(); m_start.push_back(m_nv); } + + //TODO: better initialization/configuration for this solver //solve - NonLinSol::solve(); -} + std::unique_ptr jac; + jac.reset(new Cantera::Jacobian(*this)); -doublereal ReactorNet::nonlinsol_initialValue(size_t i) -{ - for (size_t n = m_reactors.size() - 1; n >= 0; n--) - if (i >= m_start[n]) - return m_reactors[n]->initialValue(i - m_start[n]); - return -1; -} + std::unique_ptr m_newt; + m_newt.reset(new Cantera::Newton(*this, *jac)); -void ReactorNet::nonlinsol_residFunction(double *sol, double *rsd) -{ - updateState(sol); - for (size_t n = 0; n < m_reactors.size(); n++) - m_reactors[n]->residFunction(sol + m_start[n], rsd + m_start[n]); -} + for (int i = 0; i < m_nv; i++) + { + m_newt->setBounds(i, -1.0e-3, 1.01); + } + m_newt->setBounds(0, 0, 100); + m_newt->setBounds(1, 0, 5); + m_newt->setBounds(2, -10000000, 10000000); -size_t ReactorNet::nonlinsol_nEqs() -{ - return m_nv; + m_newt->solve(8); } } From 991ecf3a42820f52c1478ebf1efad1e124e3bebb Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 28 Mar 2021 18:43:51 -0400 Subject: [PATCH 3/9] Remove dependency on external "Jacobian" class --- include/cantera/numerics/Newton.h | 20 ++++---- src/numerics/Newton.cpp | 77 ++++++++++++++++++++++--------- src/zeroD/ReactorNet.cpp | 5 +- 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h index 14c0a5a36a..8bc3291cf6 100644 --- a/include/cantera/numerics/Newton.h +++ b/include/cantera/numerics/Newton.h @@ -6,8 +6,8 @@ #ifndef CT_NEWTON_H #define CT_NEWTON_H -#include "Jacobian.h" #include "FuncEval.h" +#include "DenseMatrix.h" namespace Cantera { @@ -18,7 +18,7 @@ namespace Cantera class Newton { public: - Newton(FuncEval& func, Jacobian& jac); + Newton(FuncEval& func); virtual ~Newton() {}; Newton(const Newton&) = delete; Newton& operator=(const Newton&) = delete; @@ -59,7 +59,7 @@ class Newton /// Set options. void setOptions(int maxJacAge = 5) { - m_maxAge = maxJacAge; + m_jacMaxAge = maxJacAge; } //TODO: implement get methods @@ -75,11 +75,15 @@ class Newton m_max[n] = upper; } -protected: - doublereal m_rdt = 0.0; + void evalJacobian(doublereal* x, doublereal* xdot); +protected: FuncEval* m_residfunc; - Jacobian* m_jac; + + DenseMatrix m_jacobian; + int m_jacAge, m_jacMaxAge; + doublereal m_jacRtol, m_jacAtol; + //! Work arrays of size #m_nv used in solve(). vector_fp m_x, m_x1, m_stp, m_stp1; @@ -88,12 +92,8 @@ class Newton vector_fp m_rtol_ss, m_rtol_ts; vector_fp m_atol_ss, m_atol_ts; - int m_maxAge; - //! number of variables size_t m_nv; - - doublereal m_elapsed; }; } diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index f86f648037..34e33d910c 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -17,11 +17,8 @@ namespace Cantera const doublereal DampFactor = sqrt(2.0); const size_t NDAMP = 7; -Newton::Newton(FuncEval& func, Jacobian& jac) - : m_maxAge(5) -{ +Newton::Newton(FuncEval& func) { m_residfunc = &func; - m_jac = &jac; m_nv = m_residfunc->neq(); m_x.resize(m_nv); m_x1.resize(m_nv); @@ -34,7 +31,46 @@ Newton::Newton(FuncEval& func, Jacobian& jac) m_rtol_ts.resize(m_nv, 1.0e-4); m_atol_ts.resize(m_nv, 1.0e-11); - m_elapsed = 0.0; + m_jacobian = DenseMatrix(m_nv, m_nv); + m_jacAge = 10000; + m_jacMaxAge = 5; + m_jacRtol = 1.0e-5; + m_jacAtol = sqrt(std::numeric_limits::epsilon()); +} + +void Newton::evalJacobian(doublereal* x, doublereal* xdot) { + + // // calculate unperturbed residual + // m_residfunc->eval(0, x, xdot, 0); + + for (size_t n = 0; n < m_nv; n++) { + double xsave = x[n]; + + // calculate the perturbation amount, preserving the sign of x[n] + double dx; + if (xsave >= 0) { + dx = xsave*m_jacRtol + m_jacAtol; + } else { + dx = xsave*m_jacRtol - m_jacAtol; + } + + // perturb the solution vector + x[n] = xsave + dx; + dx = x[n] - xsave; + + // calculate perturbed residual + vector_fp xdotPerturbed(m_nv); //make this member for speed? + m_residfunc->eval(0, x, xdotPerturbed.data(), 0); + + // compute nth column of Jacobian + for (size_t m = 0; m < m_nv; m++) { + m_jacobian.value(m,n) = (xdotPerturbed[m] - xdot[m])/dx; + } + // restore solution vector + x[n] = xsave; + } + + m_jacAge = 0; } doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) const @@ -50,19 +86,18 @@ doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) con void Newton::step(doublereal* x, doublereal* step, int loglevel) { - fill(step, step + m_nv, 0.0); m_residfunc->eval(0, x, step, 0); //DenseMatrix overwrites itself with LU factored version on solve() calls. //Temporary fix: recompute jac before every solve call - m_jac->eval(x, step, 0.0); + evalJacobian(x, step); for (size_t n = 0; n < m_nv; n++) { step[n] = -step[n]; } try { - m_jac->compute(step); + Cantera::solve(m_jacobian, step); } catch (CanteraError&) { // int iok = m_jac->info() - 1; int iok = -1; //TODO: enable error info @@ -166,10 +201,10 @@ int Newton::dampStep(const doublereal* x0, const doublereal* step0, // write log information if (loglevel > 0) { doublereal ss = weightedNorm(x1,step1); - writelog("\n{:d} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:4d} {:d}/{:d}", + writelog("\n{:d} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:d}/{:d}", m, damp, boundFactor, log10(ss+SmallNumber), log10(s0+SmallNumber), log10(s1+SmallNumber), - m_jac->nEvals(), m_jac->age(), m_maxAge); + m_jacAge, m_jacMaxAge); } // if the norm of s1 is less than the norm of s0, then accept this @@ -207,14 +242,13 @@ int Newton::solve(int loglevel) bool frst = true; // doublereal rdt = r.rdt(); - int j0 = m_jac->nEvals(); int nJacReeval = 0; while (true) { // Check whether the Jacobian should be re-evaluated. - if (m_jac->age() > m_maxAge) { + if (m_jacAge > m_jacMaxAge) { if (loglevel > 0) { - writelog("\nMaximum Jacobian age reached ({})\n", m_maxAge); + writelog("\nMaximum Jacobian age reached ({})\n", m_jacMaxAge); } forceNewJac = true; } @@ -222,7 +256,7 @@ int Newton::solve(int loglevel) if (forceNewJac) { fill(m_stp.begin(), m_stp.end(), 0.0); m_residfunc->eval(0, &m_x[0], &m_stp[0], 0); - m_jac->eval(&m_x[0], &m_stp[0], 0.0); + evalJacobian(&m_x[0], &m_stp[0]); // jac.updateTransient(rdt, r.transientMask().data()); forceNewJac = false; } @@ -231,7 +265,7 @@ int Newton::solve(int loglevel) step(&m_x[0], &m_stp[0], loglevel-1); // increment the Jacobian age - m_jac->incrementAge(); + m_jacAge++; // damp the Newton step m = dampStep(&m_x[0], &m_stp[0], &m_x1[0], &m_stp1[0], s1, loglevel-1, frst); @@ -242,8 +276,8 @@ int Newton::solve(int loglevel) writelog("\n ------------------------------------"); } doublereal ss = weightedNorm(&m_x[0], &m_stp[0]); - writelog("\n {:10.4f} {:10.4f} {:d}", - log10(ss),log10(s1),m_jac->nEvals()); + writelog("\n {:10.4f} {:10.4f}", + log10(ss),log10(s1)); } frst = false; @@ -261,7 +295,7 @@ int Newton::solve(int loglevel) // If dampStep fails, first try a new Jacobian if an old one was // being used. If it was a new Jacobian, then return -1 to signify // failure. - if (m_jac->age() > 1) { + if (m_jacAge > 1) { forceNewJac = true; if (nJacReeval > 3) { break; @@ -280,10 +314,9 @@ int Newton::solve(int loglevel) //TODO: add get solution method? vs copy into provided vector //copy(m_x.begin(), m_x.end(), x1); } - if (m > 0 && m_jac->nEvals() == j0) { - m = 100; - } - // m_elapsed += (clock() - t0)/(1.0*CLOCKS_PER_SEC); + // if (m > 0 && m_jac->nEvals() == j0) { + // m = 100; + // } return m; } diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index d347633917..c96ba7fe1e 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -457,11 +457,8 @@ double ReactorNet::solveSteady() //TODO: better initialization/configuration for this solver //solve - std::unique_ptr jac; - jac.reset(new Cantera::Jacobian(*this)); - std::unique_ptr m_newt; - m_newt.reset(new Cantera::Newton(*this, *jac)); + m_newt.reset(new Cantera::Newton(*this)); for (int i = 0; i < m_nv; i++) { From f8515ae80b544e5b210015398294d554c4880c83 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 28 Mar 2021 21:59:42 -0400 Subject: [PATCH 4/9] Add constant var capability, cleanup unneeded code --- include/cantera/numerics/Jacobian.h | 100 -------------------------- include/cantera/numerics/Newton.h | 7 ++ include/cantera/numerics/NonLinSol.h | 101 --------------------------- include/cantera/zeroD/Reactor.h | 9 --- src/numerics/Jacobian.cpp | 85 ---------------------- src/numerics/Newton.cpp | 62 ++++++++++------ src/zeroD/Reactor.cpp | 26 ------- src/zeroD/ReactorNet.cpp | 2 + 8 files changed, 50 insertions(+), 342 deletions(-) delete mode 100644 include/cantera/numerics/Jacobian.h delete mode 100644 include/cantera/numerics/NonLinSol.h delete mode 100644 src/numerics/Jacobian.cpp diff --git a/include/cantera/numerics/Jacobian.h b/include/cantera/numerics/Jacobian.h deleted file mode 100644 index ed0603b1df..0000000000 --- a/include/cantera/numerics/Jacobian.h +++ /dev/null @@ -1,100 +0,0 @@ -//! @file MultiJac.h - -// 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. - -#ifndef CT_JACOBIAN_H -#define CT_JACOBIAN_H - -// #include "BandMatrix.h" -#include "DenseMatrix.h" -#include "FuncEval.h" - -namespace Cantera -{ - -/** - * Class MultiJac evaluates the Jacobian of a system of equations defined by a - * residual function supplied by an instance of class OneDim. The residual - * function may consist of several linked 1D domains, with different variables - * in each domain. - * @ingroup onedim - */ -class Jacobian : public DenseMatrix -// class Jacobian : public BandMatrix -{ -public: - Jacobian(FuncEval& func); - - /** - * Evaluate the Jacobian at x0. The unperturbed residual function is resid0, - * which must be supplied on input. The third parameter 'rdt' is the - * reciprocal of the time step. If zero, the steady-state Jacobian is - * evaluated. - */ - void eval(doublereal* x0, doublereal* resid0, double rdt); - - //wrapper for DenseMatrix solve - void compute(doublereal* step){ - solve(*this, step); - } - - //wrapper for DenseMatrix multiply - void mult(doublereal* b, doublereal* result){ - multiply(*this, b, result); - } - - //! Elapsed CPU time spent computing the Jacobian. - doublereal elapsedTime() const { - return m_elapsed; - } - - //! Number of Jacobian evaluations. - int nEvals() const { - return m_nevals; - } - - //! Number of times 'incrementAge' has been called since the last evaluation - int age() const { - return m_age; - } - - //! Increment the Jacobian age. - void incrementAge() { - m_age++; - } - - void updateTransient(doublereal rdt, integer* mask); - - //! Set the Jacobian age. - void setAge(int age) { - m_age = age; - } - - vector_int& transientMask() { - return m_mask; - } - - void incrementDiagonal(int j, doublereal d); - -protected: - //! Residual evaluator for this Jacobian - /*! - * This is a pointer to the residual evaluator. This object isn't owned by - * this Jacobian object. - */ - FuncEval* m_residfunc; - - vector_fp m_r1; - doublereal m_rtol, m_atol; - doublereal m_elapsed; - vector_fp m_ssdiag; - vector_int m_mask; - int m_nevals; - int m_age; - size_t m_size; - size_t m_points; -}; -} - -#endif diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h index 8bc3291cf6..d68c23f60c 100644 --- a/include/cantera/numerics/Newton.h +++ b/include/cantera/numerics/Newton.h @@ -75,6 +75,10 @@ class Newton m_max[n] = upper; } + void setConstant(size_t component, bool constant) { + m_constant[component] = constant; + } + void evalJacobian(doublereal* x, doublereal* xdot); protected: @@ -94,6 +98,9 @@ class Newton //! number of variables size_t m_nv; + + //! constant variables + std::vector m_constant; }; } diff --git a/include/cantera/numerics/NonLinSol.h b/include/cantera/numerics/NonLinSol.h deleted file mode 100644 index 1175703050..0000000000 --- a/include/cantera/numerics/NonLinSol.h +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @file NonLinSol.h - * A nonlinear algebraic system solver, built upon Cantera's 1D multi-domain damped newton solver. - * - * NonLinSol is a simplified interface to the Cantera solver, designed for solving standard - * systems of nonlinear algebraic equations. Systems are solved by Cantera as single-domain, - * single-point problems. - */ - -#include "cantera/onedim.h" - -class NonLinSol : public Cantera::Domain1D -{ -public: -/* TO USE THIS SOLVER: - * 1. Include this header file in your code. - * #include "path/to/NonLinSol.h" - * 2. Create a NonLinSol child class, where you'll set up your problem. - * class YourClass : public NonLinSol - * 3. In the child class, provide implementations for problem-specific functions. - * void nonlinsol_residFunction(args) { ... } - * doublereal nonlinsol_initialValue(size_t i, size_t j) { ... } - * size_t nonlinsol_neq() { ... } - * 4. Initialize your class and call its inherited solve() function. - * YourClassObject.solve(); - * 5. (Optional) Reconfigure solver settings. - * YourClassObject.reconfigure(neq, lowerBound, upperBound, rtol, atol); - */ - -/// IMPLEMENT THESE FUNCTIONS: - - // Specify the residual function for the system - // sol - iteration solution vector (input) - // rsd - residual vector (output) - virtual void nonlinsol_residFunction(double *sol, double *rsd) = 0; - - // Specify guesses for the initial values. - // Note: called during Sim1D initialization - virtual doublereal nonlinsol_initialValue(size_t i) = 0; - - // Number of equations (state variables) for this reactor - virtual size_t nonlinsol_nEqs() = 0; - -/// CALLABLE FUNCTIONS: - -//TODO: need per component reconfigure here - void reconfigure(int neq, double lowerBound = -1.0e-3, double upperBound = 1.01, - double rtol = 1.0e-4, double atol = 1.0e-9) - { - Domain1D::resize(neq, 1); - for (int i = 0; i < neq; i++) - { - Domain1D::setBounds(i, lowerBound, upperBound); - Domain1D::setSteadyTolerances(rtol, atol, i); - Domain1D::setTransientTolerances(rtol, atol, i); - Domain1D::setComponentName(i, std::to_string(i)); - } - //TODO: per component reconfigure - Domain1D::setBounds(0, 0, 100); - Domain1D::setBounds(1, 0, 5); - Domain1D::setBounds(2, -10000000, 10000000); - } - - /** - * Solve the nonlinear algebraic system. - * @param loglevel controls amount of diagnostic output. - */ - void solve(int loglevel = 0) - { - if (Domain1D::nComponents() != nonlinsol_nEqs()) - reconfigure(nonlinsol_nEqs()); - - std::vector domains{this}; - Cantera::Sim1D(domains).solve(loglevel); - } - -private: -/// INTERNAL FUNCTIONS: - - // Implementing the residual function for the Cantera solver to use. Handles time integration, calls subclass residFunction for residuals. - void eval(size_t jg, double *sol, double *rsd, int *timeintMask, double rdt) - { - nonlinsol_residFunction(sol, rsd); // call subclass residFunction() implementation to update rsd - - if (rdt == 0) - return; // rdt is the reciprocal of the time step "dt"; rdt != 0 for time integration... - - // -------------------- TIME INTEGRATION -------------------------- - for (int i = 0; i < nonlinsol_nEqs(); i++) - { - rsd[i] -= rdt * (sol[i] - prevSoln(i, 0)); // backward euler method (result will be appropriately extracted from the residual) - timeintMask[i] = 1; // enable time stepping for this solution component (automatically resets each iteration) - } - } - - // Implementing the initial value function for the Cantera solver. Grid point j is always 0 (the only point in the single-point simulation), and thus unneeded - doublereal initialValue(size_t n, size_t j) - { - return nonlinsol_initialValue(n); - } -}; diff --git a/include/cantera/zeroD/Reactor.h b/include/cantera/zeroD/Reactor.h index c6af9eb99e..ae81a7a241 100644 --- a/include/cantera/zeroD/Reactor.h +++ b/include/cantera/zeroD/Reactor.h @@ -172,15 +172,6 @@ class Reactor : public ReactorBase //! @param limit value for step size limit void setAdvanceLimit(const std::string& nm, const double limit); - // Specify the residual function for the system - // sol - iteration solution vector (input) - // rsd - residual vector (output) - virtual void residFunction(double *sol, double *rsd); - - // Specify guesses for the initial values. - // Note: called during Sim1D initialization - virtual doublereal initialValue(size_t i); - //! Set reaction rate multipliers based on the sensitivity variables in //! *params*. virtual void applySensitivity(double* params); diff --git a/src/numerics/Jacobian.cpp b/src/numerics/Jacobian.cpp deleted file mode 100644 index 03b46e5e0d..0000000000 --- a/src/numerics/Jacobian.cpp +++ /dev/null @@ -1,85 +0,0 @@ -//! @file MultiJac.cpp Implementation file for class MultiJac - -// 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. - -#include "cantera/numerics/Jacobian.h" -#include "cantera/base/utilities.h" - -#include - -using namespace std; - -namespace Cantera -{ - -// MultiJac::MultiJac(OneDim& r) -Jacobian::Jacobian(FuncEval& func) - : DenseMatrix((&func)->neq(),(&func)->neq()) - // : BandMatrix((&func)->neq(), 2*(&func)->neq()-1,2*(&func)->neq()-1) -{ - m_residfunc = &func; - m_size = m_residfunc->neq(); - m_r1.resize(m_size); - m_ssdiag.resize(m_size); - m_mask.resize(m_size); - m_elapsed = 0.0; - m_nevals = 0; - m_age = 100000; - m_atol = sqrt(std::numeric_limits::epsilon()); - m_rtol = 1.0e-5; -} - -void Jacobian::updateTransient(doublereal rdt, integer* mask) -{ - for (size_t n = 0; n < m_size; n++) { - value(n,n) = m_ssdiag[n] - mask[n]*rdt; - } -} - -void Jacobian::incrementDiagonal(int j, doublereal d) -{ - m_ssdiag[j] += d; - value(j,j) = m_ssdiag[j]; -} - -void Jacobian::eval(doublereal* x0, doublereal* resid0, doublereal rdt) -{ - m_nevals++; - clock_t t0 = clock(); - //bfill(0.0); - - for (size_t n = 0; n < m_size; n++) { - // perturb x(n); preserve sign(x(n)) - double xsave = x0[n]; - double dx; - if (xsave >= 0) { - dx = xsave*m_rtol + m_atol; - } else { - dx = xsave*m_rtol - m_atol; - } - x0[n] = xsave + dx; - dx = x0[n] - xsave; - double rdx = 1.0/dx; - - // calculate perturbed residual - fill(m_r1.begin(), m_r1.end(), 0.0); - m_residfunc->eval(0, x0, m_r1.data(), 0); - - // compute nth column of Jacobian - for (size_t m = 0; m < m_size; m++) { - value(m,n) = (m_r1[m] - resid0[m])*rdx; - } - - x0[n] = xsave; - } - - for (size_t n = 0; n < m_size; n++) { - m_ssdiag[n] = value(n,n); - } - - m_elapsed += double(clock() - t0)/CLOCKS_PER_SEC; - m_age = 0; -} - -} // namespace diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index 34e33d910c..ba572e81b0 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -20,6 +20,7 @@ const size_t NDAMP = 7; Newton::Newton(FuncEval& func) { m_residfunc = &func; m_nv = m_residfunc->neq(); + m_constant.resize(m_nv, false); m_x.resize(m_nv); m_x1.resize(m_nv); m_stp.resize(m_nv); @@ -28,8 +29,8 @@ Newton::Newton(FuncEval& func) { m_min.resize(m_nv, 0.0); m_rtol_ss.resize(m_nv, 1.0e-4); m_atol_ss.resize(m_nv, 1.0e-9); - m_rtol_ts.resize(m_nv, 1.0e-4); - m_atol_ts.resize(m_nv, 1.0e-11); + // m_rtol_ts.resize(m_nv, 1.0e-4); + // m_atol_ts.resize(m_nv, 1.0e-11); m_jacobian = DenseMatrix(m_nv, m_nv); m_jacAge = 10000; @@ -44,32 +45,51 @@ void Newton::evalJacobian(doublereal* x, doublereal* xdot) { // m_residfunc->eval(0, x, xdot, 0); for (size_t n = 0; n < m_nv; n++) { - double xsave = x[n]; + // calculate the nth Jacobian column, unless component n is constant + if (!m_constant[n]) { + double xsave = x[n]; + + // calculate the perturbation amount, preserving the sign of x[n] + double dx; + if (xsave >= 0) { + dx = xsave*m_jacRtol + m_jacAtol; + } else { + dx = xsave*m_jacRtol - m_jacAtol; + } - // calculate the perturbation amount, preserving the sign of x[n] - double dx; - if (xsave >= 0) { - dx = xsave*m_jacRtol + m_jacAtol; - } else { - dx = xsave*m_jacRtol - m_jacAtol; - } + // perturb the solution vector + x[n] = xsave + dx; + dx = x[n] - xsave; - // perturb the solution vector - x[n] = xsave + dx; - dx = x[n] - xsave; + // calculate perturbed residual + vector_fp xdotPerturbed(m_nv); //make this member for speed? + m_residfunc->eval(0, x, xdotPerturbed.data(), 0); - // calculate perturbed residual - vector_fp xdotPerturbed(m_nv); //make this member for speed? - m_residfunc->eval(0, x, xdotPerturbed.data(), 0); + // compute nth column of Jacobian + for (size_t m = 0; m < m_nv; m++) { + m_jacobian.value(m,n) = (xdotPerturbed[m] - xdot[m])/dx; + } + // restore solution vector + x[n] = xsave; - // compute nth column of Jacobian - for (size_t m = 0; m < m_nv; m++) { - m_jacobian.value(m,n) = (xdotPerturbed[m] - xdot[m])/dx; + // for constant components: Jacobian column of 0's, with 1 on diagonal + // note: Jacobian row must also be 0's w/ 1 on diagonal + } else { + for (size_t m = 0; m < m_nv; m++) { + m_jacobian.value(m,n) = 0; + } + m_jacobian.value(n,n) = 1; } - // restore solution vector - x[n] = xsave; } + // writelog("\nnew jac:\n"); + // for (int i = 0; i < m_nv; i++) { + // for (int j = 0; j < m_nv; j++) { + // writelog("{:14.5} ", m_jacobian.value(i,j)); + // } + // writelog("\n"); + // } + m_jacAge = 0; } diff --git a/src/zeroD/Reactor.cpp b/src/zeroD/Reactor.cpp index 0e31b55ec0..6da06213b1 100644 --- a/src/zeroD/Reactor.cpp +++ b/src/zeroD/Reactor.cpp @@ -499,30 +499,4 @@ void Reactor::setAdvanceLimit(const string& nm, const double limit) } } -// Specify the residual function for the system -// sol - iteration solution vector (input) -// rsd - residual vector (output) -void Reactor::residFunction(double *sol, double *rsd) -{ - evalEqs(0, sol, rsd, 0); // evaluate ODE system at y = sol. store result (ydot) in rsd - rsd[1] = 1.0 - sol[1]; // constant volume - //TODO: allow volume other than 1.0 -} - -// Specify guesses for the initial values. -// Note: called during Sim1D initialization -doublereal Reactor::initialValue(size_t i) { - m_thermo->restoreState(m_state); - switch (i) - { - case 0: - return m_thermo->density() * m_vol; - case 1: - return m_vol; - case 2: - return m_thermo->intEnergy_mass() * m_thermo->density() * m_vol; - } - return m_thermo->massFraction(i-3); -} - } diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index c96ba7fe1e..e95653d8f6 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -468,6 +468,8 @@ double ReactorNet::solveSteady() m_newt->setBounds(1, 0, 5); m_newt->setBounds(2, -10000000, 10000000); + m_newt->setConstant(1, true); + m_newt->solve(8); } From 0de9ba223cdf643f7b592a191f7cf2a2204fec82 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 29 Mar 2021 12:44:02 -0400 Subject: [PATCH 5/9] Fix jacobian forced reeval issue to allow jac reuse --- src/numerics/Newton.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index ba572e81b0..b5eab8f7ec 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -1,4 +1,4 @@ -//! @file Newton.cpp damped Newton solver +//! @file Newton.cpp: A damped & bounded quasi-Newton solver // 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. @@ -42,7 +42,7 @@ Newton::Newton(FuncEval& func) { void Newton::evalJacobian(doublereal* x, doublereal* xdot) { // // calculate unperturbed residual - // m_residfunc->eval(0, x, xdot, 0); + m_residfunc->eval(0, x, xdot, 0); for (size_t n = 0; n < m_nv; n++) { // calculate the nth Jacobian column, unless component n is constant @@ -107,17 +107,18 @@ doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) con void Newton::step(doublereal* x, doublereal* step, int loglevel) { m_residfunc->eval(0, x, step, 0); - - //DenseMatrix overwrites itself with LU factored version on solve() calls. - //Temporary fix: recompute jac before every solve call - evalJacobian(x, step); - for (size_t n = 0; n < m_nv; n++) { step[n] = -step[n]; } + DenseMatrix solvejac = m_jacobian; try { - Cantera::solve(m_jacobian, step); + //Note: this function takes an unfactored jacobian, then finds its LU factorization before + // solving. Optimization is possible by saving the factored jacobian, since it can be reused. + // Also, the DenseMatrix provided here will be overwritten with the LU factored version, so + // a copy is passed instead in order to preserve the original for reuse. + Cantera::solve(solvejac, step); + } catch (CanteraError&) { // int iok = m_jac->info() - 1; int iok = -1; //TODO: enable error info @@ -253,7 +254,6 @@ int Newton::dampStep(const doublereal* x0, const doublereal* step0, int Newton::solve(int loglevel) { - // clock_t t0 = clock(); int m = 0; bool forceNewJac = false; doublereal s1=1.e30; @@ -261,7 +261,6 @@ int Newton::solve(int loglevel) m_residfunc->getState(m_x.data()); bool frst = true; - // doublereal rdt = r.rdt(); int nJacReeval = 0; while (true) { @@ -274,10 +273,7 @@ int Newton::solve(int loglevel) } if (forceNewJac) { - fill(m_stp.begin(), m_stp.end(), 0.0); - m_residfunc->eval(0, &m_x[0], &m_stp[0], 0); evalJacobian(&m_x[0], &m_stp[0]); - // jac.updateTransient(rdt, r.transientMask().data()); forceNewJac = false; } From 3326d313dd21019e9ebba3968166eb7e500c408d Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 23 Apr 2021 15:43:46 -0400 Subject: [PATCH 6/9] Initial timestep functionality and jac optimizations --- include/cantera/numerics/DenseMatrix.h | 2 + include/cantera/numerics/Newton.h | 34 ++++-- src/numerics/DenseMatrix.cpp | 77 +++++++++++++ src/numerics/Newton.cpp | 153 +++++++++++++++++-------- src/zeroD/ReactorNet.cpp | 12 +- 5 files changed, 219 insertions(+), 59 deletions(-) diff --git a/include/cantera/numerics/DenseMatrix.h b/include/cantera/numerics/DenseMatrix.h index 409041ce33..32afda839a 100644 --- a/include/cantera/numerics/DenseMatrix.h +++ b/include/cantera/numerics/DenseMatrix.h @@ -155,6 +155,8 @@ class DenseMatrix : public Array2D friend int invert(DenseMatrix& A, int nn); }; +int factor(DenseMatrix& A); +int solveFactored(DenseMatrix& A, double* b, size_t nrhs=1, size_t ldb=0); //! Solve Ax = b. Array b is overwritten on exit with x. /*! diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h index d68c23f60c..b827937070 100644 --- a/include/cantera/numerics/Newton.h +++ b/include/cantera/numerics/Newton.h @@ -52,6 +52,10 @@ class Newton //! Compute the weighted 2-norm of `step`. doublereal weightedNorm(const doublereal* x, const doublereal* step) const; + int hybridSolve(); + + int timestep(); + /** * Find the solution to F(X) = 0 by damped Newton iteration. */ @@ -75,32 +79,46 @@ class Newton m_max[n] = upper; } - void setConstant(size_t component, bool constant) { - m_constant[component] = constant; + void setConstants(vector_int constantComponents) { + m_constantComponents = constantComponents; } void evalJacobian(doublereal* x, doublereal* xdot); + void getSolution(double* x) { + for (size_t i = 0; i < m_nv; i++) { + x[i] = m_x[i]; + } + } + protected: FuncEval* m_residfunc; - DenseMatrix m_jacobian; + //! number of variables + size_t m_nv; + + //! solution converged if [weightedNorm(sol, step) < m_convergenceThreshold] + doublereal m_convergenceThreshold; + + DenseMatrix m_jacobian, m_jacFactored; int m_jacAge, m_jacMaxAge; doublereal m_jacRtol, m_jacAtol; - //! Work arrays of size #m_nv used in solve(). + //! work arrays of size #m_nv used in solve(). vector_fp m_x, m_x1, m_stp, m_stp1; vector_fp m_max, m_min; vector_fp m_rtol_ss, m_rtol_ts; vector_fp m_atol_ss, m_atol_ts; - //! number of variables - size_t m_nv; + vector_fp m_xlast, m_xsave; + + //! the indexes of any constant variables + vector_int m_constantComponents; - //! constant variables - std::vector m_constant; + //! current timestep reciprocal + doublereal m_rdt = 0; }; } diff --git a/src/numerics/DenseMatrix.cpp b/src/numerics/DenseMatrix.cpp index 2d5b014e01..e9d6783cec 100644 --- a/src/numerics/DenseMatrix.cpp +++ b/src/numerics/DenseMatrix.cpp @@ -140,6 +140,83 @@ vector_int& DenseMatrix::ipiv() return m_ipiv; } +int factor(DenseMatrix& A) +{ + int info = 0; + #if CT_USE_LAPACK + ct_dgetrf(A.nRows(), A.nColumns(), A.ptrColumn(0), + A.nRows(), &A.ipiv()[0], info); + if (info > 0) { + if (A.m_printLevel) { + writelogf("solve(DenseMatrix& A, double* b): DGETRF returned INFO = %d U(i,i) is exactly zero. The factorization has" + " been completed, but the factor U is exactly singular, and division by zero will occur if " + "it is used to solve a system of equations.\n", info); + } + if (!A.m_useReturnErrorCode) { + throw CanteraError("solve(DenseMatrix& A, double* b)", + "DGETRF returned INFO = {}. U(i,i) is exactly zero. The factorization has" + " been completed, but the factor U is exactly singular, and division by zero will occur if " + "it is used to solve a system of equations.", info); + } + return info; + } else if (info < 0) { + if (A.m_printLevel) { + writelogf("solve(DenseMatrix& A, double* b): DGETRF returned INFO = %d. The argument i has an illegal value\n", info); + } + + throw CanteraError("solve(DenseMatrix& A, double* b)", + "DGETRF returned INFO = {}. The argument i has an illegal value", info); + } + #else + MappedMatrix Am(&A(0,0), A.nRows(), A.nColumns()); + #ifdef NDEBUG + auto lu = Am.partialPivLu(); + #else + auto lu = Am.fullPivLu(); + if (lu.nonzeroPivots() < static_cast(A.nColumns())) { + throw CanteraError("solve(DenseMatrix& A, double* b)", + "Matrix appears to be rank-deficient: non-zero pivots = {}; columns = {}", + lu.nonzeroPivots(), A.nColumns()); + } + #endif + #endif + return info; +} + +int solveFactored(DenseMatrix& A, double* b, size_t nrhs, size_t ldb) +{ + if (A.nColumns() != A.nRows()) { + if (A.m_printLevel) { + writelogf("solve(DenseMatrix& A, double* b): Can only solve a square matrix\n"); + } + throw CanteraError("solve(DenseMatrix& A, double* b)", "Can only solve a square matrix"); + } + + int info = 0; + if (ldb == 0) { + ldb = A.nColumns(); + } + #if CT_USE_LAPACK + ct_dgetrs(ctlapack::NoTranspose, A.nRows(), nrhs, A.ptrColumn(0), + A.nRows(), &A.ipiv()[0], b, ldb, info); + if (info != 0) { + if (A.m_printLevel) { + writelog("solve(DenseMatrix& A, double* b): DGETRS returned INFO = {}\n", info); + } + if (info < 0 || !A.m_useReturnErrorCode) { + throw CanteraError("solve(DenseMatrix& A, double* b)", + "DGETRS returned INFO = {}", info); + } + } + #else + for (size_t i = 0; i < nrhs; i++) { + MappedVector bm(b + ldb*i, A.nColumns()); + bm = lu.solve(bm); + } + #endif + return info; +} + int solve(DenseMatrix& A, double* b, size_t nrhs, size_t ldb) { if (A.nColumns() != A.nRows()) { diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index b5eab8f7ec..cdbb5917f6 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -20,7 +20,7 @@ const size_t NDAMP = 7; Newton::Newton(FuncEval& func) { m_residfunc = &func; m_nv = m_residfunc->neq(); - m_constant.resize(m_nv, false); + m_convergenceThreshold = 1.0e-14; m_x.resize(m_nv); m_x1.resize(m_nv); m_stp.resize(m_nv); @@ -31,93 +31,102 @@ Newton::Newton(FuncEval& func) { m_atol_ss.resize(m_nv, 1.0e-9); // m_rtol_ts.resize(m_nv, 1.0e-4); // m_atol_ts.resize(m_nv, 1.0e-11); + m_xlast.resize(m_nv); + m_xsave.resize(m_nv); + + m_rdt = 0; m_jacobian = DenseMatrix(m_nv, m_nv); m_jacAge = 10000; - m_jacMaxAge = 5; - m_jacRtol = 1.0e-5; + m_jacMaxAge = 1; + m_jacRtol = 1.0e-15; m_jacAtol = sqrt(std::numeric_limits::epsilon()); } void Newton::evalJacobian(doublereal* x, doublereal* xdot) { - // // calculate unperturbed residual + // calculate unperturbed residual m_residfunc->eval(0, x, xdot, 0); for (size_t n = 0; n < m_nv; n++) { - // calculate the nth Jacobian column, unless component n is constant - if (!m_constant[n]) { - double xsave = x[n]; - - // calculate the perturbation amount, preserving the sign of x[n] - double dx; - if (xsave >= 0) { - dx = xsave*m_jacRtol + m_jacAtol; - } else { - dx = xsave*m_jacRtol - m_jacAtol; - } + // calculate the nth Jacobian column + double xsave = x[n]; - // perturb the solution vector - x[n] = xsave + dx; - dx = x[n] - xsave; + // calculate the perturbation amount, preserving the sign of x[n] + double dx; + if (xsave >= 0) { + dx = xsave*m_jacRtol + m_jacAtol; + } else { + dx = xsave*m_jacRtol - m_jacAtol; + } - // calculate perturbed residual - vector_fp xdotPerturbed(m_nv); //make this member for speed? - m_residfunc->eval(0, x, xdotPerturbed.data(), 0); + // perturb the solution vector + x[n] = xsave + dx; + dx = x[n] - xsave; - // compute nth column of Jacobian - for (size_t m = 0; m < m_nv; m++) { - m_jacobian.value(m,n) = (xdotPerturbed[m] - xdot[m])/dx; - } - // restore solution vector - x[n] = xsave; + // calculate perturbed residual + vector_fp xdotPerturbed(m_nv); //make this member for speed? + m_residfunc->eval(0, x, xdotPerturbed.data(), 0); - // for constant components: Jacobian column of 0's, with 1 on diagonal - // note: Jacobian row must also be 0's w/ 1 on diagonal - } else { - for (size_t m = 0; m < m_nv; m++) { - m_jacobian.value(m,n) = 0; + // compute nth column of Jacobian + for (size_t m = 0; m < m_nv; m++) { + m_jacobian.value(m,n) = (xdotPerturbed[m] - xdot[m])/dx; + } + // restore solution vector + x[n] = xsave; + + // for constant-valued components: 1 in diagonal position, all 0's in row and column + for (size_t i : m_constantComponents) { + for (size_t j = 0; j < m_nv; j++) { + m_jacobian.value(i,j) = 0; + m_jacobian.value(j,i) = 0; } - m_jacobian.value(n,n) = 1; + m_jacobian.value(i,i) = 1; } } // writelog("\nnew jac:\n"); // for (int i = 0; i < m_nv; i++) { + // writelog("ROW {} | ", i); // for (int j = 0; j < m_nv; j++) { - // writelog("{:14.5} ", m_jacobian.value(i,j)); + // writelog("{:.5} ", m_jacobian.value(i,j)); // } - // writelog("\n"); + // writelog("\n\n"); // } m_jacAge = 0; + + m_jacFactored = m_jacobian; + Cantera::factor(m_jacFactored); } doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) const { - double sum = 0.0; + double accum = 0.0; for (size_t n = 0; n < m_nv; n++) { - double weight = m_rtol_ss[n]*fabs(x[n]) + m_atol_ss[n]; //TODO: add transient tolerances if rdt!=0 - double f = step[n]/weight; - sum += f*f; + double f = step[n]/(x[n] + m_atol_ss[n]); + accum += f*f; } - return sqrt(sum/m_nv); + // writelog("test.... {}", m_constantComponents.size()); + // return sqrt(accum/(m_nv - m_constantComponents.size())); + return sqrt(accum/m_nv); } void Newton::step(doublereal* x, doublereal* step, int loglevel) { m_residfunc->eval(0, x, step, 0); for (size_t n = 0; n < m_nv; n++) { - step[n] = -step[n]; + step[n] = -step[n] + m_rdt*(x[n]-m_xlast[n]); } - DenseMatrix solvejac = m_jacobian; + DenseMatrix solvejac = m_jacFactored; try { //Note: this function takes an unfactored jacobian, then finds its LU factorization before // solving. Optimization is possible by saving the factored jacobian, since it can be reused. // Also, the DenseMatrix provided here will be overwritten with the LU factored version, so // a copy is passed instead in order to preserve the original for reuse. - Cantera::solve(solvejac, step); + // Cantera::solve(solvejac, step); + Cantera::solveFactored(solvejac, step); } catch (CanteraError&) { // int iok = m_jac->info() - 1; @@ -222,9 +231,9 @@ int Newton::dampStep(const doublereal* x0, const doublereal* step0, // write log information if (loglevel > 0) { doublereal ss = weightedNorm(x1,step1); - writelog("\n{:d} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:9.5f} {:d}/{:d}", - m, damp, boundFactor, log10(ss+SmallNumber), - log10(s0+SmallNumber), log10(s1+SmallNumber), + writelog("\n{:d} {:9.5f} {:9.5f} {:9.5e} {:9.5e} {:9.5e} {:d}/{:d}", + m, damp, boundFactor, ss+SmallNumber, + s0+SmallNumber, s1+SmallNumber, m_jacAge, m_jacMaxAge); } @@ -232,7 +241,7 @@ int Newton::dampStep(const doublereal* x0, const doublereal* step0, // damping coefficient. Also accept it if this step would result in a // converged solution. Otherwise, decrease the damping coefficient and // try again. - if (s1 < 1.0 || s1 < s0) { + if (s1 < m_convergenceThreshold || s1 < s0) { break; } damp /= DampFactor; @@ -242,7 +251,7 @@ int Newton::dampStep(const doublereal* x0, const doublereal* step0, // stepping by the damped step would represent a converged solution, and // return 0 otherwise. If no damping coefficient could be found, return -2. if (m < NDAMP) { - if (s1 > 1.0) { + if (s1 > m_convergenceThreshold) { return 0; } else { return 1; @@ -252,6 +261,44 @@ int Newton::dampStep(const doublereal* x0, const doublereal* step0, } } +int Newton::hybridSolve() { + int MAX = 100; + int newtonsolves = 0; + int timesteps = 0; + + m_residfunc->getState(m_x.data()); + copy(m_x.begin(), m_x.end(), m_xsave.begin()); + + for(int i = 0; i < MAX; i++) { + newtonsolves++; + if(solve() == 1) { + writelog("\nConverged in {} newton solves, {} timesteps.", newtonsolves, timesteps); + return 1; + } + copy(m_xsave.begin(), m_xsave.end(), m_x.begin()); + for(int j = 0; j < MAX; j++) { + timesteps++; + timestep(); + } + copy(m_x.begin(), m_x.end(), m_xsave.begin()); + } + writelog("Solver failure..."); + return 0; +} + +int Newton::timestep() { + m_rdt = 1.0/1.0e-5; + + // calculate time-integration Jacobian + // m_residfunc->getState(m_x.data()); + copy(m_x.begin(), m_x.end(), m_xlast.begin()); + evalJacobian(&m_x[0], &m_stp[0]); + for (size_t i = 0; i < m_nv; i++) { + m_jacobian.value(i,i) -= m_rdt; + } + return solve(); +} + int Newton::solve(int loglevel) { int m = 0; @@ -302,6 +349,16 @@ int Newton::solve(int loglevel) if (m == 0) { copy(m_x1.begin(), m_x1.end(), m_x.begin()); } else if (m == 1) { + // writelog("\nConverged. Newton steps: {}", steps); + // writelog("\nconverged!\n"); + // writelog("\nsolution components: "); + // for(int i = 0; i < m_nv; i++) { + // writelog("{:9.5e}, ", m_x[i]); + // } + // writelog("\nresidual components: "); + // for(int i = 0; i < m_nv; i++) { + // writelog("{:9.5e}, ", m_stp[i]); + // } // convergence // if (rdt == 0) { // jac.setAge(0); // for efficient sensitivity analysis diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index e95653d8f6..178ed0c988 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -464,13 +464,19 @@ double ReactorNet::solveSteady() { m_newt->setBounds(i, -1.0e-3, 1.01); } - m_newt->setBounds(0, 0, 100); + m_newt->setBounds(0, 1.0e-3, 100); m_newt->setBounds(1, 0, 5); m_newt->setBounds(2, -10000000, 10000000); - m_newt->setConstant(1, true); + m_newt->setConstants({1}); + // m_newt->setConstants({0,1,2,11,12}); + // m_newt->setConstant(0, true); + // m_newt->setConstant(1, true); + // m_newt->setConstant(2, true); + // m_newt->setConstant(11, true); + // m_newt->setConstant(12, true); - m_newt->solve(8); + return m_newt->hybridSolve(); } } From 11e2969397f5ad9ce3ad0544b4398b1a108e0478 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 3 May 2021 00:54:03 -0400 Subject: [PATCH 7/9] Solver rewrite --- include/cantera/numerics/Newton.h | 40 ++--- src/numerics/DenseMatrix.cpp | 11 ++ src/numerics/Newton.cpp | 281 ++++++++---------------------- 3 files changed, 102 insertions(+), 230 deletions(-) diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h index b827937070..4db2723b33 100644 --- a/include/cantera/numerics/Newton.h +++ b/include/cantera/numerics/Newton.h @@ -31,24 +31,6 @@ class Newton //! at `x`, but the Jacobian is not recomputed. void step(doublereal* x, doublereal* step, int loglevel); - /** - * Return the factor by which the undamped Newton step 'step0' - * must be multiplied in order to keep all solution components in - * all domains between their specified lower and upper bounds. - */ - doublereal boundStep(const doublereal* x0, const doublereal* step0, int loglevel); - - /** - * On entry, step0 must contain an undamped Newton step for the solution x0. - * This method attempts to find a damping coefficient such that the next - * undamped step would have a norm smaller than that of step0. If - * successful, the new solution after taking the damped step is returned in - * x1, and the undamped step at x1 is returned in step1. - */ - int dampStep(const doublereal* x0, const doublereal* step0, - doublereal* x1, doublereal* step1, doublereal& s1, - int loglevel, bool writetitle); - //! Compute the weighted 2-norm of `step`. doublereal weightedNorm(const doublereal* x, const doublereal* step) const; @@ -75,8 +57,8 @@ class Newton //! Set upper and lower bounds on the nth component void setBounds(size_t n, doublereal lower, doublereal upper) { - m_min[n] = lower; - m_max[n] = upper; + m_lower_bounds[n] = lower; + m_upper_bounds[n] = upper; } void setConstants(vector_int constantComponents) { @@ -98,17 +80,17 @@ class Newton size_t m_nv; //! solution converged if [weightedNorm(sol, step) < m_convergenceThreshold] - doublereal m_convergenceThreshold; + doublereal m_converge_tol; DenseMatrix m_jacobian, m_jacFactored; - int m_jacAge, m_jacMaxAge; + size_t m_jacAge, m_jacMaxAge; doublereal m_jacRtol, m_jacAtol; //! work arrays of size #m_nv used in solve(). vector_fp m_x, m_x1, m_stp, m_stp1; - vector_fp m_max, m_min; + vector_fp m_upper_bounds, m_lower_bounds; vector_fp m_rtol_ss, m_rtol_ts; vector_fp m_atol_ss, m_atol_ts; @@ -120,6 +102,18 @@ class Newton //! current timestep reciprocal doublereal m_rdt = 0; }; + +// //! Returns the weighted Root Mean Square Deviation given a vector of residuals and +// // vectors of the corresponding weights and absolute tolerances for each component. +// double weightedRMS(vector_fp residuals, vector_fp weights, vector_fp atols) { +// size_t n = residuals.size(); +// double square = 0.0; +// for (size_t i = 0; i < n; i++) { +// square += pow(residuals[i]/(weights[i] + atols[i]), 2); +// } +// return sqrt(square/n); +// } + } #endif diff --git a/src/numerics/DenseMatrix.cpp b/src/numerics/DenseMatrix.cpp index e9d6783cec..b453957fe3 100644 --- a/src/numerics/DenseMatrix.cpp +++ b/src/numerics/DenseMatrix.cpp @@ -209,6 +209,17 @@ int solveFactored(DenseMatrix& A, double* b, size_t nrhs, size_t ldb) } } #else + MappedMatrix Am(&A(0,0), A.nRows(), A.nColumns()); + #ifdef NDEBUG + auto lu = Am.partialPivLu(); + #else + auto lu = Am.fullPivLu(); + if (lu.nonzeroPivots() < static_cast(A.nColumns())) { + throw CanteraError("solve(DenseMatrix& A, double* b)", + "Matrix appears to be rank-deficient: non-zero pivots = {}; columns = {}", + lu.nonzeroPivots(), A.nColumns()); + } + #endif for (size_t i = 0; i < nrhs; i++) { MappedVector bm(b + ldb*i, A.nColumns()); bm = lu.solve(bm); diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index cdbb5917f6..7f24fcc7b3 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -14,19 +14,18 @@ namespace Cantera { // constants -const doublereal DampFactor = sqrt(2.0); -const size_t NDAMP = 7; +const double damp_factor = sqrt(2.0); +const double damp_min = 0.1; Newton::Newton(FuncEval& func) { m_residfunc = &func; m_nv = m_residfunc->neq(); - m_convergenceThreshold = 1.0e-14; m_x.resize(m_nv); m_x1.resize(m_nv); m_stp.resize(m_nv); m_stp1.resize(m_nv); - m_max.resize(m_nv, 0.0); - m_min.resize(m_nv, 0.0); + m_upper_bounds.resize(m_nv, 0.0); + m_lower_bounds.resize(m_nv, 0.0); m_rtol_ss.resize(m_nv, 1.0e-4); m_atol_ss.resize(m_nv, 1.0e-9); // m_rtol_ts.resize(m_nv, 1.0e-4); @@ -34,10 +33,11 @@ Newton::Newton(FuncEval& func) { m_xlast.resize(m_nv); m_xsave.resize(m_nv); + m_converge_tol = 1.0e-14; m_rdt = 0; m_jacobian = DenseMatrix(m_nv, m_nv); - m_jacAge = 10000; + m_jacAge = npos; m_jacMaxAge = 1; m_jacRtol = 1.0e-15; m_jacAtol = sqrt(std::numeric_limits::epsilon()); @@ -100,16 +100,14 @@ void Newton::evalJacobian(doublereal* x, doublereal* xdot) { Cantera::factor(m_jacFactored); } +// RMSD (weighted) doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) const { - double accum = 0.0; - for (size_t n = 0; n < m_nv; n++) { - double f = step[n]/(x[n] + m_atol_ss[n]); - accum += f*f; + double square = 0.0; + for (size_t i = 0; i < m_nv; i++) { + square += pow(step[i]/(x[i] + m_atol_ss[i]), 2); } - // writelog("test.... {}", m_constantComponents.size()); - // return sqrt(accum/(m_nv - m_constantComponents.size())); - return sqrt(accum/m_nv); + return sqrt(square/m_nv); } void Newton::step(doublereal* x, doublereal* step, int loglevel) @@ -141,126 +139,6 @@ void Newton::step(doublereal* x, doublereal* step, int loglevel) } } -doublereal Newton::boundStep(const doublereal* x, const doublereal* step, int loglevel) -{ - doublereal boundFactor = 1.0; - bool wroteTitle = false; - for (size_t n = 0; n < m_nv; n++) { - double upperBound = m_max[n]; - double lowerBound = m_min[n]; - double val = x[n]; - if (loglevel > 0 && (val > upperBound + 1.0e-12 || val < lowerBound - 1.0e-12)) { - writelog("\nERROR: solution out of bounds.\n"); - writelog("Component {}: {:10.3e} with bounds ({:10.3e}, {:10.3e})\n", - n, val, lowerBound, upperBound); - // writelog("domain {:d}: {:>20s}({:d}) = {:10.3e} ({:10.3e}, {:10.3e})\n", - // r.domainIndex(), r.componentName(m), j, val, below, above); - } - double newval = val + step[n]; - - if (newval > upperBound) { - boundFactor = std::max(0.0, std::min(boundFactor, (upperBound - val)/(newval - val))); - } else if (newval < lowerBound) { - boundFactor = std::min(boundFactor, (val - lowerBound)/(val - newval)); - } - if (loglevel > 1 && (newval > upperBound || newval < lowerBound)) { - if (!wroteTitle) { - writelog("\nNewton step takes solution out of bounds.\n\n"); - // writelog(" {:>12s} {:>12s} {:>4s} {:>10s} {:>10s} {:>10s} {:>10s}\n", - // "domain","component","pt","value","step","min","max"); - wroteTitle = true; - } - writelog("Component {}: {:10.3e} with bounds ({:10.3e}, {:10.3e}), step = {:10.3e}\n", - n, val, lowerBound, upperBound, step[n]); - // writelog(" {:4d} {:>12s} {:4d} {:10.3e} {:10.3e} {:10.3e} {:10.3e}\n", - // r.domainIndex(), r.componentName(m), j, - // val, step[index(m,j)], below, above); - } - } - return boundFactor; -} - -int Newton::dampStep(const doublereal* x0, const doublereal* step0, - doublereal* x1, doublereal* step1, doublereal& s1, - int loglevel, bool writetitle) -{ - // write header - if (loglevel > 0 && writetitle) { - writelog("\n\nDamped Newton iteration:\n"); - writeline('-', 65, false); - - writelog("\n{} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s} {:>5s} {:>5s}\n", - "m","F_damp","F_bound","log10(ss)", - "log10(s0)","log10(s1)","N_jac","Age"); - writeline('-', 65); - } - - // compute the weighted norm of the undamped step size step0 - doublereal s0 = weightedNorm(x0, step0); - - // compute the multiplier to keep all components in bounds - doublereal boundFactor = boundStep(x0, step0, loglevel-1); - - // if bound factor is very small, then x0 is already close to the boundary and - // step0 points out of the allowed domain. In this case, the Newton - // algorithm fails, so return an error condition. - if (boundFactor < 1.e-10) { - debuglog("\nAt limits.\n", loglevel); - return -3; - } - - // ---------- Attempt damped step ---------- - - // damping coefficient starts at 1.0 - doublereal damp = 1.0; - size_t m; - for (m = 0; m < NDAMP; m++) { - double ff = boundFactor*damp; - - // step the solution by the damped step size - for (size_t j = 0; j < m_nv; j++) { - x1[j] = ff*step0[j] + x0[j]; - } - - // compute the next undamped step that would result if x1 is accepted - step(x1, step1, loglevel-1); - - // compute the weighted norm of step1 - s1 = weightedNorm(x1, step1); - - // write log information - if (loglevel > 0) { - doublereal ss = weightedNorm(x1,step1); - writelog("\n{:d} {:9.5f} {:9.5f} {:9.5e} {:9.5e} {:9.5e} {:d}/{:d}", - m, damp, boundFactor, ss+SmallNumber, - s0+SmallNumber, s1+SmallNumber, - m_jacAge, m_jacMaxAge); - } - - // if the norm of s1 is less than the norm of s0, then accept this - // damping coefficient. Also accept it if this step would result in a - // converged solution. Otherwise, decrease the damping coefficient and - // try again. - if (s1 < m_convergenceThreshold || s1 < s0) { - break; - } - damp /= DampFactor; - } - - // If a damping coefficient was found, return 1 if the solution after - // stepping by the damped step would represent a converged solution, and - // return 0 otherwise. If no damping coefficient could be found, return -2. - if (m < NDAMP) { - if (s1 > m_convergenceThreshold) { - return 0; - } else { - return 1; - } - } else { - return -2; - } -} - int Newton::hybridSolve() { int MAX = 100; int newtonsolves = 0; @@ -270,24 +148,25 @@ int Newton::hybridSolve() { copy(m_x.begin(), m_x.end(), m_xsave.begin()); for(int i = 0; i < MAX; i++) { - newtonsolves++; - if(solve() == 1) { - writelog("\nConverged in {} newton solves, {} timesteps.", newtonsolves, timesteps); - return 1; - } copy(m_xsave.begin(), m_xsave.end(), m_x.begin()); for(int j = 0; j < MAX; j++) { timesteps++; timestep(); } copy(m_x.begin(), m_x.end(), m_xsave.begin()); + newtonsolves++; + m_rdt = 0; + if(solve() == 1) { + writelog("\nConverged in {} newton solves, {} timesteps.", newtonsolves, timesteps); + return 1; + } } writelog("Solver failure..."); return 0; } int Newton::timestep() { - m_rdt = 1.0/1.0e-5; + m_rdt = 1.0/(1.0e-5); // calculate time-integration Jacobian // m_residfunc->getState(m_x.data()); @@ -301,96 +180,84 @@ int Newton::timestep() { int Newton::solve(int loglevel) { - int m = 0; - bool forceNewJac = false; - doublereal s1=1.e30; - m_residfunc->getState(m_x.data()); - bool frst = true; - int nJacReeval = 0; - while (true) { // Check whether the Jacobian should be re-evaluated. if (m_jacAge > m_jacMaxAge) { - if (loglevel > 0) { - writelog("\nMaximum Jacobian age reached ({})\n", m_jacMaxAge); - } - forceNewJac = true; - } - - if (forceNewJac) { evalJacobian(&m_x[0], &m_stp[0]); - forceNewJac = false; } // compute the undamped Newton step step(&m_x[0], &m_stp[0], loglevel-1); - - // increment the Jacobian age m_jacAge++; - // damp the Newton step - m = dampStep(&m_x[0], &m_stp[0], &m_x1[0], &m_stp1[0], s1, loglevel-1, frst); - if (loglevel == 1 && m >= 0) { - if (frst) { - writelog("\n\n {:>10s} {:>10s} {:>5s}", - "log10(ss)","log10(s1)","N_jac"); - writelog("\n ------------------------------------"); + // compute the weighted norm of the undamped step size step0 + double step_rms = weightedNorm(&m_x[0], &m_stp[0]); + + // compute the multiplier to keep all components in bounds. + double bound_factor = 1.0; + for (size_t i = 0; i < m_nv; i++) { + double upper_bound = m_upper_bounds[i]; + double lower_bound = m_lower_bounds[i]; + double val = m_x[i]; + if (val > upper_bound + 1.0e-12 || val < lower_bound - 1.0e-12) { + throw CanteraError("Newton::dampStep", "solution out of bounds"); } - doublereal ss = weightedNorm(&m_x[0], &m_stp[0]); - writelog("\n {:10.4f} {:10.4f}", - log10(ss),log10(s1)); + double newval = val + m_stp[i]; + + if (newval > upper_bound) { + bound_factor = max(0.0, min(bound_factor, (upper_bound - val)/(newval - val))); + } else if (newval < lower_bound) { + bound_factor = min(bound_factor, (val - lower_bound)/(val - newval)); + } + } + // if bound factor is very small, then x0 is already close to the boundary and + // step0 points out of the allowed domain. In this case, the Newton + // algorithm fails, so return an error condition. + if (bound_factor < 1.e-10) { + throw CanteraError("Newton::dampStep", "solution at limits"); } - frst = false; - - // Successful step, but not converged yet. Take the damped step, and try - // again. - if (m == 0) { - copy(m_x1.begin(), m_x1.end(), m_x.begin()); - } else if (m == 1) { - // writelog("\nConverged. Newton steps: {}", steps); - // writelog("\nconverged!\n"); - // writelog("\nsolution components: "); - // for(int i = 0; i < m_nv; i++) { - // writelog("{:9.5e}, ", m_x[i]); - // } - // writelog("\nresidual components: "); - // for(int i = 0; i < m_nv; i++) { - // writelog("{:9.5e}, ", m_stp[i]); - // } - // convergence - // if (rdt == 0) { - // jac.setAge(0); // for efficient sensitivity analysis - // } - break; - } else if (m < 0) { + + int m = -1; + // damped step - attempt to find a damping coefficient such that the next + // undamped step would have a RMSD smaller than that of step0 + for (double damp = bound_factor; damp > damp_min; damp /= damp_factor) { + // step the solution by the damped step size + for (size_t j = 0; j < m_nv; j++) { + m_x1[j] = damp*m_stp[j] + m_x[j]; + } + // compute the next undamped step that would result if x1 is accepted + step(&m_x1[0], &m_stp1[0], loglevel-1); + + // compute the weighted norm of step1 + double nextstep_rms = weightedNorm(&m_x1[0], &m_stp1[0]); + + // converged solution criteria + if (nextstep_rms < m_converge_tol) { + return 1; + } + // Also accept it if this step would result in a + // converged solution - successful step, but not converged yet. + // Take the damped step and try again. + if (nextstep_rms < step_rms) { + m = 0; + copy(m_x1.begin(), m_x1.end(), m_x.begin()); + break; + } + } + + if (m < 0) { // If dampStep fails, first try a new Jacobian if an old one was // being used. If it was a new Jacobian, then return -1 to signify // failure. if (m_jacAge > 1) { - forceNewJac = true; - if (nJacReeval > 3) { - break; - } - nJacReeval++; - debuglog("\nRe-evaluating Jacobian, since no damping " - "coefficient\ncould be found with this Jacobian.\n", - loglevel); + m_jacAge = npos; } else { - break; + return m; } } } - - if (m < 0) { - //TODO: add get solution method? vs copy into provided vector - //copy(m_x.begin(), m_x.end(), x1); - } - // if (m > 0 && m_jac->nEvals() == j0) { - // m = 100; - // } - return m; } } // end namespace Cantera From 7075f5a764afc1369430cc5015150d5c978a50de Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 9 May 2021 22:37:42 -0400 Subject: [PATCH 8/9] Newton solver rewrite part 2 --- include/cantera/numerics/Newton.h | 14 +--- src/numerics/Newton.cpp | 111 ++++++++++++++---------------- 2 files changed, 53 insertions(+), 72 deletions(-) diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h index 4db2723b33..1ccaafd671 100644 --- a/include/cantera/numerics/Newton.h +++ b/include/cantera/numerics/Newton.h @@ -29,24 +29,14 @@ class Newton //! Compute the undamped Newton step. The residual function is evaluated //! at `x`, but the Jacobian is not recomputed. - void step(doublereal* x, doublereal* step, int loglevel); + void step(doublereal* x, doublereal* step); //! Compute the weighted 2-norm of `step`. doublereal weightedNorm(const doublereal* x, const doublereal* step) const; int hybridSolve(); - int timestep(); - - /** - * Find the solution to F(X) = 0 by damped Newton iteration. - */ - int solve(int loglevel=0); - - /// Set options. - void setOptions(int maxJacAge = 5) { - m_jacMaxAge = maxJacAge; - } + int solve(double* x, double dt=0); //TODO: implement get methods //nice implementation for steady vs transient below diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index 7f24fcc7b3..d64e19ce47 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -6,8 +6,6 @@ #include "cantera/numerics/Newton.h" #include "cantera/base/utilities.h" -#include - using namespace std; namespace Cantera @@ -38,7 +36,7 @@ Newton::Newton(FuncEval& func) { m_jacobian = DenseMatrix(m_nv, m_nv); m_jacAge = npos; - m_jacMaxAge = 1; + m_jacMaxAge = 5; m_jacRtol = 1.0e-15; m_jacAtol = sqrt(std::numeric_limits::epsilon()); } @@ -74,28 +72,25 @@ void Newton::evalJacobian(doublereal* x, doublereal* xdot) { } // restore solution vector x[n] = xsave; + } - // for constant-valued components: 1 in diagonal position, all 0's in row and column - for (size_t i : m_constantComponents) { - for (size_t j = 0; j < m_nv; j++) { - m_jacobian.value(i,j) = 0; - m_jacobian.value(j,i) = 0; - } - m_jacobian.value(i,i) = 1; + // for constant-valued components: 1 in diagonal position, all 0's in row and column + for (size_t i : m_constantComponents) { + for (size_t j = 0; j < m_nv; j++) { + m_jacobian.value(i,j) = 0; + m_jacobian.value(j,i) = 0; } + m_jacobian.value(i,i) = 1; } - // writelog("\nnew jac:\n"); - // for (int i = 0; i < m_nv; i++) { - // writelog("ROW {} | ", i); - // for (int j = 0; j < m_nv; j++) { - // writelog("{:.5} ", m_jacobian.value(i,j)); - // } - // writelog("\n\n"); - // } - - m_jacAge = 0; + // timestep components + if (m_rdt > 0) { + for (size_t i = 0; i < m_nv; i++) { + m_jacobian.value(i,i) -= m_rdt; + } + } + // factor and save jacobian, will be reused for faster step computation m_jacFactored = m_jacobian; Cantera::factor(m_jacFactored); } @@ -110,7 +105,7 @@ doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) con return sqrt(square/m_nv); } -void Newton::step(doublereal* x, doublereal* step, int loglevel) +void Newton::step(doublereal* x, doublereal* step) { m_residfunc->eval(0, x, step, 0); for (size_t n = 0; n < m_nv; n++) { @@ -119,13 +114,7 @@ void Newton::step(doublereal* x, doublereal* step, int loglevel) DenseMatrix solvejac = m_jacFactored; try { - //Note: this function takes an unfactored jacobian, then finds its LU factorization before - // solving. Optimization is possible by saving the factored jacobian, since it can be reused. - // Also, the DenseMatrix provided here will be overwritten with the LU factored version, so - // a copy is passed instead in order to preserve the original for reuse. - // Cantera::solve(solvejac, step); Cantera::solveFactored(solvejac, step); - } catch (CanteraError&) { // int iok = m_jac->info() - 1; int iok = -1; //TODO: enable error info @@ -140,60 +129,62 @@ void Newton::step(doublereal* x, doublereal* step, int loglevel) } int Newton::hybridSolve() { - int MAX = 100; + int MAX = 17; int newtonsolves = 0; int timesteps = 0; + // initial state m_residfunc->getState(m_x.data()); copy(m_x.begin(), m_x.end(), m_xsave.begin()); for(int i = 0; i < MAX; i++) { + newtonsolves++; + if(solve(m_x.data()) > 0) { + writelog("\nConverged in {} newton solves, {} timesteps.", newtonsolves, timesteps); + return 1; + } copy(m_xsave.begin(), m_xsave.end(), m_x.begin()); for(int j = 0; j < MAX; j++) { + if (solve(m_x.data(), 1.0e-5) < 0) { + writelog("\nTimestep failure after {} newton solves, {} timesteps.", newtonsolves, timesteps); + return -1; + } timesteps++; - timestep(); } copy(m_x.begin(), m_x.end(), m_xsave.begin()); - newtonsolves++; - m_rdt = 0; - if(solve() == 1) { - writelog("\nConverged in {} newton solves, {} timesteps.", newtonsolves, timesteps); - return 1; - } } - writelog("Solver failure..."); + writelog("Failure to converge in {} steps.", MAX); return 0; } -int Newton::timestep() { - m_rdt = 1.0/(1.0e-5); +//! input initial state using parameter `x` +// for time integration, input parameter `dt` != 0 +// call without `dt`for direct nonlinear solution +// solution state available in *x* on return +int Newton::solve(double* x, double dt) +{ + copy(&x[0], &x[m_nv], m_x.begin()); - // calculate time-integration Jacobian - // m_residfunc->getState(m_x.data()); - copy(m_x.begin(), m_x.end(), m_xlast.begin()); - evalJacobian(&m_x[0], &m_stp[0]); - for (size_t i = 0; i < m_nv; i++) { - m_jacobian.value(i,i) -= m_rdt; + if (dt) { + copy(m_x.begin(), m_x.end(), m_xlast.begin()); + m_rdt = 1/dt; + m_jacAge = npos; + } else { + m_rdt = 0; } - return solve(); -} - -int Newton::solve(int loglevel) -{ - m_residfunc->getState(m_x.data()); while (true) { + // Check whether the Jacobian should be re-evaluated. if (m_jacAge > m_jacMaxAge) { evalJacobian(&m_x[0], &m_stp[0]); + m_jacAge = 0; } - // compute the undamped Newton step - step(&m_x[0], &m_stp[0], loglevel-1); - m_jacAge++; - - // compute the weighted norm of the undamped step size step0 + //compute the undamped Newton step and save its weighted norm + step(&m_x[0], &m_stp[0]); double step_rms = weightedNorm(&m_x[0], &m_stp[0]); + m_jacAge++; // compute the multiplier to keep all components in bounds. double bound_factor = 1.0; @@ -216,6 +207,7 @@ int Newton::solve(int loglevel) // step0 points out of the allowed domain. In this case, the Newton // algorithm fails, so return an error condition. if (bound_factor < 1.e-10) { + return -1; throw CanteraError("Newton::dampStep", "solution at limits"); } @@ -227,14 +219,13 @@ int Newton::solve(int loglevel) for (size_t j = 0; j < m_nv; j++) { m_x1[j] = damp*m_stp[j] + m_x[j]; } - // compute the next undamped step that would result if x1 is accepted - step(&m_x1[0], &m_stp1[0], loglevel-1); - - // compute the weighted norm of step1 + // compute the next undamped step that would result if x1 is accepted, and save its weighted norm + step(&m_x1[0], &m_stp1[0]); double nextstep_rms = weightedNorm(&m_x1[0], &m_stp1[0]); // converged solution criteria if (nextstep_rms < m_converge_tol) { + copy(m_x1.begin(), m_x1.end(), &x[0]); return 1; } // Also accept it if this step would result in a @@ -254,7 +245,7 @@ int Newton::solve(int loglevel) if (m_jacAge > 1) { m_jacAge = npos; } else { - return m; + return -1; } } } From b1ad331a90c3d2d9fa8369aaf455422a01872ba4 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 10 May 2021 18:25:40 -0400 Subject: [PATCH 9/9] Add configurations, for alt settings btwn solver modes --- include/cantera/numerics/Newton.h | 25 +++++++----- src/numerics/Newton.cpp | 66 ++++++++++++++++++------------- src/zeroD/ReactorNet.cpp | 5 --- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/include/cantera/numerics/Newton.h b/include/cantera/numerics/Newton.h index 1ccaafd671..596b04a1d9 100644 --- a/include/cantera/numerics/Newton.h +++ b/include/cantera/numerics/Newton.h @@ -12,6 +12,16 @@ namespace Cantera { +struct Configuration { + vector_fp rtol; + vector_fp atol; + double convtol; + double dt; + size_t jac_maxage; + double jac_rtol; + double jac_atol; +}; + /** * A Newton solver. */ @@ -36,7 +46,7 @@ class Newton int hybridSolve(); - int solve(double* x, double dt=0); + int solve(double* x); //TODO: implement get methods //nice implementation for steady vs transient below @@ -69,20 +79,13 @@ class Newton //! number of variables size_t m_nv; - //! solution converged if [weightedNorm(sol, step) < m_convergenceThreshold] - doublereal m_converge_tol; - DenseMatrix m_jacobian, m_jacFactored; - size_t m_jacAge, m_jacMaxAge; - doublereal m_jacRtol, m_jacAtol; - + size_t m_jacAge; //! work arrays of size #m_nv used in solve(). vector_fp m_x, m_x1, m_stp, m_stp1; vector_fp m_upper_bounds, m_lower_bounds; - vector_fp m_rtol_ss, m_rtol_ts; - vector_fp m_atol_ss, m_atol_ts; vector_fp m_xlast, m_xsave; @@ -91,6 +94,10 @@ class Newton //! current timestep reciprocal doublereal m_rdt = 0; + + Configuration m_directsolve_config; + Configuration m_timestep_config; + Configuration* m_config; }; // //! Returns the weighted Root Mean Square Deviation given a vector of residuals and diff --git a/src/numerics/Newton.cpp b/src/numerics/Newton.cpp index d64e19ce47..bf0fd95f80 100644 --- a/src/numerics/Newton.cpp +++ b/src/numerics/Newton.cpp @@ -24,21 +24,30 @@ Newton::Newton(FuncEval& func) { m_stp1.resize(m_nv); m_upper_bounds.resize(m_nv, 0.0); m_lower_bounds.resize(m_nv, 0.0); - m_rtol_ss.resize(m_nv, 1.0e-4); - m_atol_ss.resize(m_nv, 1.0e-9); - // m_rtol_ts.resize(m_nv, 1.0e-4); - // m_atol_ts.resize(m_nv, 1.0e-11); + + m_directsolve_config.rtol.resize(m_nv, 1.0e-4); + m_directsolve_config.atol.resize(m_nv, 1.0e-9); + m_directsolve_config.convtol = 1.0e-14; + m_directsolve_config.dt = 0; + m_directsolve_config.jac_maxage = 5; + m_directsolve_config.jac_rtol = 1.0e-15; + m_directsolve_config.jac_atol = sqrt(std::numeric_limits::epsilon()); + + m_timestep_config.rtol.resize(m_nv, 1.0e-4); + m_timestep_config.atol.resize(m_nv, 1.0e-11); + m_timestep_config.convtol = 1.0e-14; + m_timestep_config.dt = 1.0e-5; + m_timestep_config.jac_maxage = 5; + m_timestep_config.jac_rtol = 1.0e-15; + m_timestep_config.jac_atol = sqrt(std::numeric_limits::epsilon()); + + m_config = &m_directsolve_config; + m_xlast.resize(m_nv); m_xsave.resize(m_nv); - m_converge_tol = 1.0e-14; - m_rdt = 0; - m_jacobian = DenseMatrix(m_nv, m_nv); m_jacAge = npos; - m_jacMaxAge = 5; - m_jacRtol = 1.0e-15; - m_jacAtol = sqrt(std::numeric_limits::epsilon()); } void Newton::evalJacobian(doublereal* x, doublereal* xdot) { @@ -53,9 +62,9 @@ void Newton::evalJacobian(doublereal* x, doublereal* xdot) { // calculate the perturbation amount, preserving the sign of x[n] double dx; if (xsave >= 0) { - dx = xsave*m_jacRtol + m_jacAtol; + dx = xsave*m_config->jac_rtol + m_config->jac_atol; } else { - dx = xsave*m_jacRtol - m_jacAtol; + dx = xsave*m_config->jac_rtol - m_config->jac_atol; } // perturb the solution vector @@ -74,6 +83,13 @@ void Newton::evalJacobian(doublereal* x, doublereal* xdot) { x[n] = xsave; } + // timestep components + if (m_rdt > 0) { + for (size_t i = 0; i < m_nv; i++) { + m_jacobian.value(i,i) -= m_rdt; + } + } + // for constant-valued components: 1 in diagonal position, all 0's in row and column for (size_t i : m_constantComponents) { for (size_t j = 0; j < m_nv; j++) { @@ -83,13 +99,6 @@ void Newton::evalJacobian(doublereal* x, doublereal* xdot) { m_jacobian.value(i,i) = 1; } - // timestep components - if (m_rdt > 0) { - for (size_t i = 0; i < m_nv; i++) { - m_jacobian.value(i,i) -= m_rdt; - } - } - // factor and save jacobian, will be reused for faster step computation m_jacFactored = m_jacobian; Cantera::factor(m_jacFactored); @@ -100,7 +109,7 @@ doublereal Newton::weightedNorm(const doublereal* x, const doublereal* step) con { double square = 0.0; for (size_t i = 0; i < m_nv; i++) { - square += pow(step[i]/(x[i] + m_atol_ss[i]), 2); + square += pow(step[i]/(x[i] + m_config->atol[i]), 2); } return sqrt(square/m_nv); } @@ -139,13 +148,15 @@ int Newton::hybridSolve() { for(int i = 0; i < MAX; i++) { newtonsolves++; + m_config = &m_directsolve_config; if(solve(m_x.data()) > 0) { writelog("\nConverged in {} newton solves, {} timesteps.", newtonsolves, timesteps); return 1; } + m_config = &m_timestep_config; copy(m_xsave.begin(), m_xsave.end(), m_x.begin()); for(int j = 0; j < MAX; j++) { - if (solve(m_x.data(), 1.0e-5) < 0) { + if (solve(m_x.data()) < 0) { writelog("\nTimestep failure after {} newton solves, {} timesteps.", newtonsolves, timesteps); return -1; } @@ -161,22 +172,21 @@ int Newton::hybridSolve() { // for time integration, input parameter `dt` != 0 // call without `dt`for direct nonlinear solution // solution state available in *x* on return -int Newton::solve(double* x, double dt) +int Newton::solve(double* x) { copy(&x[0], &x[m_nv], m_x.begin()); + m_jacAge = npos; - if (dt) { + if (m_config->dt) { copy(m_x.begin(), m_x.end(), m_xlast.begin()); - m_rdt = 1/dt; - m_jacAge = npos; + m_rdt = 1/m_config->dt; } else { m_rdt = 0; } while (true) { - // Check whether the Jacobian should be re-evaluated. - if (m_jacAge > m_jacMaxAge) { + if (m_jacAge > m_config->jac_maxage) { evalJacobian(&m_x[0], &m_stp[0]); m_jacAge = 0; } @@ -224,7 +234,7 @@ int Newton::solve(double* x, double dt) double nextstep_rms = weightedNorm(&m_x1[0], &m_stp1[0]); // converged solution criteria - if (nextstep_rms < m_converge_tol) { + if (nextstep_rms < m_config->convtol) { copy(m_x1.begin(), m_x1.end(), &x[0]); return 1; } diff --git a/src/zeroD/ReactorNet.cpp b/src/zeroD/ReactorNet.cpp index 178ed0c988..6d72852ecb 100644 --- a/src/zeroD/ReactorNet.cpp +++ b/src/zeroD/ReactorNet.cpp @@ -470,11 +470,6 @@ double ReactorNet::solveSteady() m_newt->setConstants({1}); // m_newt->setConstants({0,1,2,11,12}); - // m_newt->setConstant(0, true); - // m_newt->setConstant(1, true); - // m_newt->setConstant(2, true); - // m_newt->setConstant(11, true); - // m_newt->setConstant(12, true); return m_newt->hybridSolve(); }