Skip to content

Step 5 — Evolution and Listeners

Capability

Configure `Evolution(System, Method)` directly and attach a listener that observes the run.

Problem Statement

Once there is a system worth stepping, the docs need to explain the execution nouns without regrowing a hidden runtime stepper abstraction.

Mathematical Idea

Evolution owns time-step policy and listeners. The system owns the PDE state. That split keeps execution mechanics small and prevents “solver class” from swallowing the model.

Flux Concepts

  • Evolution(...).config().dt(...).steps(...).listen(...).init(...) is the canonical construction path.
  • Listeners observe begin/step/end events without changing the system type.
  • Method-local behavior belongs to the chosen method family, not to an extra wrapper noun.

Minimal Runnable Snippet

Commented walkthrough:

const std = @import("std"); const flux = @import("flux"); const ToySystem = []f64; const ToyMethod = struct { pub fn initialize(_: std.mem.Allocator, system: ToySystem, _: f64) void { for (system, 0..) |*value, index| { value.* = @as(f64, @floatFromInt(index)); } } pub fn advance(_: std.mem.Allocator, system: ToySystem, dt: f64) !void { for (system) |*value| { value.* += dt; } } }; const CounterListener = struct { on_step_calls: u32 = 0, pub fn onStep(self: *@This(), _: anytype) !void { self.on_step_calls += 1; } }; pub fn run(allocator: std.mem.Allocator) !void { var storage = [_]f64{ 0.0, 0.0, 0.0 }; var listener = CounterListener{}; var evolution = try flux.evolution.Evolution(ToySystem, ToyMethod).config() .dt(0.25) .steps(4) .listen(&listener) .init(allocator, storage[0..]); defer evolution.deinit(); const result = try evolution.run(); try std.testing.expect(result.elapsed_s >= 0.0); try std.testing.expectEqual(@as(u32, 4), listener.on_step_calls); try std.testing.expectApproxEqAbs(1.0, storage[0], 1e-12); } test "step 05 commented snippet compiles and runs" { try run(std.testing.allocator); }

Plain program:

const std = @import("std"); const flux = @import("flux"); const ToyMethod = struct { pub fn initialize(_: std.mem.Allocator, system: []f64, _: f64) void { for (system, 0..) |*value, index| value.* = @as(f64, @floatFromInt(index)); } pub fn advance(_: std.mem.Allocator, system: []f64, dt: f64) !void { for (system) |*value| value.* += dt; } }; const CounterListener = struct { on_step_calls: u32 = 0, pub fn onStep(self: *@This(), _: anytype) !void { self.on_step_calls += 1; } }; pub fn run(allocator: std.mem.Allocator) !void { var storage = [_]f64{ 0.0, 0.0, 0.0 }; var listener = CounterListener{}; var evolution = try flux.evolution.Evolution([]f64, ToyMethod).config() .dt(0.25) .steps(4) .listen(&listener) .init(allocator, storage[0..]); defer evolution.deinit(); _ = try evolution.run(); try std.testing.expectEqual(@as(u32, 4), listener.on_step_calls); } test "step 05 plain snippet compiles and runs" { try run(std.testing.allocator); }

Expected Result

The tiny example uses a synthetic system slice instead of a full PDE, because the goal is to make the execution surface obvious. The listener counts step events, and the state changes by dt each step.

Possible Extensions

  • Swap the toy method for one with Options and follow the methodOptions(...) path.
  • Compare this tiny example against the real listeners used in shipped examples.
  • Trace how measurements would enter without changing the Evolution noun.

API Reference Jump

See flux.evolution.Evolution and flux.listeners.