4. Ordinary differential equations
State makes history matter
Section titled “State makes history matter”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.
Learning outcomes
Section titled “Learning outcomes”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;
- run the example with the installed Python package and select a series by
its
FieldRef; and - diagnose a physically wrong sign that remains dimensionally consistent.
From a rate of cooling to a trajectory
Section titled “From a rate of cooling to a trajectory”Return to the bead in chapter 1. Its heat capacity stores energy, and its conductance transfers energy to a room at fixed . The balance predicts a rate, but our thermometer asks for a temperature at a later time. Integrating the rate equation bridges those questions. Before calculating, predict three features: the bead stays between its initial temperature and room temperature, the difference decreases monotonically, and the cooling slows with time.
Normalize with and let . The resulting source contains just one state, one rate, and one initial value. It also describes other first-order relaxation systems after the physical quantities have been mapped correctly. A shared equation does not make their parameters interchangeable.
Derive the decay equation
Section titled “Derive the decay equation”Let dimensionless state decay with constant rate :
Separating variables and integrating from to gives
so the solution to compare against is
The executable lesson uses and observes , , and . They let us compare the trajectory at a quarter, half, and one characteristic time.
What the solution tells us before a run
Section titled “What the solution tells us before a run”The characteristic time is . At , the remaining fraction is , independent of the unit used to express time. The half-life is found from , hence . Repeated equal time intervals multiply by the same factor; they do not subtract equal amounts.
For , the analytic values are:
| Time (s) | Interpretation | |
|---|---|---|
| 0.25 | 0.7788007831 | about 78% remains |
| 0.50 | 0.6065306597 | about 61% remains |
| 1.00 | 0.3678794412 | one characteristic time |
These numbers come from evaluating the analytic solution, not reading a solver trace. The curve satisfies both and ; checking the initial value matters because satisfies the same differential equation.
Recover physical temperature using
. With the illustrative bead’s
, , and ,
the predicted temperature after 100 seconds is about .
Changing to this rate requires changing rate to 0.01; the default script’s
one-second window then covers only one hundredth of a characteristic time.
Model source
Section titled “Model source”// 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.
Execute the model
Section titled “Execute the model”Follow Get started to install the published package and download the same
decay.eqi and run.py used here. Set rate to 1, then run from their folder:
$ uv run python run.pyThe script compiles, resolves, and runs the model:
"""Run the decay model shared by Get started and the ODE lesson."""
from __future__ import annotations
import argparsefrom 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, 0, 0, False): 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()The program prints each time and computed value. With these settings, each value should be within of , far tighter than the rounded percentages in the table. If you change the model, the interval, or the integration settings, recompute the expected values and reconsider the tolerance.
Read the result
Section titled “Read the result”The result series, selected with the field handle, contains the three requested output times; the solver may take additional internal steps between them.
If the values differ from the closed form, inspect the equation sign, initial value, rate, and solver settings.
What can we reuse?
Section titled “What can we reuse?”decay.eqi is already the complete mathematical description for
this small problem. There is no need to hide a one-line evolution law behind a
new component. Reuse its source and run script, then open their definitions when
you need to change the law. In the circuits path,
resistors and sources are larger reusable physical parts, and imports remove
repetition after their equations have been derived.
Change an assumption before changing a solver
Section titled “Change an assumption before changing a solver”Suppose the room temperature becomes . The energy balance remains , but the old normalization no longer produces unforced decay. For ,
This follows by differentiating the temperature difference. It explains why a varying ambient temperature needs a forcing term: cooling a difference and cooling an absolute temperature are different statements when the reference moves. Work this extension on paper before editing source.
By contrast, recording a fourth output time leaves the differential equation unchanged. Tightening an integrator tolerance changes how accurately the trajectory is approximated. The numerical-simulation path examines that distinction through time-step error and convergence.
Deliberate failure: the wrong sign still compiles
Section titled “Deliberate failure: the wrong sign still compiles”Change the residual to:
relation flow { derivative(x) - rate * x = 0;}Both terms still have inverse-time dimension, so compilation can succeed. But the relation now implies : growth instead of decay, so the comparison with the decay solution fails. Fix the sign by revisiting the physical assumption; loosening the tolerance would only hide the error.
Exercises
Section titled “Exercises”- Derive the solution for an arbitrary positive initial value .
- Predict the three observed values when
rateis , then state which part of the source and which comparison expression must change. - Run the wrong-sign mutant. Explain why dimensional checking accepts it and the closed-form comparison rejects it.
- Add one requested output time without changing the Model. Explain why this changes a Run request rather than mathematical meaning.
- List two reasons the three passing samples do not establish a convergence rate or suitability for stiff systems.
Exercise guidance
Section titled “Exercise guidance”Exercise 1 gives . For exercise 2, replace with in the comparison; the source’s rate and the comparison must describe the same model. For exercise 5, several samples on one smooth trajectory do not reveal how error changes as resolution changes. A stiff system introduces competing time scales that this scalar example does not contain.
Inverse question. Given two positive observations and , derive . Ask what happens if measurement noise makes the second value larger than the first. The inverse-problems path takes up the distinction between estimating a parameter and validating the assumed law.
Continue with stochastic evolution
Section titled “Continue with stochastic evolution”The stochastic-process notebook authors Ornstein–Uhlenbeck and geometric Brownian motion from scratch, then executes individual paths and computes typed ensemble moments. It separates sampling uncertainty from time-discretization bias and compares coupled GBM paths with the exact Ito solution. Every equation and execution step appears in the notebook; it requires the development checkout installed in its kernel.
References
Section titled “References”- Arthur Mattuck, Haynes Miller, Jeremy Orloff, and John Lewis, Differential Equations, MIT OpenCourseWare 18.03SC (2011), Basic DE’s and separable equations, especially “The Most Important DE” and “Separation of Variables.” The exponential and its initial-value interpretation underlie this chapter’s comparison.
Previous: Algebraic relations and networks · Back to the series map · Next: Fields and spatial domains