-
Notifications
You must be signed in to change notification settings - Fork 19
1038 odeintegrator can run indefinitely #1046
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2173956
bb413a3
b0cba3b
802550f
c1f3bdc
782d81c
9c73129
a46a18a
0716e48
59f4f1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -208,19 +208,22 @@ class RKIntegratorCore : public IntegratorCore<FP> | |
| * @param[in,out] t current time | ||
| * @param[in,out] dt current time step size h=dt | ||
| * @param[out] ytp1 approximated value y(t+1) | ||
| * @param[in] force_step_size Forces a step with exactly size dt. | ||
| */ | ||
| bool step(const DerivFunction<FP>& f, Eigen::Ref<Eigen::VectorXd const> yt, double& t, double& dt, | ||
| Eigen::Ref<Eigen::VectorXd> ytp1) const override | ||
| Eigen::Ref<Eigen::VectorXd> ytp1, const bool force_step_size = false) const override | ||
| { | ||
| assert(0 <= m_dt_min); | ||
| assert(m_dt_min <= m_dt_max); | ||
|
|
||
| if (dt < m_dt_min || dt > m_dt_max) { | ||
| mio::log_warning("IntegratorCore: Restricting given step size dt = {} to [{}, {}].", dt, m_dt_min, | ||
| m_dt_max); | ||
| } | ||
| if (!force_step_size) { | ||
| if (dt < m_dt_min || dt > m_dt_max) { | ||
| mio::log_warning("IntegratorCore: Restricting given step size dt = {} to [{}, {}].", dt, m_dt_min, | ||
| m_dt_max); | ||
| } | ||
|
|
||
| dt = std::min(dt, m_dt_max); | ||
| dt = std::min(dt, m_dt_max); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this line not part of the if statement? It will only change dt if dt>m_dt_max.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont we need
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I thought about it, and the code behavior is not affected by the placement of the min. Personally, I like it more outside such that the if only affects the log message, and no further code behavior. I can also move the min down above the while loop so it is closer to the "max", i.e. the if statement right at the beginning of the while. Maybe this also avoids confusion about the missing max/clamp? |
||
| } | ||
|
|
||
| double t_eval; // shifted time for evaluating yt | ||
| double dt_new; // updated dt | ||
|
|
@@ -236,7 +239,7 @@ class RKIntegratorCore : public IntegratorCore<FP> | |
| m_yt_eval = yt; | ||
|
|
||
| while (!converged && !dt_is_invalid) { | ||
| if (dt < m_dt_min) { | ||
| if (dt < m_dt_min && !force_step_size) { | ||
| dt_is_invalid = true; | ||
| dt = m_dt_min; | ||
| } | ||
|
|
@@ -264,7 +267,7 @@ class RKIntegratorCore : public IntegratorCore<FP> | |
| // calculate mixed tolerance | ||
| m_eps = m_abs_tol + ytp1.array().abs() * m_rel_tol; | ||
|
|
||
| converged = (m_error_estimate <= m_eps).all(); // convergence criterion | ||
| converged = (m_error_estimate <= m_eps).all() || force_step_size; // convergence criterion | ||
|
|
||
| if (converged || dt_is_invalid) { | ||
| // if sufficiently exact, return ytp1, which currently contains the lower order approximation | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,11 +60,14 @@ class IntegratorCore | |
| * @param[out] ytp1 Set to the approximated value of y at time t + dt. | ||
| * (If adaptive, this time may be smaller, but it is at least t + dt_min, at most t + dt_max. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be adjusted?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided against it to not duplicate information, but I think I could just add "If adaptive and force_step_size==false" or something similar to hint at the other parameter. |
||
| * Note that the increment on t may be different from the returned value of dt.) | ||
| * @return Always true for nonadaptive methods. | ||
| * @param[in] force_step_size This value may be ignored by non adaptive integrators. False by default. | ||
| * (If adaptive, setting this to true allows making integration steps with any dt, ignoring the restriction to | ||
| * [dt_min, dt_max]. This is meant to enable making a final integration step that is smaller than dt_min.) | ||
| * @return Always true for non adaptive methods. | ||
| * (If adaptive, returns whether the adaptive step sizing was successful.) | ||
| */ | ||
| virtual bool step(const DerivFunction<FP>& f, Eigen::Ref<const Vector<FP>> yt, FP& t, FP& dt, | ||
| Eigen::Ref<Vector<FP>> ytp1) const = 0; | ||
| Eigen::Ref<Vector<FP>> ytp1, const bool force_step_size = false) const = 0; | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -109,7 +112,7 @@ class OdeIntegrator | |
|
|
||
| results.reserve(results.get_num_time_points() + num_steps); | ||
|
|
||
| bool step_okay = true; | ||
| bool is_step_sizing_okay = true; | ||
|
|
||
| FP dt_copy; // used to check whether step sizing is adaptive | ||
| FP dt_restore = 0; // used to restore dt if dt was decreased to reach tmax | ||
|
|
@@ -127,19 +130,30 @@ class OdeIntegrator | |
| dt_copy = dt; | ||
|
|
||
| results.add_time_point(); | ||
| step_okay &= m_core->step(f, results[i], t, dt, results[i + 1]); | ||
| results.get_last_time() = t; | ||
| bool step_okay = m_core->step(f, results[i], t, dt, results[i + 1]); | ||
|
|
||
| // if dt has been changed (even slighly) by step, register the current m_core as adaptive | ||
| m_is_adaptive |= !floating_point_equal(dt, dt_copy); | ||
|
|
||
| // catch case where dt_min of an adaptive stepper is larger than tmax - t | ||
| if (t > tmax) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a tolerance here? However, it would be hard to set since we have the template FP type
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do have floating_point compare functions, which work at least with those ad types, and can use both relative and absolute tolerances. To be clear, that tolerance is then used as "t >= tmax, and w.r.t. tolerances t != tmax" (see here) Do you think that tolerance is needed? Should we use the same tolerance as in the for loop? |
||
| log_info("Forcing last step size."); | ||
| // reset time and step size | ||
| dt = dt_copy; | ||
| t = tmax - dt_copy; | ||
| // recalculate the erroneous last step and overwrite its results | ||
| step_okay = m_core->step(f, results[i], t, dt, results[i + 1], true); | ||
| } | ||
| is_step_sizing_okay &= step_okay; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be enough to just use one bool.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With only one bool and a forced last step it is difficult to report what the state before the forced step was. I assume that if we discard the step that went beyond tmax, we also want to discard its "step_okay" value. And if that step was not okay (i.e. false), the &= will overwrite the previous state, making it unrecoverable. Do you agree with this assumption? |
||
| results.get_last_time() = t; | ||
| } | ||
| // if dt was decreased to reach tmax in the last time iteration, | ||
| // we restore it as it is now probably smaller than required for tolerances | ||
| dt = max(dt, dt_restore); | ||
|
|
||
| if (m_is_adaptive) { | ||
| if (!step_okay) { | ||
| log_warning("Adaptive step sizing failed. Forcing an integration step of size dt_min."); | ||
| if (!is_step_sizing_okay) { | ||
| log_warning("Adaptive step sizing failed. Forced at least one integration step of size dt_min."); | ||
| } | ||
| else if (fabs((tmax - t) / (tmax - t0)) > 1e-14) { | ||
| log_warning("Last time step too small. Could not reach tmax exactly."); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,8 @@ | |
| #include "boost/numeric/odeint/stepper/controlled_runge_kutta.hpp" | ||
| #include "boost/numeric/odeint/stepper/runge_kutta_fehlberg78.hpp" | ||
| #include "boost/numeric/odeint/stepper/runge_kutta_cash_karp54.hpp" | ||
|
|
||
| #include <limits> | ||
| // #include "boost/numeric/odeint/stepper/runge_kutta_dopri5.hpp" // TODO: reenable once boost bug is fixed | ||
|
|
||
| namespace mio | ||
|
|
@@ -70,24 +72,27 @@ class ControlledStepperWrapper : public mio::IntegratorCore<FP> | |
| } | ||
|
|
||
| /** | ||
| * @brief Make a single integration step on a system of ODEs and adapt the step size dt. | ||
|
|
||
| * @param[in] yt Value of y at t_{k}, y(t_{k}). | ||
| * @param[in,out] t Current time step t_{k} for some k. Will be set to t_{k+1} in [t_{k} + dt_min, t_{k} + dt]. | ||
| * @param[in,out] dt Current time step size h=dt. Overwritten by an estimated optimal step size for the next step. | ||
| * @param[out] ytp1 The approximated value of y(t_{k+1}). | ||
| */ | ||
| * @brief Make a single integration step on a system of ODEs and adapt the step size dt. | ||
| * | ||
| * @param[in] yt Value of y at t_{k}, y(t_{k}). | ||
| * @param[in,out] t Current time step t_{k} for some k. Will be set to t_{k+1} in [t_{k} + dt_min, t_{k} + dt]. | ||
| * @param[in,out] dt Current time step size h=dt. Overwritten by an estimated optimal step size for the next step. | ||
| * @param[out] ytp1 The approximated value of y(t_{k+1}). | ||
| * @param[in] force_step_size Forces a step with exactly size dt. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a little more detailed (as in the integrator)? |
||
| */ | ||
| bool step(const mio::DerivFunction<FP>& f, Eigen::Ref<Vector<FP> const> yt, FP& t, FP& dt, | ||
| Eigen::Ref<Vector<FP>> ytp1) const override | ||
| Eigen::Ref<Vector<FP>> ytp1, const bool force_step_size = false) const override | ||
| { | ||
| using boost::numeric::odeint::fail; | ||
| using std::max; | ||
| assert(0 <= m_dt_min); | ||
| assert(m_dt_min <= m_dt_max); | ||
|
|
||
| if (dt < m_dt_min || dt > m_dt_max) { | ||
| const FP dt_forced = dt; | ||
| if ((dt < m_dt_min || dt > m_dt_max) && !force_step_size) { | ||
| mio::log_warning("IntegratorCore: Restricting given step size dt = {} to [{}, {}].", dt, m_dt_min, | ||
| m_dt_max); | ||
| dt = std::min(dt, m_dt_max); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as in adapt_rk.h
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not use clamp here nor in adapt_rk as a small optimization, as the main loop of both integrators already check and bound to dt_min. Though the std::min should trip up some AD simulation, don't the tests cover simulations? |
||
| } | ||
| // set initial values for exit conditions | ||
| auto step_result = fail; | ||
|
|
@@ -98,14 +103,14 @@ class ControlledStepperWrapper : public mio::IntegratorCore<FP> | |
| // make a integration step, adapting dt to a possibly larger value on success, | ||
| // or a strictly smaller value on fail. | ||
| // stop only on a successful step or a failed step size adaption (w.r.t. the minimal step size m_dt_min) | ||
| while (step_result == fail && is_dt_valid) { | ||
| if (dt < m_dt_min) { | ||
| do { | ||
| if (dt < m_dt_min && !force_step_size) { | ||
| is_dt_valid = false; | ||
| dt = m_dt_min; | ||
| } | ||
| // we use the scheme try_step(sys, in, t, out, dt) with sys=f, in=y(t_{k}), out=y(t_{k+1}). | ||
| // this is similiar to do_step, but it can adapt the step size dt. If successful, it also updates t. | ||
|
|
||
| // note that the step_adjuster_type never allows or returns dt >= dt_max | ||
| if constexpr (!is_fsal_stepper) { // prevent compile time errors with fsal steppers | ||
| step_result = m_stepper.try_step( | ||
| // reorder arguments of the DerivFunction f for the stepper | ||
|
|
@@ -115,18 +120,18 @@ class ControlledStepperWrapper : public mio::IntegratorCore<FP> | |
| }, | ||
| m_yt, t, m_ytp1, dt); | ||
| } | ||
| } | ||
| } while (step_result == fail && is_dt_valid && !force_step_size); | ||
| // output the new value by copying it back to the reference | ||
| ytp1 = m_ytp1; | ||
| // bound dt from below | ||
| // the last adaptive step (successful or not) may have calculated a new step size smaller than m_dt_min | ||
|
|
||
| dt = max(dt, m_dt_min); | ||
| // clamp dt (again) | ||
| // the last adaptive step (successful or not) may have calculated a new step size smaller than m_dt_min, | ||
| // and when forcing step size, dt may have been larger than m_dt_max | ||
| dt = std::clamp(dt, m_dt_min, m_dt_max); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isnt this only necessary if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, as mentioned in the comment. Though it is now technically possible for a successful step without forcing to grow larger than dt_max, I will update that. I also cannot disable the adaptive step sizing (without some additional code or run time cost). |
||
| // check whether the last step failed (which means that m_dt_min was still too large to suffice tolerances) | ||
| if (step_result == fail) { | ||
| // adaptive stepping failed, but we still return the result of the last attempt | ||
| t += m_dt_min; | ||
| return false; | ||
| t += force_step_size ? dt_forced : dt; | ||
| return force_step_size; | ||
| } | ||
| else { | ||
| // successfully made an integration step | ||
|
|
@@ -157,8 +162,7 @@ class ControlledStepperWrapper : public mio::IntegratorCore<FP> | |
| /// @param dt_max sets the maximum step size | ||
| void set_dt_max(FP dt_max) | ||
| { | ||
| m_dt_max = dt_max; | ||
| m_stepper = create_stepper(); | ||
| m_dt_max = dt_max; | ||
| } | ||
|
|
||
| private: | ||
|
|
@@ -167,7 +171,7 @@ class ControlledStepperWrapper : public mio::IntegratorCore<FP> | |
| { | ||
| // for more options see: boost/boost/numeric/odeint/stepper/controlled_runge_kutta.hpp | ||
| return Stepper(typename Stepper::error_checker_type(m_abs_tol, m_rel_tol), | ||
| typename Stepper::step_adjuster_type(m_dt_max)); | ||
| typename Stepper::step_adjuster_type(std::numeric_limits<FP>::max())); // handle dt_max manually | ||
| } | ||
|
|
||
| FP m_abs_tol, m_rel_tol; ///< Absolute and relative tolerances for integration. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is important that we check this. But only in the debug build. I think it makes sense to issue at least a warning to the user here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this is a perfect use case for for an assert, since we cannot continue if these conditions fail, but checking this every iteration is needlessly expensive.
And since the asserts are printed in plain text on failure, I don't see the need to add text to it.