Skip to content
Get started

4. Ordinary differential equations

An ordinary differential equation relates quantities to derivatives with respect to one independent variable, here time. A first-order initial-value problem needs both an evolution relation and an initial state. Without the initial value, the relation describes a family of trajectories rather than one trajectory.

After this chapter, you should be able to:

  • identify state, parameter, derivative relation, and initial value;
  • derive the closed form for constant-rate scalar decay;
  • keep the time method and output schedule outside the Model;
  • execute the example with the installed Python package and select a series by its exact FieldRef; and
  • diagnose a physically wrong sign that remains dimensionally consistent.

Let dimensionless state x(t)x(t) decay with constant rate k>0k>0:

dxdt+kx=0,x(0)=1.\frac{\mathrm{d}x}{\mathrm{d}t} + kx = 0, \qquad x(0)=1.

Separating variables and integrating from 00 to tt gives

1x(t)1xdx=0tkdt,logx(t)=kt,\int_1^{x(t)}\frac{1}{x}\,\mathrm{d}x =-\int_0^t k\,\mathrm{d}t, \qquad \log x(t)=-kt,

so the independently stated comparison is

x(t)=exp(kt).x(t)=\exp(-kt).

The executable lesson uses k=1s1k=1\,\mathrm{s}^{-1} and observes t=0.25t=0.25, 0.50.5, and 1s1\,\mathrm{s}. Those choices bound this lesson; they do not define general ODE support.

decay.eqi
// A minimal implicit ODE: x decays at the rate supplied by the user.
model decay() {
// `x` is dimensionless and starts at 1. `rate` has inverse-time units.
state x: 1;
initial { x = 1; }
parameter rate: 1 / s = 1;
// Eqiora writes the evolution law as a residual equal to zero.
relation flow {
derivative(x) + rate * x = 0;
}
}

The initial value and rate belong to the Model. The adaptive method, its tolerances, the final time, and requested observations belong to the Realization or Run.

Follow Get started to install the source build and download the same decay.eqi and run.py used here. Set rate to 1, then run from their folder:

Terminal windowconsole
$ uv run --no-project --python .venv/bin/python python run.py

The complete script uses the public compile → resolve → run path:

run.py
"""Run the decay model shared by Get started and the ODE lesson."""
from __future__ import annotations
import argparse
from pathlib import Path
import eqiora
OUTPUT_TIMES_S = (0.25, 0.5, 1.0)
def solve(source_path: Path) -> tuple[tuple[float, float], ...]:
"""Return the requested (time in seconds, dimensionless value) samples."""
model = eqiora.compile(path=source_path)
field = model.field(model.field_ids[0])
plan = eqiora.resolve(
model,
temporal=eqiora.time.Tsitouras45(
initial_step_s=0.01,
relative_tolerance=1.0e-9,
absolute_tolerances={field: 1.0e-11},
),
)
result = eqiora.run(
plan,
state=eqiora.State.initial(plan),
until_s=OUTPUT_TIMES_S[-1],
output_times_s=OUTPUT_TIMES_S,
)
series = result.series(field)
times = series.time.numpy()
values = series.values.numpy()
return tuple(
(float(time_s), float(value))
for time_s, value in zip(times, values, strict=True)
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("model", nargs="?", type=Path, default=Path("decay.eqi"))
args = parser.parse_args()
for time_s, value in solve(args.model):
print(f"t={time_s:.2f}, x={value:.10f}")
if __name__ == "__main__":
main()

View the maintained run script.

The program prints the time and computed value. Compare each value with exp(t)\exp(-t) independently. The focused installed-package test requires the absolute difference to remain at most 2×1082\times10^{-8} for exactly these three samples. That is adequate for this lesson; it is not a tolerance for a different model, interval, backend, or method.

The Result owns an immutable series selected by the exact field handle. This chapter observes only three scalar values. It does not inspect internal adaptive steps, infer values between requested times, or treat plotting choices as model meaning.

The closed form supplies the comparison independently of the program’s output. If the comparison fails, the expected expression is not replaced with the computed values; the model, realization, execution, and test are investigated.

Deliberate failure: the wrong sign still compiles

Section titled “Deliberate failure: the wrong sign still compiles”

Change the residual to:

eqiora
relation flow {
derivative(x) - rate * x = 0;
}

Both terms still have inverse-time dimension, so compilation can succeed. But the relation now implies x(t)=exp(+kt)x(t)=\exp(+kt): growth instead of decay. Comparing it with the pre-stated decay solution must fail. The repair is to revisit the physical assumption and residual sign, not to loosen the comparison tolerance.

  1. Derive the solution for an arbitrary positive initial value x(0)=x0x(0)=x_0.
  2. Predict the three observed values when rate is 2s12\,\mathrm{s}^{-1}, then state which part of the source and which comparison expression must change.
  3. Run the wrong-sign mutant. Explain why dimensional checking accepts it and the closed-form comparison rejects it.
  4. Add one requested output time without changing the Model. Explain why this changes a Run request rather than mathematical meaning.
  5. List two reasons the three passing samples do not establish a convergence rate or suitability for stiff systems.

Previous: Algebraic relations and networks · Back to the series map · Next: Fields and spatial domains