adapt_rk.h Source File

CPP API: adapt_rk.h Source File
adapt_rk.h
Go to the documentation of this file.
1 /*
2 * Copyright (C) 2020-2026 MEmilio
3 *
4 * Authors: Martin J. Kuehn, Daniel Abele
5 *
6 * Contact: Martin J. Kuehn <Martin.Kuehn@DLR.de>
7 *
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20 #ifndef ADAPT_RK_H_
21 #define ADAPT_RK_H_
22 
24 
25 #include <cstdio>
26 #include <vector>
27 
28 namespace mio
29 {
30 
62 template <typename FP>
63 class Tableau
64 {
65 public:
66  std::vector<Eigen::VectorX<FP>> entries;
67 
72  {
73  entries.resize(5);
74  for (size_t i = 0; i < entries.size(); i++) {
75  entries[i].resize(i + 2);
76  }
77 
78  entries[0][0] = 0.25;
79  entries[0][1] = 0.25;
80  entries[1][0] = 3 / 8.0;
81  entries[1][1] = 3 / 32.0;
82  entries[1][2] = 9 / 32.0;
83  entries[2][0] = 12 / 13.0;
84  entries[2][1] = 1932 / 2197.0;
85  entries[2][2] = -7200 / 2197.0;
86  entries[2][3] = 7296 / 2197.0;
87  entries[3][0] = 1.0;
88  entries[3][1] = 439 / 216.0;
89  entries[3][2] = -8.0;
90  entries[3][3] = 3680 / 513.0;
91  entries[3][4] = -845 / 4104.0;
92  entries[4][0] = 0.5;
93  entries[4][1] = -8 / 27.0;
94  entries[4][2] = 2.0;
95  entries[4][3] = -3544 / 2565.0;
96  entries[4][4] = 1859 / 4104.0;
97  entries[4][5] = -11 / 40.0;
98  }
99 };
100 
101 /*
102  * Final row(s) of the Butcher tableau
103  *
104  * Repitition from above: Runge-Kutta-4 is:
105  * | 25/216 0 1408/2565 2197/1404 -1/5 0
106  * The higher order (5th) approximation only differs by the last line which is
107  * | 16/135 0 6656/12825 28561/56430 -9/50 2/55
108  *
109  */
110 template <typename FP>
112 {
113 public:
114  Eigen::VectorX<FP> entries_low;
115  Eigen::VectorX<FP> entries_high;
116 
121  {
122  entries_low.resize(6);
123  entries_high.resize(6);
124 
125  entries_low[0] = 25 / 216.0;
126  entries_low[1] = 0.0;
127  entries_low[2] = 1408 / 2565.0;
128  entries_low[3] = 2197 / 4104.0;
129  entries_low[4] = -0.2;
130  entries_low[5] = 0.0;
131  entries_high[0] = 16 / 135.0;
132  entries_high[1] = 0.0;
133  entries_high[2] = 6656 / 12825.0;
134  entries_high[3] = 28561 / 56430.0;
135  entries_high[4] = -9 / 50.0;
136  entries_high[5] = 2 / 55.0;
137  }
138 };
139 
145 template <typename FP>
147 {
148 public:
153  : OdeIntegratorCore<FP>(std::numeric_limits<FP>::min(), std::numeric_limits<FP>::max())
154  , m_abs_tol(1e-10)
155  , m_rel_tol(1e-5)
156  {
157  }
158 
166  RKIntegratorCore(const FP abs_tol, const FP rel_tol, const FP dt_min, const FP dt_max)
167  : OdeIntegratorCore<FP>(dt_min, dt_max)
168  , m_abs_tol(abs_tol)
169  , m_rel_tol(rel_tol)
170  {
171  }
172 
173  std::unique_ptr<OdeIntegratorCore<FP>> clone() const override
174  {
175  return std::make_unique<RKIntegratorCore>(*this);
176  }
177 
179  void set_abs_tolerance(FP tol)
180  {
181  m_abs_tol = tol;
182  }
183 
185  void set_rel_tolerance(FP tol)
186  {
187  m_rel_tol = tol;
188  }
189 
191  void set_dt_min(FP dt_min)
192  {
193  this->get_dt_min() = dt_min;
194  }
195 
197  void set_dt_max(FP dt_max)
198  {
199  this->get_dt_max() = dt_max;
200  }
201 
202  // Allow setting different RK tableau schemes
203  void set_tableaus(const Tableau<FP>& tab, const TableauFinal<FP>& final_tab)
204  {
205  m_tab = tab;
206  m_tab_final = final_tab;
207  m_kt_values.resize(Eigen::NoChange, m_tab_final.entries_low.size());
208  }
209 
217  bool step(const DerivFunction<FP>& f, Eigen::Ref<const Eigen::VectorX<FP>> yt, FP& t, FP& dt,
218  Eigen::Ref<Eigen::VectorX<FP>> ytp1) const override
219  {
220  using std::max;
221  using std::min;
222  using std::pow;
223 
224  assert(0 <= this->get_dt_min());
225  assert(this->get_dt_min() <= this->get_dt_max());
226 
227  if (dt < this->get_dt_min() || dt > this->get_dt_max()) {
228  mio::log_warning("IntegratorCore: Restricting given step size dt = {} to [{}, {}].", dt, this->get_dt_min(),
229  this->get_dt_max());
230  }
231  dt = min<FP>(dt, this->get_dt_max());
232 
233  FP t_eval; // shifted time for evaluating yt
234  FP dt_new; // updated dt
235 
236  bool converged = false; // carry for convergence criterion
237  bool dt_is_invalid = false;
238 
239  if (m_yt_eval.size() != yt.size()) {
240  m_yt_eval.resize(yt.size());
241  m_kt_values.resize(yt.size(), m_tab_final.entries_low.size());
242  }
243 
244  m_yt_eval = yt;
245 
246  while (!converged && !dt_is_invalid) {
247  if (dt < this->get_dt_min()) {
248  dt_is_invalid = true;
249  dt = this->get_dt_min();
250  }
251  // compute first column of kt, i.e. kt_0 for each y in yt_eval
252  f(m_yt_eval, t, m_kt_values.col(0));
253 
254  for (Eigen::Index i = 1; i < m_kt_values.cols(); i++) {
255  // we first compute k_n1 for each y_j, then k_n2 for each y_j, etc.
256  t_eval = t;
257  t_eval += m_tab.entries[i - 1][0] *
258  dt; // t_eval = t + c_i * h // note: line zero of Butcher tableau not stored in array
259  // use ytp1 as temporary storage for evaluating m_kt_values[i]
260  ytp1 = m_yt_eval;
261  for (Eigen::Index k = 1; k < m_tab.entries[i - 1].size(); k++) {
262  ytp1 += (dt * m_tab.entries[i - 1][k]) * m_kt_values.col(k - 1);
263  }
264  // get the derivatives, i.e., compute kt_i for all y in ytp1: kt_i = f(t_eval, ytp1_low)
265  f(ytp1, t_eval, m_kt_values.col(i));
266  }
267  // calculate low order estimate
268  ytp1 = m_yt_eval;
269  ytp1 += (dt * (m_kt_values * m_tab_final.entries_low));
270  // truncation error estimate: yt_low - yt_high = O(h^(p+1)) where p = order of convergence
271  m_error_estimate = dt * (m_kt_values * (m_tab_final.entries_high - m_tab_final.entries_low)).array().abs();
272  // calculate mixed tolerance
273  m_eps = m_abs_tol + ytp1.array().abs() * m_rel_tol;
274 
275  converged = (m_error_estimate <= m_eps).all(); // convergence criterion
276 
277  if (converged || dt_is_invalid) {
278  // if sufficiently exact, return ytp1, which currently contains the lower order approximation
279  // (higher order is not always higher accuracy)
280  t += dt; // this is the t where ytp1 belongs to
281  }
282  // else: repeat the calculation above (with updated dt)
283 
284  // compute new value for dt
285  // converged implies eps/error_estimate >= 1, so dt will be increased for the next step
286  // hence !converged implies 0 < eps/error_estimate < 1, strictly decreasing dt
287  dt_new = dt * pow((m_eps / m_error_estimate).minCoeff(), (1. / (m_tab_final.entries_low.size() - 1)));
288  // safety factor for more conservative step increases,
289  // and to avoid dt_new -> dt for step decreases when |error_estimate - eps| -> 0
290  dt_new *= 0.9;
291  // check if updated dt stays within desired bounds and update dt for next step
292  dt = min<FP>(dt_new, this->get_dt_max());
293  }
294  dt = max<FP>(dt, this->get_dt_min());
295  // return 'converged' in favor of '!dt_is_invalid', as these values only differ if step sizing failed,
296  // but the step with size dt_min was accepted.
297  return converged;
298  }
299 
300 protected:
304  mutable Eigen::Matrix<FP, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor> m_kt_values;
305  mutable Eigen::VectorX<FP> m_yt_eval;
306 
307 private:
308  mutable Eigen::Array<FP, Eigen::Dynamic, Eigen::Dynamic> m_eps,
309  m_error_estimate; // tolerance and estimate used for time step adaption
310 };
311 
312 } // namespace mio
313 
314 #endif // ADAPT_RK_H_
Interface class defining the integration step used in a SystemIntegrator.
Definition: integrator.h:48
FP & get_dt_min()
Access lower bound to the step size dt.
Definition: integrator.h:101
FP & get_dt_max()
Access upper bound to the step size dt.
Definition: integrator.h:117
Two scheme Runge-Kutta numerical integrator with adaptive step width.
Definition: adapt_rk.h:147
FP m_abs_tol
Definition: adapt_rk.h:303
Eigen::Array< FP, Eigen::Dynamic, Eigen::Dynamic > m_eps
Definition: adapt_rk.h:308
void set_rel_tolerance(FP tol)
Definition: adapt_rk.h:185
Eigen::VectorX< FP > m_yt_eval
Definition: adapt_rk.h:305
void set_dt_min(FP dt_min)
Definition: adapt_rk.h:191
Tableau< FP > m_tab
Definition: adapt_rk.h:301
std::unique_ptr< OdeIntegratorCore< FP > > clone() const override
Definition: adapt_rk.h:173
void set_dt_max(FP dt_max)
Definition: adapt_rk.h:197
RKIntegratorCore()
Setting up the integrator.
Definition: adapt_rk.h:152
Eigen::Matrix< FP, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor > m_kt_values
Definition: adapt_rk.h:304
void set_tableaus(const Tableau< FP > &tab, const TableauFinal< FP > &final_tab)
Definition: adapt_rk.h:203
FP m_rel_tol
Definition: adapt_rk.h:303
TableauFinal< FP > m_tab_final
Definition: adapt_rk.h:302
Eigen::Array< FP, Eigen::Dynamic, Eigen::Dynamic > m_error_estimate
Definition: adapt_rk.h:309
bool step(const DerivFunction< FP > &f, Eigen::Ref< const Eigen::VectorX< FP >> yt, FP &t, FP &dt, Eigen::Ref< Eigen::VectorX< FP >> ytp1) const override
Make a single integration step of a system of ODEs and adapt the step size.
Definition: adapt_rk.h:217
void set_abs_tolerance(FP tol)
Definition: adapt_rk.h:179
RKIntegratorCore(const FP abs_tol, const FP rel_tol, const FP dt_min, const FP dt_max)
Set up the integrator.
Definition: adapt_rk.h:166
Definition: adapt_rk.h:112
TableauFinal()
default is Runge-Kutta-Fehlberg4(5) tableau
Definition: adapt_rk.h:120
Eigen::VectorX< FP > entries_low
Definition: adapt_rk.h:114
Eigen::VectorX< FP > entries_high
Definition: adapt_rk.h:115
Two scheme Runge-Kutta numerical integrator with adaptive step width for ODE y'(t) = f(t,...
Definition: adapt_rk.h:64
Tableau()
default is Runge-Kutta-Fehlberg4(5) tableau
Definition: adapt_rk.h:71
std::vector< Eigen::VectorX< FP > > entries
Definition: adapt_rk.h:66
static min_max_return_type< ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 >, ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > >::type min(const ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > &a, const ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > &b)
Definition: ad.hpp:2599
ad::internal::binary_intermediate_aa< AD_TAPE_REAL, ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 >, ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 >, ad::operations::ad_pow_aa< AD_TAPE_REAL > > pow(const ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > &x1, const ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > &x2)
Definition: ad.hpp:1610
static min_max_return_type< ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 >, ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > >::type max(const ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > &a, const ad::internal::active_type< AD_TAPE_REAL, DATA_HANDLER_1 > &b)
Definition: ad.hpp:2596
A collection of classes to simplify handling of matrix shapes in meta programming.
Definition: models/abm/analyze_result.h:30
auto i
Definition: io.h:809
void log_warning(spdlog::string_view_t fmt, const Args &... args)
Definition: logging.h:112
auto max(const Eigen::MatrixBase< A > &a, B &&b)
coefficient wise maximum of two matrices.
Definition: eigen_util.h:171
std::function< void(Eigen::Ref< const Eigen::VectorX< FP > > y, FP t, Eigen::Ref< Eigen::VectorX< FP > > dydt)> DerivFunction
Function template to be integrated.
Definition: integrator.h:39
Definition: io.h:94