58.16 ODE Trajectories and Streamlines
Use ode-flow-position for a direct fixed-step calculation, prepare-ode-trajectory for repeated time lookup, and streamline or flow-particle for scene content. Fixed RK4 checkpoints and adaptive RK45 dense output have different preparation and lookup behavior, described below. Ordinary function and data plots are in Function, Parametric, and Data Plots.
See also Function, Parametric, and Data Plots, Relations and Live Layout.
58.16.1 Deterministic ODE Flow and Streamlines
The ODE operations turn a two-dimensional vector field into reproducible integral-curve geometry. A two-argument field is autonomous; a three-argument field receives time first. Fixed-step fourth-order Runge–Kutta (RK4) is the default, with the caller selecting the step size and number of streamline steps. Neither the direct numerical solver nor a prepared trajectory depends on a prior rendered frame. prepare-ode-trajectory stores immutable canonical RK4 checkpoints, so an animated particle does not recompute the complete seed-to-time prefix for every frame. The renderer batches selected frame times by checkpoint interval, sharing each interval’s full RK4 suffix steps.
An optional deterministic adaptive Dormand–Prince 5(4) backend is also available. It stores accepted endpoint derivatives for cubic Hermite dense lookup, accepts time-dependent fields, can stop at one scalar sign-crossing event, and records immutable diagnostics. Once an adaptive trajectory is prepared, dense lookup and frame rendering never invoke the author field.
Both trajectory families express their numerical operations through an immutable ode-state-space. The built-in real-ode-state-space, vec2-ode-state-space, and vec3-ode-state-space values make the shared RK4 and RK45 algorithms explicit; (numeric-vector-ode-state-space n) creates a fixed-length immutable numeric-vector state space. These values are useful when building a numerical extension, while the ordinary two-dimensional and spatial trajectory constructors remain the author-facing API.
58.16.1.1 Solver State Spaces
struct
(struct ode-state-space ( dimension add subtract scale norm interpolate finite?) #:transparent) dimension : exact-positive-integer? add : procedure? subtract : procedure? scale : procedure? norm : procedure? interpolate : procedure? finite? : procedure?
procedure
(numeric-vector-ode-state-space dimension) → ode-state-space?
dimension : exact-positive-integer?
58.16.1.2 Direct Fixed-Step Integration
procedure
(ode-flow-position field seed time [ #:step-size step-size]) → vec2?
field :
(or/c (procedure-arity-includes/c 2) (procedure-arity-includes/c 3)) seed : vec2? time : finite-real? step-size : (and/c finite-real? positive?) = 1/20
The final partial step is included, so the requested time is reached exactly in ordinary arithmetic rather than rounded to a step-grid endpoint. This is a fixed-step solver, not an adaptive tolerance-controlled integrator.
A constant field gives uniform motion from the seed.
58.16.1.3 Adaptive Configuration and Events
procedure
(adaptive-rk45 [ #:relative-tolerance relative-tolerance #:absolute-tolerance absolute-tolerance #:initial-step initial-step #:minimum-step minimum-step #:maximum-step maximum-step #:maximum-steps maximum-steps]) → adaptive-rk45? relative-tolerance : (and/c finite-real? positive?) = 1e-6 absolute-tolerance : (and/c finite-real? positive?) = 1e-8 initial-step : (and/c finite-real? positive?) = 1/10 minimum-step : (and/c finite-real? positive?) = 1e-8 maximum-step : (and/c finite-real? positive?) = 1 maximum-steps : exact-positive-integer? = 100000
procedure
(adaptive-rk45? value) → boolean?
value : any/c
procedure
(ode-event function [ #:direction direction #:name name]) → ode-event?
function :
(or/c (procedure-arity-includes/c 2) (procedure-arity-includes/c 3)) direction : (or/c 'any 'increasing 'decreasing) = 'any name : symbol? = 'event
procedure
(ode-event? value) → boolean?
value : any/c
58.16.1.4 Prepared Trajectories
procedure
(ode-trajectory? value) → boolean?
value : any/c
procedure
(prepare-ode-trajectory field seed #:time-range time-range [ #:step-size step-size #:checkpoint-every checkpoint-every #:solver solver #:event event]) → ode-trajectory?
field :
(or/c (procedure-arity-includes/c 2) (procedure-arity-includes/c 3)) seed : vec2? time-range : (cons/c finite-real? finite-real?) step-size : (and/c finite-real? positive?) = 1/20 checkpoint-every : exact-positive-integer? = 16 solver : (or/c false/c adaptive-rk45?) = #f event : (or/c false/c ode-event?) = #f
For a lookup, the trajectory begins at the preceding checkpoint between zero and the requested time, takes fewer than checkpoint-every full steps, and then takes the usual final remainder step. It therefore preserves the fixed-RK4 numerical meaning of ode-flow-position without repeating a long prefix for each frame.
The field must be pure and stable for the lifetime of the prepared value. The library cannot determine whether an arbitrary Racket procedure’s captured state has changed.
When solver is #f, this is the established fixed-RK4 checkpoint trajectory. event is then rejected. With an adaptive-rk45? value, accepted Dormand–Prince endpoint positions and derivatives are stored instead. event, when supplied, truncates the actual supported range at its dense scalar root.
procedure
(ode-trajectory-time-range trajectory)
→ (cons/c finite-real? finite-real?) trajectory : ode-trajectory?
procedure
(ode-trajectory-step-size trajectory)
→ (or/c (and/c finite-real? positive?) false/c) trajectory : ode-trajectory?
procedure
(ode-trajectory-checkpoint-every trajectory)
→ (or/c exact-positive-integer? false/c) trajectory : ode-trajectory?
procedure
(ode-trajectory-solver trajectory)
→ (or/c 'fixed-rk4 adaptive-rk45?) trajectory : ode-trajectory?
procedure
(ode-trajectory-diagnostics trajectory)
→ (or/c false/c ode-trajectory-diagnostics?) trajectory : ode-trajectory?
procedure
(ode-trajectory-diagnostics? value) → boolean?
value : any/c
procedure
(ode-trajectory-position trajectory time) → vec2?
trajectory : ode-trajectory? time : finite-real?
58.16.1.5 Streamlines and Flow Particles
procedure
(streamline-points field seed [ #:direction direction #:step-size step-size #:steps steps]) → (listof vec2?)
field :
(or/c (procedure-arity-includes/c 2) (procedure-arity-includes/c 3)) seed : vec2? direction : (or/c 'forward 'backward 'both) = 'both step-size : (and/c finite-real? positive?) = 1/20 steps : exact-positive-integer? = 120
procedure
(streamline axes field seed #:id id [ #:direction direction #:step-size step-size #:steps steps #:opacity opacity #:stroke stroke #:stroke-width stroke-width]) → path-visual? axes : axes-visual?
field :
(or/c (procedure-arity-includes/c 2) (procedure-arity-includes/c 3)) seed : vec2? id : symbol? direction : (or/c 'forward 'backward 'both) = 'both step-size : (and/c finite-real? positive?) = 1/20 steps : exact-positive-integer? = 120 opacity : opacity? = 1 stroke : any/c = "royalblue" stroke-width : (and/c finite-real? (>=/c 0)) = 2
procedure
(streamlines axes field seeds #:id id [ #:direction direction #:step-size step-size #:steps steps #:opacity opacity #:stroke stroke #:stroke-width stroke-width]) → group-visual? axes : axes-visual?
field :
(or/c (procedure-arity-includes/c 2) (procedure-arity-includes/c 3)) seeds : (listof vec2?) id : symbol? direction : (or/c 'forward 'backward 'both) = 'both step-size : (and/c finite-real? positive?) = 1/20 steps : exact-positive-integer? = 120 opacity : opacity? = 1 stroke : any/c = "royalblue" stroke-width : (and/c finite-real? (>=/c 0)) = 2
procedure
(flow-particle axes trajectory phase #:id id [ #:shape shape #:size size #:fill fill #:stroke stroke #:stroke-width stroke-width #:opacity opacity]) → derived-visual? axes : axes-visual? trajectory : ode-trajectory? phase : (or/c symbol? scene-parameter?) id : symbol? shape : point-marker-shape? = 'circle size : (and/c finite-real? positive?) = 1/5 fill : any/c = "crimson" stroke : any/c = "black" stroke-width : (and/c finite-real? (>=/c 0)) = 1 opacity : opacity? = 1
Before render-frames! creates frame workers, it samples all requested phase values and freezes the corresponding particle coordinates in an immutable table. The preparation pass walks each used checkpoint interval once, sharing full RK4 suffix steps among its selected times. Workers only read those coordinates; they never call the author field. Direct arbitrary-time scene sampling remains deterministic through ode-trajectory-position.
For an adaptive trajectory, the preparation pass instead reads its stored dense output directly; no numerical integration or field call occurs after the trajectory has been prepared.