-
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
Conversation
cpp/tests/test_graph_simulation.cpp
Outdated
| auto expected_values_n0 = std::vector<double>{692.0, 43.647065696491225, 95.758901343443526, 159.59403296006528}; | ||
| auto actual_values_n0 = std::vector<double>{result_n0[0], result_n0[1], result_n0[2], result_n0[3]}; | ||
| auto expected_values_n1 = std::vector<double>{708.0, 44.063147085799322, 96.485223892060375, 160.45162902214025}; | ||
| auto expected_values_n1 = std::vector<double>{708.0, 44.04685402850226, 96.476851270558342, 160.47629470093941}; |
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.
Can you explain to me why these values have to be changed?
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.
Yes, it is due to the newly forced steps. Disabling forced step sizing (i.e. overriding the bool with true) does recover the old results.
The graph simulation makes small time steps in which it calls each node simulation, which usually calls advance with dt < t_max - t, hence it makes only one forced time step. There are at least two steps smaller than dt_min, hence the discrepancy.
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.
Actually, there was a problem with the OdeIntegrator. In its advance function, I forced a step whenever making the last step, i.e. dt < t_max - t. However, we do not care whether this is the last step or not, we want to know whether the last step would be smaller than dt_min (so whenever the step with dt = t_max - t would fail to adapt). Only then we actually need to force a smaller step, otherwise we may loose accuracy.
The new logic in advance re-takes a step, which is not optimal, but currently there is no way to get dt_min from advance. Maybe we need another virtual method?
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1046 +/- ##
==========================================
+ Coverage 96.23% 96.24% +0.01%
==========================================
Files 128 128
Lines 10852 10861 +9
==========================================
+ Hits 10443 10453 +10
+ Misses 409 408 -1 ☔ View full report in Codecov by Sentry. |
|
With these changes, the application where the problem occurred works as expected. Will review the code soon. |
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.
Thank you for the work! I still need to review the test.
You stated in the description: a forced step currently always counts as successful.
Nevertheless, I still get the warning "Adaptive step sizing failed. Forced at least one integration step of size dt_min." if the last step is forced.
Is this a problem?
| } | ||
|
|
||
| dt = std::min(dt, m_dt_max); | ||
| dt = std::min(dt, m_dt_max); |
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.
Why is this line not part of the if statement? It will only change dt if dt>m_dt_max.
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.
dont we need dt = std::max(dt, m_dt_min); (or usestd::clamp(dt, m_dt_min, m_dt_max)as in the stepper_wrapper) additionally?
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.
Why is this line not part of the if statement?
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?
| * (If adaptive, the given dt is used as the maximum step size, and must be within [dt_min, dt_max]. | ||
| * During integration, dt is adjusted in [dt_min, dt_max] to have an optimal size for the next step.) | ||
| * @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. |
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.
Should this be adjusted?
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.
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.
| // 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; |
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.
I think it would be enough to just use one bool.
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.
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?
| * @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. |
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.
Maybe a little more detailed (as in the integrator)?
The message should only appear if a non-forced step failed to adapt, i.e. the stepper wanted to make a step smaller than dt_min. Are you sure this message is due to the last step? |
Co-authored-by: lenaploetzke <70579874+lenaploetzke@users.noreply.github.com>
HenrZu
left a comment
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.
Just a few short comments. Otherwise really great, especially the tests :)
| assert(0 <= m_dt_min); | ||
| assert(m_dt_min <= m_dt_max); |
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.
| } | ||
|
|
||
| dt = std::min(dt, m_dt_max); | ||
| dt = std::min(dt, m_dt_max); |
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.
dont we need dt = std::max(dt, m_dt_min); (or usestd::clamp(dt, m_dt_min, m_dt_max)as in the stepper_wrapper) additionally?
| 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) { |
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.
Maybe add a tolerance here? However, it would be hard to set since we have the template FP type
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.
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?
| 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); |
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.
same as in adapt_rk.h
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.
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?
| // 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); |
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.
isnt this only necessary if !is_dt_valid && !force_step_size ?
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.
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).
|
@HenrZu @lenaploetzke I think I don't like the force_step_size approach anymore, it makes both the OdeIntegrator as well as the IntegratorCore weirdly complicated. I made another version in #1049, could you please check that one out instead? |
|
Closed in favor of v2. |
Changes and Information
Please briefly list the changes (main added features, changed items, or corrected bugs) made:
If need be, add additional information and what the reviewer should look out for in particular:
Merge Request - Guideline Checklist
Please check our git workflow. Use the draft feature if the Pull Request is not yet ready to review.
Checks by code author
Checks by code reviewer(s)
Closes #1038