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
Optionsand follow themethodOptions(...)path. - Compare this tiny example against the real listeners used in shipped examples.
- Trace how measurements would enter without changing the
Evolutionnoun.
API Reference Jump
See flux.evolution.Evolution and
flux.listeners.