Program Listing for File integrator.cc¶
↰ Return to documentation for file (numerics/integrator.cc)
#include "lupnt/numerics/integrator.h"
#include "lupnt/core/error.h"
#include "lupnt/core/progress_bar.h"
namespace lupnt {
void IntegratorParams::CheckIntegratorParams() {
LUPNT_CHECK(max_iter > 0, "max_iter must be greater than 0", "IntegratorParams");
LUPNT_CHECK(abstol > 0, "abstol must be non-negative", "IntegratorParams");
LUPNT_CHECK(reltol > 0, "reltol must be non-negative", "IntegratorParams");
}
State Integrator::Propagate(const ODE& odefunc, Real t0, Real tf, const State& x0, Real dt) {
LUPNT_CHECK(dt > 0, "dt must be positive", "Integrator");
State x = x0;
Real t = t0;
Ptr<ProgressBar> pbar = nullptr;
if (print_progress_) pbar = Logger::GetProgressBar(100, "", "Integrator");
while (t < tf) {
dt = std::min(dt, tf - t);
Real prev_dt = dt;
x = Step(odefunc, t, x, dt);
t += prev_dt;
if (print_progress_) pbar->Update(int((t - t0) / (tf - t0) * 100));
}
if (print_progress_) pbar->Finish();
return x;
};
State Integrator::Propagate(const ODE& odefunc, Real t0, Real tf, const State& x0, Real dt,
MatXd* J) {
auto func = [=, this](const State& x) { return Propagate(odefunc, t0, tf, x, dt); };
State xf = (J != nullptr) ? JacobianParallel(func, x0, *J) : func(x0);
return xf;
};
IntegratorResult Integrator::PropagateEx(const ODE& odefunc, Real t0, Real tf, const VecX& x0,
Real dt) {
LUPNT_CHECK(dt > 0, "dt must be positive (magnitude)", "Integrator"); // treat as magnitude
VecX x = x0;
Real t = t0;
int steps = 0;
const double span = std::abs((tf - t0).val());
if (span == 0) {
// No propagation needed
return {x0, t0, TerminationReason::ReachedTf, 0};
}
const double dir = (tf.val() > t0.val()) ? 1 : -1; // forward or backward
if (print_progress_) {
ProgressBar pbar(100);
pbar.SetDescription(dir > 0 ? "Integrating (forward)" : "Integrating (backward)");
// initial early-termination check
if (params_.terminate_if && params_.terminate_if(t, x)) {
pbar.Finish();
return {x, t, TerminationReason::UserCondition, steps};
}
while ((dir > 0 && t < tf) || (dir < 0 && t > tf)) {
// signed step limited so we land exactly on tf
Real h = dir * std::min(std::abs(dt.val()), std::abs((tf - t).val()));
Real prev_h = h;
x = Step(odefunc, t, State(x), h);
t += prev_h;
++steps;
if (params_.terminate_if && params_.terminate_if(t, x)) {
// progress uses absolute distances in time
int pct = int(std::round(std::abs((t - t0).val()) / span * 100.0));
pbar.Update(std::min(100, std::max(0, pct)));
pbar.Finish();
return {x, t, TerminationReason::UserCondition, steps};
}
int pct = int(std::round(std::abs((t - t0).val()) / span * 100.0));
pbar.Update(std::min(100, std::max(0, pct)));
}
pbar.Finish();
return {x, t, TerminationReason::ReachedTf, steps};
} else {
// same logic without progress bar
if (params_.terminate_if && params_.terminate_if(t, x)) {
return {x, t, TerminationReason::UserCondition, steps};
}
while ((dir > 0 && t < tf) || (dir < 0 && t > tf)) {
Real h = dir * std::min(std::abs(dt.val()), std::abs((tf - t).val()));
Real prev_h = h;
x = Step(odefunc, t, State(x), h);
t += prev_h;
++steps;
if (params_.terminate_if && params_.terminate_if(t, x)) {
return {x, t, TerminationReason::UserCondition, steps};
}
}
return {x, t, TerminationReason::ReachedTf, steps};
}
}
IntegratorResult Integrator::PropagateEx(const ODE& f, Real t0, Real tf, const VecX& x0, Real dt,
MatXd* J) {
// One simple approach: propagate nominally and get Jacobian via your existing
// path. If you need the Jacobian at the *actual stop time*, call PropagateEx
// inside the Jacobian evaluator.
auto func = [=, this](const VecX& x) { return PropagateEx(f, t0, tf, x, dt); };
if (J) JacobianParallel([&](const VecX& x) { return func(x).x; }, x0, *J);
return func(x0);
}
VecX Integrator::Propagate(const ODE& odefunc, Real t0, Real tf, const VecX& x0, Real dt) {
IntegratorResult res = PropagateEx(odefunc, t0, tf, x0, dt);
return res.x;
};
VecX Integrator::Propagate(const ODE& odefunc, Real t0, Real tf, const VecX& x0, Real dt,
MatXd* J) {
auto func = [=, this](const VecX& x) { return Propagate(odefunc, t0, tf, x, dt); };
VecX xf = (J != nullptr) ? JacobianParallel(func, x0, *J) : func(x0);
return xf;
};
State RK4::Step(const ODE& f, Real t, const State& x, Real dt) {
// usleep(1000);
// return x + x;
/* Evaluate `f` (i.e., `dx`) at the 4 locations defined bx the RK4 method */
State k_1 = f(t, x) * dt;
Real t1 = t + dt / 2.0;
State x1 = x + k_1 / 2.0;
State k_2 = f(t1, x1) * dt;
Real t2 = t + dt / 2.0;
State x2 = x + k_2 / 2.0;
State k_3 = f(t2, x2) * dt;
Real t3 = t + dt;
State x3 = x + k_3;
State k_4 = f(t3, x3) * dt;
/* Average the 4 derivatives to approtimate `dx` */
State dx = (k_1 + k_2 * 2.0 + k_3 * 2.0 + k_4) / 6.0;
return x + dx;
}
State RK8::Step(const ODE& f, Real t, const State& x, Real dt) {
// 1
State k_1 = f(t, x) * dt;
// 2
Real t1 = t + dt * (4.0 / 27.0);
State x1 = x + k_1 * (4.0 / 27.0);
State k_2 = f(t1, x1) * dt;
// 3
Real t2 = t + dt * (2.0 / 9);
State x2 = x + (1.0 / 18) * (k_1 + 3 * k_2);
State k_3 = f(t2, x2) * dt;
// 4
Real t3 = t + dt * (1.0 / 3);
State x3 = x + (1.0 / 12) * (k_1 + 3 * k_3);
State k_4 = f(t3, x3) * dt;
// 5
Real t4 = t + dt * (1.0 / 2);
State x4 = x + (1.0 / 8) * (k_1 + 3 * k_4);
State k_5 = f(t4, x4) * dt;
// 6
Real t5 = t + dt * (2.0 / 3);
State x5 = x + (1.0 / 54) * (13 * k_1 - 27 * k_3 + 42 * k_4 + 8 * k_5);
State k_6 = f(t5, x5) * dt;
// 7
Real t6 = t + dt * (1.0 / 6);
State x6 = x + (1.0 / 4320) * (389 * k_1 - 54 * k_3 + 966 * k_4 - 824 * k_5 + 243 * k_6);
State k_7 = f(t6, x6) * dt;
// 8
Real t7 = t + dt;
State x7
= x + (1.0 / 20) * (-234 * k_1 + 81 * k_3 - 1164 * k_4 + 656 * k_5 - 122 * k_6 + 800 * k_7);
State k_8 = f(t7, x7) * dt;
// 9
Real t8 = t + (5.0 / 6) * dt;
State x8
= x
+ (1.0 / 288)
* (-127 * k_1 + 18 * k_3 - 678 * k_4 + 456 * k_5 - 9 * k_6 + 576 * k_7 + 4 * k_8);
State k_9 = f(t8, x8) * dt;
// 10
Real t9 = t + dt;
State x9 = x
+ (1.0 / 820)
* (1481 * k_1 - 81 * k_3 + 7104 * k_4 - 3376 * k_5 + 72 * k_6 - 5040 * k_7
- 60 * k_8 + 720 * k_9);
State k_10 = f(t9, x9) * dt;
/* Approtimate `dx` */
State dx
= (41 * k_1 + 27 * k_4 + 272 * k_5 + 27 * k_6 + 216 * k_7 + 216 * k_9 + 41 * k_10) / 840;
return x + dx;
}
State IRKF::Step(const ODE& f, Real t, const State& x, Real dt) {
int n = x.size();
State x_new_low(n), x_new_high(n);
int iter;
for (iter = 0; iter < params_.max_iter; ++iter) {
Update(f, t, x, dt, x_new_low, x_new_high);
bool within_tolerance = ComputeRelError(x_new_low, x_new_high, dt);
if (within_tolerance) break;
}
LUPNT_CHECK(iter < params_.max_iter,
"IRKF did not converge, increase max_iter or reduce tolerance", "IRKF");
return x_new_low;
}
bool IRKF::ComputeRelError(const State& x_new_low, const State& x_new_high, Real& dt) {
// Compute the error norm
double tol = 0.0;
double error = 0.0;
bool within_tolerance = true;
double max_error = 0.0;
// Non-conservative acceptance threshold
double accept_thresh
= std::pow(order_ + 1,
(order_ + 1) / order_); // J.C. Butcher, Numerical Methods for
// Ordinary Differential Equations, p291
for (size_t i = 0; i < x_new_low.size(); ++i) {
error = abs(x_new_high(i) - x_new_low(i));
tol = max(params_.reltol * abs(x_new_high(i)), params_.abstol);
max_error = std::max(max_error, error / tol);
if ((error / tol) > accept_thresh) within_tolerance = false;
}
// Update timestep
double beta = 0.9; // safety factor
double s = beta * std::pow(1.0 / max_error, 1.0 / (order_ + 1));
s = std::max(0.5, std::min(2.0, s)); // J.C. Butcher, Numerical Methods for
// Ordinary Differential Equations, (371a)
dt = s * dt;
return within_tolerance;
}
void RKF45::Update(const ODE& f, Real t, const State& x, const Real dt, State& x_new_low,
State& x_new_high) {
// Compute k1 to k6
State k1 = f(t, x) * dt;
State k2 = f(t + dt / 4.0, x + k1 * (1.0 / 4.0)) * dt;
State k3 = f(t + dt * 3.0 / 8.0, x + k1 * (3.0 / 32.0) + k2 * (9.0 / 32.0)) * dt;
State k4 = f(t + dt * 12.0 / 13.0,
x + k1 * (1932.0 / 2197.0) - k2 * (7200.0 / 2197.0) + k3 * (7296.0 / 2197.0))
* dt;
State k5 = f(t + dt, x + k1 * (439.0 / 216.0) - k2 * 8.0 + k3 * (3680.0 / 513.0)
- k4 * (845.0 / 4104.0))
* dt;
State k6 = f(t + dt / 2.0, x - k1 * (8.0 / 27.0) + k2 * 2.0 - k3 * (3544.0 / 2565.0)
+ k4 * (1859.0 / 4104.0) - k5 * (11.0 / 40.0))
* dt;
// 4th order solution
x_new_low = x + k1 * (25.0 / 216.0) + k3 * (1408.0 / 2565.0) + k4 * (2197.0 / 4104.0)
- k5 * (1.0 / 5.0);
// 5th order solution
x_new_high = x + k1 * (16.0 / 135.0) + k3 * (6656.0 / 12825.0) + k4 * (28561.0 / 56430.0)
- k5 * (9.0 / 50.0) + k6 * (2.0 / 55.0);
}
const std::array<std::array<double, 6>, 7> PD45::A_
= {{{0, 0, 0, 0, 0, 0},
{1.0 / 5, 0, 0, 0, 0, 0},
{3.0 / 40, 9.0 / 40, 0, 0, 0, 0},
{44.0 / 45, -56.0 / 15, 32.0 / 9, 0, 0, 0},
{19372.0 / 6561, -25360.0 / 2187, 64448.0 / 6561, -212.0 / 729, 0, 0},
{9017.0 / 3168, -355.0 / 33, 46732.0 / 5247, 49.0 / 176, -5103.0 / 18656, 0},
{35.0 / 384, 0, 500.0 / 1113, 125.0 / 192, -2187.0 / 6784, 11.0 / 84}}};
const std::array<double, 7> PD45::b_
= {35.0 / 384, 0, 500.0 / 1113, 125.0 / 192, -2187.0 / 6784, 11.0 / 84, 0};
const std::array<double, 7> PD45::b_star_{
5179.0 / 57600, 0, 7571.0 / 16695, 393.0 / 640, -92097.0 / 339200, 187.0 / 2100, 1.0 / 40};
State PD45::Step(const ODE& f, Real t, const State& x, Real dt) {
const int stages = 7;
std::vector<State> k(stages); // To store intermediate k values
double safety = 0.9; // Safety factor for step size adjustment
double dt_min = 1e-3; // Minimum allowable step size
int iteration = 0;
while (iteration < this->params_.max_iter) {
// Compute intermediate stages
k[0] = dt * f(t, x);
for (int i = 1; i < stages; ++i) {
State sum = State::Zero(x.size());
for (int j = 0; j < i; ++j) {
sum += A_[i][j] * k[j];
}
k[i] = dt * f(t + A_[i][0] * dt, x + sum);
}
// Compute the high-order and low-order solutions
State y_high = x; // High-order solution
State y_low = x; // Low-order solution
for (int i = 0; i < stages; ++i) {
y_high += b_[i] * k[i];
y_low += b_star_[i] * k[i];
}
// Compute the error norm
State error_vec = y_high - y_low;
State scale
= (Real(this->params_.abstol) + this->params_.reltol * x.cwiseAbs().array()).matrix();
Real error_norm = (error_vec.cwiseQuotient(scale)).norm() / std::sqrt(x.size());
// Adjust step size based on error norm
if (error_norm <= 1.0) {
// Step is accepted
t += dt;
return y_high; // Return the high-order solution
} else {
// Step is rejected
double factor = std::pow(safety / error_norm.val(), 0.25);
dt *= std::max(factor, 0.1); // Decrease step size
}
// Ensure the step size is not too small
if (abs(dt.val()) < dt_min) {
throw std::runtime_error("Step size too small, unable to proceed.");
}
iteration++;
}
throw std::runtime_error("Maximum iterations exceeded in PD45 step.");
}
} // namespace lupnt