LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lsoda.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_LSODA_H
6#define LINE_UTIL_LSODA_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * LSODA: the LINE-facing wrapper over the vendored solver in
12 * `third_party/lsoda.hpp`.
13 *
14 * WHY LSODA AND NOT THE ROSENBROCK IN `ode.h`. Both integrate a stiff system,
15 * but the fluid solver has to AGREE WITH THE OTHER CODEBASES, not merely be
16 * accurate. MATLAB drives `ode15s`/`ode23t`, the JAR drives
17 * `jline.solvers.fluid.LSODAExt` over imperial-qore/lsoda-java, and native
18 * Python drives `line_solver/lib/lsoda.py`; the latter two are ports of the
19 * same Heng Li `lsoda.c` that the vendored C++ descends from. Running the same
20 * algorithm with the same coefficients is what makes the fluid results line up
21 * across languages; a different stiff integrator would be defensible
22 * numerically and would still disagree in the last digits everywhere.
23 * `ode.h` stays the integrator for everything that has no cross-codebase
24 * counterpart to match.
25 *
26 * DOUBLE ONLY, BY CONSTRUCTION. LSODA selects its own order and step from
27 * floating-point error estimates and the machine epsilon of `double` is baked
28 * into its coefficients, so there is no meaningful `Real<N>` or `Rational`
29 * instantiation of it. Callers templated on `T` must refuse by name for any
30 * other backend rather than silently narrowing to `double` -- `solver_fluid`
31 * does exactly that.
32 */
33
34#include <array>
35#include <cmath>
36#include <cstddef>
37#include <functional>
38#include <string>
39#include <vector>
40
41#include "line/util/error.h"
42#include "lsoda.hpp"
43
44namespace line {
45
46/**
47 * The right-hand side dy/dt = f(t, y).
48 *
49 * `y` and `dydt` are plain 0-based arrays of length neq. The vendored solver
50 * keeps its state 1-based internally, as the Fortran original did, and hands
51 * the callback pointers already offset past the unused slot, so nothing here
52 * has to know about that.
53 */
54using LsodaRhs = std::function<void(double t, const double* y, double* dydt)>;
55
56/**
57 * Integration controls.
58 *
59 * `max_steps` defaults to the JAR's value, not LSODA's own. Upstream defaults
60 * to mxstep = 500, which a stiff fluid model exhausts long before the horizon
61 * and then reports istate = -1; `LSODAExt` exists in the JAR precisely to raise
62 * it, and this default keeps the two in step.
63 */
65 double rtol = 1e-6; ///< relative tolerance, applied to every component
66 double atol = 1e-6; ///< absolute tolerance, applied to every component
67 /**
68 * Per-component tolerances. Empty means "use the scalars above"; otherwise
69 * the vector must have one entry per equation and overrides its scalar.
70 * A stiff model whose components live on different scales needs this --
71 * Robertson is the standard example, with atol 1e-10 on the fast species
72 * and 1e-6 on the other two.
73 */
74 std::vector<double> rtol_vec;
75 std::vector<double> atol_vec;
76 double h_init = 0.0; ///< initial step; 0 lets LSODA choose
77 double h_min = 0.0; ///< smallest admissible step; 0 means no bound
78 double h_max = 0.0; ///< largest admissible step; 0 means no bound
79 std::size_t max_steps = 10000000; ///< internal steps between output points
80 int max_order_nonstiff = 12; ///< Adams order cap (mxordn)
81 int max_order_stiff = 5; ///< BDF order cap (mxords)
82 /**
83 * Start on BDF and never switch to Adams.
84 *
85 * LSODA starts on Adams and switches only when its own detector says the
86 * problem is stiff, and that detector needs `pdlast`, the dominant
87 * eigenvalue read off the CORRECTOR's convergence rate. AT A FIXED POINT the
88 * corrector converges on the `del <= 100*pnorm*ETA` branch before `pdest` is
89 * ever formed, so `pdlast` stays 0, `scaleh`'s stability guard never binds,
90 * `h` runs up to `h_max`, and a high-order Adams-Moulton outside its
91 * stability region WANDERS around the fixed point instead of settling on
92 * it -- which is where a fluid solver spends most of its time. The JAR pins
93 * it for exactly this reason (`LSODA.setForceStiff(true)`), MATLAB and
94 * native Python carry the same flag, and MATLAB's own stiff slot is
95 * `ode15s`, a BDF/NDF method, so pinning is also what matches the reference.
96 *
97 * NOT THE DEFAULT HERE, and the reason is measured: pinning BDF means a
98 * finite-difference Jacobian from the first step, and LSODA sizes that
99 * increment as `max(sqrt(eps)*|y_j|, r0/ewt_j)`, which collapses to ~1e-19
100 * for a component sitting at exactly zero under a tight atol. Robertson
101 * started at (1, 0, 0) with atol (1e-6, 1e-10, 1e-6) diverges that way.
102 * A fluid drift is not that problem, but a general-purpose default must not
103 * carry the hazard.
104 */
105 bool force_stiff = false;
106 /**
107 * A test consulted after every ACCEPTED STEP, ending the integration when it
108 * returns true and holding the state for the rest of the grid.
109 *
110 * WHY THE OPTION EXISTS. `lsoda_integrate` reports at output times only, and
111 * a window whose drift has reached a fixed point has nothing left to report:
112 * f(y*) = 0 means y is that state for every later t, so the remaining span
113 * is known. Without this the step controller grinds -- a stiff controller
114 * handed a state it is already at cannot pick a step -- and the window never
115 * returns. See `solver_fluid.h` and FLUID_FIXED_POINT_GUARD (MATLAB).
116 *
117 * Empty by default, and the integration then runs exactly as it always has:
118 * the itask = 1 loop below is untouched, so no existing caller changes. Set,
119 * it selects the stepwise driver, which reproduces the same output grid one
120 * accepted step at a time through `LsodaStepper`.
121 */
122 std::function<bool(double, const std::vector<double>&)> step_stop;
123};
124
125/** Result of an integration, mirroring `OdeSolution` in `ode.h`. */
127 std::vector<double> t; ///< output times, t[0] = t_eval[0]
128 std::vector<std::vector<double>> y; ///< y[i] is the state at t[i]
129 std::size_t steps = 0; ///< internal steps taken (nst)
130 std::size_t f_evals = 0; ///< right-hand side evaluations (nfe)
131 std::size_t jacobians = 0; ///< Jacobian evaluations (nje)
132 bool success = true; ///< false when LSODA returned istate < 0
133 int istate = 2; ///< the solver's final istate
134 /** The method in force at the end: "adams" (nonstiff) or "bdf" (stiff). */
135 std::string method = "adams";
136
137 const std::vector<double>& final_state() const {
138 if (y.empty()) throw NumericError("LsodaSolution: no state was recorded");
139 return y.back();
140 }
141 double final_time() const {
142 if (t.empty()) throw NumericError("LsodaSolution: no state was recorded");
143 return t.back();
144 }
145};
146
147namespace lsoda_detail {
148
149/** Trampoline: the vendored callback is a raw function pointer plus a void*. */
150inline void rhs_trampoline(double t, double* y, double* dydt, void* data) {
151 (*static_cast<const LsodaRhs*>(data))(t, y, dydt);
152}
153
154} // namespace lsoda_detail
155
156/**
157 * Integrate dy/dt = f(t, y) from `t_eval.front()` through every later entry of
158 * `t_eval`, returning the state at each.
159 *
160 * `t_eval` must be non-empty and non-decreasing; its first entry is the initial
161 * time and is reported back unchanged with `y0`. This is the `tspan` form the
162 * fluid analyzers use, so one call covers both "give me the steady state at
163 * T" (two entries) and "give me the transient" (many).
164 *
165 * A solver failure is reported, not thrown: `success` is false and `istate`
166 * carries LSODA's own code, because the fluid iteration treats a step that did
167 * not converge as a signal to shorten the horizon rather than as an error.
168 */
169/**
170 * The stepwise driver behind `LsodaOptions::step_stop`; defined below, after
171 * `LsodaStepper`, which it drives. Declared here so `lsoda_integrate` can hand
172 * off to it without moving either function.
173 */
174inline LsodaSolution lsoda_integrate_stepwise(const LsodaRhs& f, const std::vector<double>& y0,
175 const std::vector<double>& t_eval,
176 const LsodaOptions& opt);
177
178inline LsodaSolution lsoda_integrate(const LsodaRhs& f, const std::vector<double>& y0,
179 const std::vector<double>& t_eval,
180 const LsodaOptions& opt = LsodaOptions()) {
181 if (t_eval.empty())
182 throw InputError("lsoda_integrate: t_eval is empty; it must carry at least the start time");
183 if (y0.empty())
184 throw InputError("lsoda_integrate: the initial state is empty");
185 for (std::size_t i = 1; i < t_eval.size(); ++i)
186 if (t_eval[i] < t_eval[i - 1])
187 throw InputError("lsoda_integrate: t_eval must be non-decreasing");
188
189 // A stop test needs the trajectory one accepted step at a time, which the
190 // itask = 1 loop below does not expose; the stepwise driver does, on the
191 // same grid. Only that caller pays for it -- everyone else keeps this loop.
192 if (opt.step_stop) return lsoda_integrate_stepwise(f, y0, t_eval, opt);
193
194 const std::size_t neq = y0.size();
195 LsodaSolution out;
196 out.t.push_back(t_eval[0]);
197 out.y.push_back(y0);
198
199 lsoda_impl::LSODA solver;
200 // Error weights, 1-based with slot 0 unused. itol follows ODEPACK: 1 both
201 // scalar, 2 atol per component, 3 rtol per component, 4 both.
202 const bool rvec = !opt.rtol_vec.empty(), avec = !opt.atol_vec.empty();
203 if (rvec && opt.rtol_vec.size() != neq)
204 throw InputError("lsoda_integrate: rtol_vec has one entry per equation or none");
205 if (avec && opt.atol_vec.size() != neq)
206 throw InputError("lsoda_integrate: atol_vec has one entry per equation or none");
207 std::vector<double> rtol1(neq + 1, opt.rtol), atol1(neq + 1, opt.atol);
208 rtol1[0] = 0.0;
209 atol1[0] = 0.0;
210 for (std::size_t k = 0; k < neq; ++k) {
211 if (rvec) rtol1[k + 1] = opt.rtol_vec[k];
212 if (avec) atol1[k + 1] = opt.atol_vec[k];
213 }
214 const int itol = rvec ? (avec ? 4 : 3) : (avec ? 2 : 1);
215 solver.set_tolerances(rtol1, atol1, itol);
216 solver.set_force_stiff(opt.force_stiff);
217
218 std::vector<double> y = y0; // 0-based, as lsoda_update expects
219 std::vector<double> yout;
220 double t = t_eval[0];
221 int istate = 1;
222
223 // iworks = {ml, mu, ixpr, mxstep, mxhnil, mxordn, mxords}
224 std::array<int, 7> iworks = {{0, 0, 0, static_cast<int>(opt.max_steps), 0,
225 opt.max_order_nonstiff, opt.max_order_stiff}};
226 // rworks = {tcrit, h0, hmax, hmin}; hmax is the step itself, and the solver
227 // inverts it into hmxi -- unlike lsoda-java, which is handed the inverse.
228 std::array<double, 4> rworks = {{0.0, opt.h_init, opt.h_max, opt.h_min}};
229 const int iopt = 1; // the optional inputs above are in force
230 const int jt = 2; // Jacobian generated internally, full: LSODA's default
231
232 const LsodaRhs* fp = &f;
233 for (std::size_t i = 1; i < t_eval.size(); ++i) {
234 const double tout = t_eval[i];
235 if (tout == t) { // a repeated output time asks for the state again
236 out.t.push_back(t);
237 out.y.push_back(y);
238 continue;
239 }
240 yout.assign(neq + 1, 0.0);
241 for (std::size_t k = 0; k < neq; ++k) yout[k + 1] = y[k];
242 solver.lsoda(lsoda_detail::rhs_trampoline, neq, yout, &t, tout, 1 /*itask*/, &istate,
243 iopt, jt, iworks, rworks, const_cast<LsodaRhs*>(fp));
244 for (std::size_t k = 0; k < neq; ++k) y[k] = yout[k + 1];
245 out.t.push_back(t);
246 out.y.push_back(y);
247 if (istate < 0) { // stop at the first failure; the caller decides what to do
248 out.success = false;
249 break;
250 }
251 istate = 2; // continue the same integration at the next output time
252 }
253
254 out.istate = istate;
255 out.steps = solver.get_nst();
256 out.f_evals = solver.get_nfe();
257 out.jacobians = solver.get_nje();
258 out.method = solver.get_mused() == 2 ? "bdf" : "adams";
259 return out;
260}
261
262/**
263 * One internal step at a time: ODEPACK's itask = 2.
264 *
265 * WHY THIS EXISTS. `lsoda_integrate` above reports the state only at the output
266 * times it was handed, and the integrator is free to do whatever it likes in
267 * between. MATLAB's `odeset('NonNegative')` is not a property of the output
268 * grid, it is a rule the STEP CONTROLLER applies to every accepted step -- it
269 * charges a negative excursion as error, and it clips the accepted state and
270 * resets the divided-difference table. Reproducing that needs the trajectory
271 * one accepted step at a time, which is what this exposes; it is the analogue
272 * of scipy's `LSODA.step()`, which the native-Python port drives for the same
273 * reason.
274 *
275 * THE RESTART IS THE HISTORY RESET. There is no way to reach into the vendored
276 * solver's Nordsieck array and rewrite it, and no need to: constructing a fresh
277 * stepper from a modified state is exactly what resetting the difference table
278 * accomplishes, because a first call (istate = 1) rebuilds the history from the
279 * initial state alone. The caller therefore expresses "clip and reset" by
280 * discarding the stepper and building another one.
281 *
282 * `step()` may carry `t()` PAST `t1`, as itask = 2 always may; `settle_at_end`
283 * interpolates back onto `t1` through the same history, which is what itask = 1
284 * does on a continuation call.
285 */
287 public:
288 LsodaStepper(const LsodaRhs& f, const std::vector<double>& y0, double t0, double t1,
289 const LsodaOptions& opt = LsodaOptions())
290 : f_(f), y_(y0), t_(t0), t1_(t1), neq_(y0.size()) {
291 if (y0.empty()) throw InputError("LsodaStepper: the initial state is empty");
292 if (!(t1 > t0))
293 throw InputError("LsodaStepper: the final time must exceed the initial time");
294 const bool rvec = !opt.rtol_vec.empty(), avec = !opt.atol_vec.empty();
295 if (rvec && opt.rtol_vec.size() != neq_)
296 throw InputError("LsodaStepper: rtol_vec has one entry per equation or none");
297 if (avec && opt.atol_vec.size() != neq_)
298 throw InputError("LsodaStepper: atol_vec has one entry per equation or none");
299 std::vector<double> rtol1(neq_ + 1, opt.rtol), atol1(neq_ + 1, opt.atol);
300 rtol1[0] = 0.0;
301 atol1[0] = 0.0;
302 for (std::size_t k = 0; k < neq_; ++k) {
303 if (rvec) rtol1[k + 1] = opt.rtol_vec[k];
304 if (avec) atol1[k + 1] = opt.atol_vec[k];
305 }
306 solver_.set_tolerances(rtol1, atol1, rvec ? (avec ? 4 : 3) : (avec ? 2 : 1));
307 solver_.set_force_stiff(opt.force_stiff);
308 iworks_ = {{0, 0, 0, static_cast<int>(opt.max_steps), 0, opt.max_order_nonstiff,
309 opt.max_order_stiff}};
310 rworks_ = {{0.0, opt.h_init, opt.h_max, opt.h_min}};
311 }
312
313 /** Advance one accepted step. False once the horizon is reached or LSODA gave up. */
314 bool step() {
315 if (done_ || istate_ < 0) return false;
316 call(2, t1_);
317 if (istate_ < 0) return false;
318 if (t_ >= t1_) done_ = true;
319 return true;
320 }
321
322 /** Interpolate the state back onto t1 after itask = 2 stepped past it. */
324 if (istate_ < 0 || !(t_ > t1_)) return;
325 call(1, t1_);
326 }
327
328 double t() const { return t_; }
329 double t_end() const { return t1_; }
330 const std::vector<double>& y() const { return y_; }
331 int istate() const { return istate_; }
332 bool failed() const { return istate_ < 0; }
333 bool finished() const { return done_; }
334 std::size_t steps() const { return solver_.get_nst(); }
335 std::size_t f_evals() const { return solver_.get_nfe(); }
336
337 private:
338 void call(int itask, double tout) {
339 std::vector<double> yout(neq_ + 1, 0.0);
340 for (std::size_t k = 0; k < neq_; ++k) yout[k + 1] = y_[k];
341 solver_.lsoda(lsoda_detail::rhs_trampoline, neq_, yout, &t_, tout, itask, &istate_, 1 /*iopt*/,
342 2 /*jt*/, iworks_, rworks_, &f_);
343 for (std::size_t k = 0; k < neq_; ++k) y_[k] = yout[k + 1];
344 if (istate_ > 0) istate_ = 2; // continue the same integration on the next call
345 }
346
347 LsodaRhs f_; ///< held by value: a driver builds steppers from temporaries
348 lsoda_impl::LSODA solver_;
349 std::vector<double> y_;
350 double t_ = 0.0;
351 double t1_ = 0.0;
352 std::size_t neq_ = 0;
353 int istate_ = 1;
354 bool done_ = false;
355 std::array<int, 7> iworks_ = {{0, 0, 0, 0, 0, 12, 5}};
356 std::array<double, 4> rworks_ = {{0.0, 0.0, 0.0, 0.0}};
357};
358
359/**
360 * `lsoda_integrate` on the same output grid, driven one accepted step at a
361 * time so `LsodaOptions::step_stop` can end a window early.
362 *
363 * ONE STEPPER PER OUTPUT INTERVAL, and that is what keeps the grid honest.
364 * itask = 2 may carry `t` PAST the interval's end, so each interval is stepped
365 * until the stepper reports it is done and then `settle_at_end` interpolates
366 * back onto the requested instant through the same history -- exactly what a
367 * continuation call with itask = 1 does. The restart between intervals costs a
368 * little accuracy against the single itask = 1 call, which is why this driver is
369 * NOT the default: only a caller that asked for a stop test pays for it, and the
370 * fluid windows that do hand over a two-entry grid, i.e. one interval.
371 *
372 * WHEN THE TEST FIRES the state is held for the WHOLE remaining grid rather than
373 * the trajectory being truncated. The test says the drift is zero to double
374 * precision, so y is that state for every later t and the held values are exact,
375 * not padded: a caller reading `final_state()` or a transient grid cannot tell
376 * this window from one that was stepped to its end, which is the point -- an
377 * early return must not read as a failure to any of them.
378 */
379inline LsodaSolution lsoda_integrate_stepwise(const LsodaRhs& f, const std::vector<double>& y0,
380 const std::vector<double>& t_eval,
381 const LsodaOptions& opt) {
382 LsodaSolution out;
383 out.t.push_back(t_eval[0]);
384 out.y.push_back(y0);
385
386 std::vector<double> y = y0;
387 double t = t_eval[0];
388 std::size_t nst = 0, nfe = 0;
389 bool stopped = false;
390
391 for (std::size_t i = 1; i < t_eval.size(); ++i) {
392 const double tout = t_eval[i];
393 if (stopped || tout == t) { // settled, or a repeated instant: report again
394 out.t.push_back(tout);
395 out.y.push_back(y);
396 continue;
397 }
398 LsodaStepper stepper(f, y, t, tout, opt);
399 while (stepper.step()) {
400 if (opt.step_stop(stepper.t(), stepper.y())) {
401 y = stepper.y();
402 t = stepper.t();
403 stopped = true;
404 break;
405 }
406 }
407 nst += stepper.steps();
408 nfe += stepper.f_evals();
409 if (stepper.failed()) {
410 out.istate = stepper.istate();
411 out.success = false;
412 out.t.push_back(stepper.t());
413 out.y.push_back(stepper.y());
414 break;
415 }
416 if (!stopped) {
417 stepper.settle_at_end();
418 y = stepper.y();
419 t = stepper.t_end();
420 }
421 out.t.push_back(tout);
422 out.y.push_back(y);
423 }
424
425 out.steps = nst;
426 out.f_evals = nfe;
427 // The stepper exposes no nje, and a caller reading it off a stopped window
428 // would be reading a count this driver never collected: leave it at 0 rather
429 // than report a number that is not the Jacobian count.
430 out.jacobians = 0;
431 return out;
432}
433
434/** Convenience form: integrate from t0 to t1 and report only the end state. */
435inline std::vector<double> lsoda_final(const LsodaRhs& f, const std::vector<double>& y0, double t0,
436 double t1, const LsodaOptions& opt = LsodaOptions()) {
437 const std::vector<double> span{t0, t1};
438 return lsoda_integrate(f, y0, span, opt).final_state();
439}
440
441} // namespace line
442
443#endif // LINE_UTIL_LSODA_H
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
InputError(const std::string &what)
Definition error.h:39
One internal step at a time: ODEPACK's itask = 2.
Definition lsoda.h:286
std::size_t f_evals() const
Definition lsoda.h:335
bool step()
Advance one accepted step.
Definition lsoda.h:314
std::size_t steps() const
Definition lsoda.h:334
void settle_at_end()
Interpolate the state back onto t1 after itask = 2 stepped past it.
Definition lsoda.h:323
LsodaStepper(const LsodaRhs &f, const std::vector< double > &y0, double t0, double t1, const LsodaOptions &opt=LsodaOptions())
Definition lsoda.h:288
int istate() const
Definition lsoda.h:331
double t() const
Definition lsoda.h:328
const std::vector< double > & y() const
Definition lsoda.h:330
bool finished() const
Definition lsoda.h:333
bool failed() const
Definition lsoda.h:332
double t_end() const
Definition lsoda.h:329
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
std::function< void(double t, const double *y, double *dydt)> LsodaRhs
The right-hand side dy/dt = f(t, y).
Definition lsoda.h:54
std::vector< double > lsoda_final(const LsodaRhs &f, const std::vector< double > &y0, double t0, double t1, const LsodaOptions &opt=LsodaOptions())
Convenience form: integrate from t0 to t1 and report only the end state.
Definition lsoda.h:435
LsodaSolution lsoda_integrate(const LsodaRhs &f, const std::vector< double > &y0, const std::vector< double > &t_eval, const LsodaOptions &opt=LsodaOptions())
Definition lsoda.h:178
LsodaSolution lsoda_integrate_stepwise(const LsodaRhs &f, const std::vector< double > &y0, const std::vector< double > &t_eval, const LsodaOptions &opt)
Integrate dy/dt = f(t, y) from t_eval.front() through every later entry of t_eval,...
Definition lsoda.h:379
Integration controls.
Definition lsoda.h:64
std::vector< double > rtol_vec
Per-component tolerances.
Definition lsoda.h:74
std::size_t max_steps
internal steps between output points
Definition lsoda.h:79
int max_order_stiff
BDF order cap (mxords).
Definition lsoda.h:81
double h_init
initial step; 0 lets LSODA choose
Definition lsoda.h:76
double atol
absolute tolerance, applied to every component
Definition lsoda.h:66
int max_order_nonstiff
Adams order cap (mxordn).
Definition lsoda.h:80
double h_min
smallest admissible step; 0 means no bound
Definition lsoda.h:77
double rtol
relative tolerance, applied to every component
Definition lsoda.h:65
double h_max
largest admissible step; 0 means no bound
Definition lsoda.h:78
std::function< bool(double, const std::vector< double > &)> step_stop
A test consulted after every ACCEPTED STEP, ending the integration when it returns true and holding t...
Definition lsoda.h:122
bool force_stiff
Start on BDF and never switch to Adams.
Definition lsoda.h:105
std::vector< double > atol_vec
Definition lsoda.h:75
Result of an integration, mirroring OdeSolution in ode.h.
Definition lsoda.h:126
std::size_t f_evals
right-hand side evaluations (nfe)
Definition lsoda.h:130
bool success
false when LSODA returned istate < 0
Definition lsoda.h:132
const std::vector< double > & final_state() const
Definition lsoda.h:137
int istate
the solver's final istate
Definition lsoda.h:133
std::vector< std::vector< double > > y
y[i] is the state at t[i]
Definition lsoda.h:128
std::size_t jacobians
Jacobian evaluations (nje).
Definition lsoda.h:131
std::vector< double > t
output times, t[0] = t_eval[0]
Definition lsoda.h:127
double final_time() const
Definition lsoda.h:141
std::size_t steps
internal steps taken (nst)
Definition lsoda.h:129
std::string method
The method in force at the end: "adams" (nonstiff) or "bdf" (stiff).
Definition lsoda.h:135