#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// C callback for derivatives: fills dydt of length n given y of length n at time t.
typedef void (*ode45_deriv_cb)(double t, const double* y, double* dydt, size_t n, void* ctx);
// Integrate from t0 to t1 using Dormand-Prince 5(4) with adaptive steps
// Integrate from t0 to t1 using Dormand-Prince 5(4) with adaptive steps.
// Inputs:
// - t0, y0[n]: initial time/state
// - t1: target time
// - h_init: initial step size (non-zero)
// - atol, rtol: tolerances
// - normControl: 0 (component-wise max) or 1 (global 2-norm)
// - max_steps: safety cap
// - cb, ctx: derivative callback + user context
// Output:
// - y_out[n]: state at t1
// Returns 0 on success, non-zero on failure.
int ode45_integrate_final(double t0,
const double* y0,
size_t n,
double t1,
double h_init,
double atol,
double rtol,
int normControl,
int max_steps,
ode45_deriv_cb cb,
void* ctx,
double* y_out);
// Integrate and return samples at explicit times.
// Inputs:
// - t0, y0[n]
// - times[m]: strictly monotone in desired direction (first equals t0 recommended)
// - Remaining parameters like above.
// Output:
// - y_out[m*n]: concatenated row-major blocks of length n for each time.
// Returns 0 on success, non-zero on failure.
int ode45_integrate_times(double t0,
const double* y0,
size_t n,
const double* times,
size_t m,
double h_init,
double atol,
double rtol,
int normControl,
int max_steps,
ode45_deriv_cb cb,
void* ctx,
double* y_out);
#ifdef __cplusplus
}
#endif
#include <cmath>
#include <cstdlib>
#include <functional>
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <array>
#include <utility>
#include <string>
#include <vector>
// Standalone Dormand-Prince 5(4) (DOPRI5) adaptive Runge-Kutta integrator.
// No Boost dependencies.
using State = std::vector<double>;
using DerivFunc = std::function<void(double /*t*/, const State& /*y*/, State& /*dydt*/)>;
// Forward declaration for NormControl two-norm error
static double two_norm_scaled(const State& err, const State& y, const State& y_next, double atol, double rtol);
static State zeros_like(const State& y) {
return State(y.size(), 0.0);
}
// Dense output for Dormand-Prince 5(4): Hermite quartic interpolation (Octave-like)
static State dopri5_dense_output(double dt, const State& x0, const State& x1, const std::array<State,7>& K, double s) {
// s in [0,1], dt = h, K[0]=k1 at t, K[6]=k7 at t+dt
const size_t n = x0.size();
State out(n);
if (n == 0) return out;
// Coefficients for u_half = y(t+dt/2)
constexpr double c_half[7] = {
6025192743.0/30085553152.0,
0.0,
51252292925.0/65400821598.0,
-2691868925.0/45128329728.0,
187940372067.0/1594534317056.0,
-1776094331.0/19743644256.0,
11237099.0/235043384.0,
};
State u_half(n, 0.0);
for (size_t i = 0; i < n; ++i) {
double acc = 0.0;
for (int k = 0; k < 7; ++k) acc += c_half[k] * K[k][i];
u_half[i] = x0[i] + 0.5 * dt * acc;
}
// Hermite quartic basis polynomials
const double s2 = s*s;
const double s3 = s2*s;
const double s4 = s3*s;
const double H0 = 1.0 - 11.0*s2 + 18.0*s3 - 8.0*s4;
const double H1 = s - 4.0*s2 + 5.0*s3 - 2.0*s4;
const double H2 = 16.0*s2 - 32.0*s3 + 16.0*s4;
const double H3 = -5.0*s2 + 14.0*s3 - 8.0*s4;
const double H4 = s2 - 3.0*s3 + 2.0*s4;
for (size_t i = 0; i < n; ++i) {
out[i] = H0 * x0[i]
+ H1 * (dt * K[0][i])
+ H2 * u_half[i]
+ H3 * x1[i]
+ H4 * (dt * K[6][i]);
}
return out;
}
static double inf_norm_scaled(const State& err, const State& y, const State& y_next, double atol, double rtol) {
// Matches Octave's AbsRel_norm with NormControl == false:
// err_norm = max_i (|err_i| / scale_i), scale_i = max(AbsTol, RelTol * max(|y_i|, |y_next_i|))
if (err.size() != y.size() || err.size() != y_next.size()) {
throw std::runtime_error("State size mismatch");
}
if (err.empty()) {
return 0.0;
}
double max_ratio = 0.0;
for (size_t i = 0; i < err.size(); ++i) {
const double sc = std::max(atol, rtol * std::max(std::abs(y[i]), std::abs(y_next[i])));
const double r = (sc > 0.0) ? (std::abs(err[i]) / sc) : std::abs(err[i]);
if (r > max_ratio) max_ratio = r;
}
return max_ratio;
}
// Performs one DOPRI5 step. Computes y_next (5th order) and err (difference 5th-4th).
static void runge_kutta_dopri5_step(const DerivFunc& f, double t, const State& y, double h, State& y_next,
State& err, std::array<State,7>& K) {
const size_t n = y.size();
if (n == 0) {
y_next.clear();
err.clear();
return;
}
State k1(n), k2(n), k3(n), k4(n), k5(n), k6(n), k7(n);
State tmp(n);
// Butcher tableau coefficients for Dormand-Prince 5(4)
// c_i
constexpr double c2 = 1.0 / 5.0;
constexpr double c3 = 3.0 / 10.0;
constexpr double c4 = 4.0 / 5.0;
constexpr double c5 = 8.0 / 9.0;
constexpr double c6 = 1.0;
constexpr double c7 = 1.0;
// a (lower triangular):
constexpr double a21 = 1.0 / 5.0;
constexpr double a31 = 3.0 / 40.0;
constexpr double a32 = 9.0 / 40.0;
constexpr double a41 = 44.0 / 45.0;
constexpr double a42 = -56.0 / 15.0;
constexpr double a43 = 32.0 / 9.0;
constexpr double a51 = 19372.0 / 6561.0;
constexpr double a52 = -25360.0 / 2187.0;
constexpr double a53 = 64448.0 / 6561.0;
constexpr double a54 = -212.0 / 729.0;
constexpr double a61 = 9017.0 / 3168.0;
constexpr double a62 = -355.0 / 33.0;
constexpr double a63 = 46732.0 / 5247.0;
constexpr double a64 = 49.0 / 176.0;
constexpr double a65 = -5103.0 / 18656.0;
constexpr double a71 = 35.0 / 384.0;
constexpr double a72 = 0.0;
constexpr double a73 = 500.0 / 1113.0;
constexpr double a74 = 125.0 / 192.0;
constexpr double a75 = -2187.0 / 6784.0;
constexpr double a76 = 11.0 / 84.0;
// b (5th order) equals row 7 coefficients (a71..a76) with b7=0.
constexpr double b1 = a71;
constexpr double b2 = a72;
constexpr double b3 = a73;
constexpr double b4 = a74;
constexpr double b5 = a75;
constexpr double b6 = a76;
constexpr double b7 = 0.0;
// b_hat (4th order):
constexpr double bh1 = 5179.0 / 57600.0;
constexpr double bh2 = 0.0;
constexpr double bh3 = 7571.0 / 16695.0;
constexpr double bh4 = 393.0 / 640.0;
constexpr double bh5 = -92097.0 / 339200.0;
constexpr double bh6 = 187.0 / 2100.0;
constexpr double bh7 = 1.0 / 40.0;
// k1
f(t, y, k1);
// k2
for (size_t i = 0; i < n; ++i) tmp[i] = y[i] + h * (a21 * k1[i]);
f(t + c2 * h, tmp, k2);
// k3
for (size_t i = 0; i < n; ++i) tmp[i] = y[i] + h * (a31 * k1[i] + a32 * k2[i]);
f(t + c3 * h, tmp, k3);
// k4
for (size_t i = 0; i < n; ++i) tmp[i] = y[i] + h * (a41 * k1[i] + a42 * k2[i] + a43 * k3[i]);
f(t + c4 * h, tmp, k4);
// k5
for (size_t i = 0; i < n; ++i)
tmp[i] = y[i] + h * (a51 * k1[i] + a52 * k2[i] + a53 * k3[i] + a54 * k4[i]);
f(t + c5 * h, tmp, k5);
// k6
for (size_t i = 0; i < n; ++i)
tmp[i] = y[i] + h * (a61 * k1[i] + a62 * k2[i] + a63 * k3[i] + a64 * k4[i] + a65 * k5[i]);
f(t + c6 * h, tmp, k6);
// 5th order solution y_next using b1..b6
y_next.resize(n);
for (size_t i = 0; i < n; ++i)
y_next[i] = y[i] + h * (b1 * k1[i] + b2 * k2[i] + b3 * k3[i] + b4 * k4[i] + b5 * k5[i] + b6 * k6[i] + b7 * 0.0);
// k7 evaluated at (t+h, y_next)
f(t + c7 * h, y_next, k7);
// error estimate (difference between 5th and 4th order solutions)
err.resize(n);
for (size_t i = 0; i < n; ++i) {
const double e = h * ((b1 - bh1) * k1[i] + (b2 - bh2) * k2[i] + (b3 - bh3) * k3[i] + (b4 - bh4) * k4[i] +
(b5 - bh5) * k5[i] + (b6 - bh6) * k6[i] + (0.0 - bh7) * k7[i]);
err[i] = e;
}
// Export stages for dense output interpolation
K[0] = std::move(k1);
K[1] = std::move(k2);
K[2] = std::move(k3);
K[3] = std::move(k4);
K[4] = std::move(k5);
K[5] = std::move(k6);
K[6] = std::move(k7);
}
// Adaptive integration wrapper.
static State integrate_runge_kutta_dopri5(const DerivFunc& f,
double t0,
const State& y0,
double t1,
double h_init,
double atol,
double rtol,
bool normControl = false,
int max_steps = 100000) {
if (h_init == 0.0) {
throw std::runtime_error("h_init must be non-zero");
}
if (max_steps <= 0) {
throw std::runtime_error("max_steps must be positive");
}
const double dir = (t1 >= t0) ? 1.0 : -1.0;
double t = t0;
State y = y0;
double h = std::abs(h_init) * dir;
// Step-size control parameters
const double safety = 0.9;
const double min_factor = 0.2;
const double max_factor = 10.0;
const double tiny = std::numeric_limits<double>::min();
for (int step = 0; step < max_steps; ++step) {
if ((dir > 0.0 && t >= t1) || (dir < 0.0 && t <= t1)) {
return y;
}
// Clamp step to hit t1 exactly
if ((dir > 0.0 && t + h > t1) || (dir < 0.0 && t + h < t1)) {
h = t1 - t;
}
State y_next, err;
std::array<State,7> K;
runge_kutta_dopri5_step(f, t, y, h, y_next, err, K);
const double err_norm = normControl
? two_norm_scaled(err, y, y_next, atol, rtol)
: inf_norm_scaled(err, y, y_next, atol, rtol);
if (err_norm <= 1.0) {
// Accept step
t += h;
y.swap(y_next);
}
// Compute next step size
// Embedded RK 5(4): controller exponent 1/5.
double factor;
if (err_norm <= tiny) {
factor = max_factor;
} else if (!std::isfinite(err_norm)) {
// Non-finite error: shrink step aggressively
factor = min_factor;
} else {
factor = safety * std::pow(1.0 / err_norm, 0.2);
factor = std::min(max_factor, std::max(min_factor, factor));
}
h *= factor;
if (h == 0.0) {
throw std::runtime_error("Step size underflow");
}
// If rejected (including non-finite), try again without advancing time/state.
if (err_norm > 1.0 || !std::isfinite(err_norm)) {
continue;
}
}
throw std::runtime_error("Reached max_steps without finishing integration");
}
struct Trajectory {
std::vector<double> t;
std::vector<State> y;
};
// Integrate and return dense output samples according to refine (>=1)
static Trajectory integrate_runge_kutta_dopri5_refined(const DerivFunc& f,
double t0,
const State& y0,
double t1,
double h_init,
double atol,
double rtol,
bool normControl,
int refine,
int max_steps = 100000) {
if (h_init == 0.0) throw std::runtime_error("h_init must be non-zero");
if (max_steps <= 0) throw std::runtime_error("max_steps must be positive");
if (refine < 1) refine = 1;
const double dir = (t1 >= t0) ? 1.0 : -1.0;
double t = t0;
State y = y0;
double h = std::abs(h_init) * dir;
const double safety = 0.9;
const double min_factor = 0.2;
const double max_factor = 10.0;
const double tiny = std::numeric_limits<double>::min();
Trajectory traj;
traj.t.push_back(t);
traj.y.push_back(y);
for (int step = 0; step < max_steps; ++step) {
if ((dir > 0.0 && t >= t1) || (dir < 0.0 && t <= t1)) {
return traj;
}
if ((dir > 0.0 && t + h > t1) || (dir < 0.0 && t + h < t1)) {
h = t1 - t;
}
State y_next, err;
std::array<State,7> K;
runge_kutta_dopri5_step(f, t, y, h, y_next, err, K);
const double err_norm = normControl
? two_norm_scaled(err, y, y_next, atol, rtol)
: inf_norm_scaled(err, y, y_next, atol, rtol);
if (err_norm <= 1.0) {
// Accepted: emit interior refined points using dense output
const double dt = h;
for (int i = 1; i < refine; ++i) {
const double s = static_cast<double>(i) / static_cast<double>(refine);
State yi = dopri5_dense_output(dt, y, y_next, K, s);
traj.t.push_back(t + s * h);
traj.y.push_back(std::move(yi));
}
// Endpoint
t += h;
y.swap(y_next);
traj.t.push_back(t);
traj.y.push_back(y);
}
// Step-size controller
double factor;
if (err_norm <= tiny) {
factor = max_factor;
} else if (!std::isfinite(err_norm)) {
factor = min_factor;
} else {
factor = safety * std::pow(1.0 / err_norm, 0.2);
factor = std::min(max_factor, std::max(min_factor, factor));
}
h *= factor;
if (h == 0.0) throw std::runtime_error("Step size underflow");
if (err_norm > 1.0 || !std::isfinite(err_norm)) continue;
}
throw std::runtime_error("Reached max_steps without finishing integration");
}
// Integrate and return samples exactly at requested times using dense output per accepted step.
static Trajectory integrate_runge_kutta_dopri5_at_times(const DerivFunc& f,
double t0,
const State& y0,
const std::vector<double>& times,
double h_init,
double atol,
double rtol,
bool normControl,
int max_steps = 100000) {
if (h_init == 0.0) throw std::runtime_error("h_init must be non-zero");
if (max_steps <= 0) throw std::runtime_error("max_steps must be positive");
if (times.empty()) throw std::runtime_error("times must be non-empty");
// Ensure monotonicity direction matches integration
const double dir = (times.back() >= times.front()) ? 1.0 : -1.0;
// t0 must equal first time to match initial condition semantics
if (std::abs(times.front() - t0) > 0) {
t0 = times.front();
}
double t = t0;
State y = y0;
double h = std::abs(h_init) * dir;
const double safety = 0.9;
const double min_factor = 0.2;
const double max_factor = 10.0;
const double tiny = std::numeric_limits<double>::min();
Trajectory traj;
size_t idx = 0;
// Emit initial sample if requested equals t0
if ((dir > 0 && times[idx] <= t) || (dir < 0 && times[idx] >= t)) {
traj.t.push_back(times[idx]);
traj.y.push_back(y);
++idx;
}
for (int step = 0; step < max_steps && idx < times.size(); ++step) {
// If we've passed the last requested time, stop
if ((dir > 0.0 && t >= times.back()) || (dir < 0.0 && t <= times.back())) {
break;
}
// Adjust step to not skip beyond last requested time
if ((dir > 0.0 && t + h > times.back()) || (dir < 0.0 && t + h < times.back())) {
h = times.back() - t;
}
State y_next, err;
std::array<State,7> K;
runge_kutta_dopri5_step(f, t, y, h, y_next, err, K);
const double err_norm = normControl
? two_norm_scaled(err, y, y_next, atol, rtol)
: inf_norm_scaled(err, y, y_next, atol, rtol);
// Step-size controller
double factor;
if (err_norm <= tiny) {
factor = max_factor;
} else if (!std::isfinite(err_norm)) {
factor = min_factor;
} else {
factor = safety * std::pow(1.0 / err_norm, 0.2);
factor = std::min(max_factor, std::max(min_factor, factor));
}
h *= factor;
if (h == 0.0) throw std::runtime_error("Step size underflow");
if (err_norm > 1.0 || !std::isfinite(err_norm)) continue; // rejected
// Accepted: emit samples at times within (t, t+h]
const double dt = times.back() - times.front(); // unused; keep h for s calc
while (idx < times.size()) {
const double tau = times[idx];
// Check whether tau is within this step interval
const bool in_interval = (dir > 0.0) ? (tau > t && tau <= t + h) : (tau < t && tau >= t + h);
if (!in_interval) break;
const double s = (tau - t) / h; // works both directions
State yi = dopri5_dense_output(std::abs(h), y, y_next, K, s);
traj.t.push_back(tau);
traj.y.push_back(std::move(yi));
++idx;
}
// Advance to end of step
t += h;
y.swap(y_next);
}
if (idx != times.size()) {
throw std::runtime_error("Failed to sample all requested times before reaching max_steps");
}
return traj;
}
struct Options {
double t0 = 0.0;
double t1 = 5.0;
double h_init = 0.1;
double atol = 1e-10;
double rtol = 1e-10;
bool normControl = false; // Octave default is off
int refine = 4; // Octave default is 4 for ode45
int max_steps = 100000;
std::vector<double> times; // explicit sample times; overrides refine when non-empty
};
static Options parseArgs(int argc, char** argv, const Options& defaults) {
Options opt = defaults;
auto starts_with = [](const std::string& s, const std::string& p) { return s.rfind(p,0)==0; };
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--help" || a == "-h") {
std::cout << "Options:\n"
<< " --t0=VAL start time (default 0)\n"
<< " --t1=VAL end time (default 5)\n"
<< " --h-init=VAL initial step size\n"
<< " --atol=VAL absolute tolerance\n"
<< " --rtol=VAL relative tolerance\n"
<< " --normcontrol=on|off norm control (default off)\n"
<< " --refine=N points per step (default 4)\n"
<< " --maxsteps=N max steps (default 100000)\n"
<< " --times=t0,t1,... explicit times to sample (overrides refine)\n";
std::exit(0);
}
auto eq = a.find('=');
if (eq == std::string::npos) continue;
const std::string key = a.substr(0, eq+1);
const std::string val = a.substr(eq+1);
try {
if (key == "--t0=") opt.t0 = std::stod(val);
else if (key == "--t1=") opt.t1 = std::stod(val);
else if (key == "--h-init=") opt.h_init = std::stod(val);
else if (key == "--atol=") opt.atol = std::stod(val);
else if (key == "--rtol=") opt.rtol = std::stod(val);
else if (key == "--refine=") opt.refine = std::stoi(val);
else if (key == "--maxsteps=") opt.max_steps = std::stoi(val);
else if (key == "--normcontrol=") {
const std::string v = val;
opt.normControl = (v == "on" || v == "ON" || v == "true" || v == "1");
} else if (key == "--times=") {
// Parse comma-separated list of doubles
opt.times.clear();
size_t start = 0; while (start <= val.size()) {
size_t comma = val.find(',', start);
std::string tok = val.substr(start, (comma==std::string::npos)? std::string::npos : (comma-start));
if (!tok.empty()) opt.times.push_back(std::stod(tok));
if (comma == std::string::npos) break; else start = comma + 1;
}
}
} catch (...) {
std::cerr << "Invalid value for option: " << a << "\n";
std::exit(1);
}
}
if (opt.refine < 1) opt.refine = 1;
return opt;
}
int main(int argc, char** argv) {
// Example: y' = -y, y(0) = 1. Exact solution: y(t) = exp(-t)
DerivFunc f = [](double /*t*/, const State& y, State& dydt) {
dydt.resize(y.size());
for (size_t i = 0; i < y.size(); ++i) {
dydt[i] = -y[i];
}
};
const State y0 = {1.0};
Options defaults;
defaults.normControl = true; // demo default on; Octave is off
Options opt = parseArgs(argc, argv, defaults);
try {
// If explicit times provided, sample there; otherwise run default and optional refine
if (!opt.times.empty()) {
// Expect first time equals opt.t0; if not, we start at first time.
auto traj = integrate_runge_kutta_dopri5_at_times(f, opt.t0, y0, opt.times, opt.h_init,
opt.atol, opt.rtol, opt.normControl, opt.max_steps);
std::cout << "samples (first 5 of explicit times):\n";
for (size_t i = 0; i < std::min<size_t>(traj.t.size(),5); ++i) {
std::cout << std::fixed << std::setprecision(8) << traj.t[i] << ": " << traj.y[i][0] << "\n";
}
// Also print last sample
std::cout << std::setprecision(16);
std::cout << "y(" << traj.t.back() << ") = " << traj.y.back()[0] << "\n";
} else {
const State y1 = integrate_runge_kutta_dopri5(f, opt.t0, y0, opt.t1, opt.h_init, opt.atol,
opt.rtol, opt.normControl, opt.max_steps);
const double exact = std::exp(-opt.t1);
std::cout << std::setprecision(16);
std::cout << "y(" << opt.t1 << ") = " << y1[0] << "\n";
std::cout << "exact = " << exact << "\n";
std::cout << "abs err = " << std::abs(y1[0] - exact) << "\n";
if (opt.refine > 1) {
auto traj = integrate_runge_kutta_dopri5_refined(f, opt.t0, y0, opt.t1, opt.h_init, opt.atol,
opt.rtol, opt.normControl, opt.refine, opt.max_steps);
std::cout << "samples (first 5):\n";
for (size_t i = 0; i < std::min<size_t>(traj.t.size(),5); ++i) {
std::cout << std::fixed << std::setprecision(8) << traj.t[i] << ": " << traj.y[i][0] << "\n";
}
// Verify Octave's length relationship: len_R = R*len_1 - (R-1)
auto coarse = integrate_runge_kutta_dopri5_refined(f, opt.t0, y0, opt.t1, opt.h_init, opt.atol,
opt.rtol, opt.normControl, 1, opt.max_steps);
const size_t lenR = traj.t.size();
const size_t len1 = coarse.t.size();
std::cout << "length refine = " << opt.refine << ": " << lenR << ", expected " <<
(opt.refine*len1 - (opt.refine-1)) << "\n";
}
}
return 0;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
}
static double two_norm_scaled(const State& err, const State& y, const State& y_next, double atol, double rtol) {
// Matches two_norm_scaled with NormControl == true:
// sc = max(AbsTol(:), RelTol * max(||y||_2, ||y_next||_2))
// err_norm = ||err||_2 / sc
if (err.size() != y.size() || err.size() != y_next.size()) {
throw std::runtime_error("State size mismatch");
}
if (err.empty()) {
return 0.0;
}
auto norm2 = [](const State& v) {
long double s = 0.0L;
for (double vi : v) s += static_cast<long double>(vi) * static_cast<long double>(vi);
return std::sqrt(static_cast<double>(s));
};
const double sc = std::max(atol, rtol * std::max(norm2(y), norm2(y_next)));
const double e2 = norm2(err);
return (sc > 0.0) ? (e2 / sc) : e2;
}
// --- C API wrappers (extern "C") ---
extern "C" {
typedef void (*ode45_deriv_cb)(double t, const double* y, double* dydt, size_t n, void* ctx);
int ode45_integrate_final(double t0,
const double* y0,
size_t n,
double t1,
double h_init,
double atol,
double rtol,
int norm_control,
int max_steps,
ode45_deriv_cb cb,
void* ctx,
double* y_out) {
try {
State y0v(n);
for (size_t i = 0; i < n; ++i) y0v[i] = y0[i];
DerivFunc f = [cb, n, ctx](double t, const State& y, State& dydt) {
dydt.resize(n);
cb(t, y.data(), dydt.data(), n, ctx);
};
State y1 = integrate_runge_kutta_dopri5(f, t0, y0v, t1, h_init, atol, rtol, norm_control != 0, max_steps);
for (size_t i = 0; i < n; ++i) y_out[i] = y1[i];
return 0;
} catch (...) {
return -1;
}
}
int ode45_integrate_times(double t0,
const double* y0,
size_t n,
const double* times,
size_t m,
double h_init,
double atol,
double rtol,
int normControl,
int max_steps,
ode45_deriv_cb cb,
void* ctx,
double* y_out) {
try {
State y0v(n);
for (size_t i = 0; i < n; ++i) y0v[i] = y0[i];
std::vector<double> tv(m);
for (size_t i = 0; i < m; ++i) tv[i] = times[i];
DerivFunc f = [cb, n, ctx](double t, const State& y, State& dydt) {
dydt.resize(n);
cb(t, y.data(), dydt.data(), n, ctx);
};
Trajectory traj = integrate_runge_kutta_dopri5_at_times(f, t0, y0v, tv, h_init, atol, rtol, normControl != 0, max_steps);
if (traj.t.size() != m || traj.y.size() != m) return -2;
// Row-major blocks: for each time i, copy n entries
for (size_t i = 0; i < m; ++i) {
const State& yi = traj.y[i];
for (size_t j = 0; j < n; ++j) {
y_out[i*n + j] = yi[j];
}
}
return 0;
} catch (...) {
return -1;
}
}
}
/*
y(5) = 0.00673794718173955
exact = 0.006737946999085467
abs err = 1.98847773490845e-011
samples (first 5):
0.00000000: 1.00000000
0.00927251: 0.99077034
0.01854503: 0.98162587
0.02781754: 0.97256580
0.03709006: 0.96358935
length refine=4: 349, expected 349
*/