In this page
World Diff
A world is only useful if you can tell what it changed. A scenario is described by its differences with the real data: the points it predicts, the configuration it changes, the values it disagrees on. GreyCat computes these differences natively, without any bookkeeping in your code:
| Tool | Answers | Level |
|---|---|---|
WorldChangesReader |
Which nodes did this world modify? | the graph |
WorldDiffReader |
Which keys of this node differ from the base, and how? | one node |
Both read the current world (the world the task runs in) against its base. Both are in the runtime module of std and are read like the file readers: can_read(), then read().
Use them to:
- show a user what a scenario changes before they adopt it
- explain the result of a search: why is this candidate on the Pareto front?
- summarise a scenario as KPI deltas
- measure the drift between a prediction and reality, as measurements arrive in main
- audit what a namespace or a sandbox changed
Which nodes changed: WorldChangesReader
var nodes = WorldChangesReader {};
while (nodes.can_read()) {
var n = nodes.read(); // a node<T>, nodeTime, nodeIndex, nodeList or nodeGeo
}
It lists each node the world modified, once, in node reference order. available() counts the nodes left to read, and pos the nodes already read.
Building the list costs nothing at commit time: the reader goes through the world’s commits table and compares block addresses with the base’s index. It looks only at the blocks the world wrote, so its cost follows the size of the scenario, not the size of the graph.
What changed in a node: WorldDiffReader
var diff = WorldDiffReader { node: n };
while (diff.can_read()) {
var d = diff.read(); // WorldDiff
}
Each WorldDiff is one key whose value differs between the world and its base:
| Field | Content |
|---|---|
kind |
WorldDiffKind::added (only the world holds the key), removed (only the base holds it) or changed (both hold it, with different values) |
key |
The time of a nodeTime, the position of a nodeList, the key of a nodeIndex, the geo of a nodeGeo; null for a node<T> |
base |
The base’s value, null when added |
world |
The world’s value, null when removed |
Keys come in the node’s order: by time for a nodeTime, by position for a nodeList, by encoded key for a nodeGeo and a nodeIndex with a scalar key, and by hash for a nodeIndex with any other key (String, objects). A key whose two values are deep-equal is never listed.
A complete diff
Put the two readers together to print everything a world changed:
@expose
fn show_diff() {
var nodes = WorldChangesReader {};
while (nodes.can_read()) {
var n = nodes.read();
println("node ${n}");
var diff = WorldDiffReader { node: n };
while (diff.can_read()) {
var d = diff.read();
println(" ${d.kind} ${d.key}: ${d.base} -> ${d.world}");
}
}
}
Run in the slow world of the Simulation page, which sailed four more hours at 8 knots in eco mode:
curl -H "world: slow" -X POST -d '[]' http://localhost:8080/project::show_diff
node 36864nodeTime
WorldDiffKind::added 2026-10-26T12:00:00Z: null -> 8.0
WorldDiffKind::added 2026-10-26T13:00:00Z: null -> 8.0
WorldDiffKind::added 2026-10-26T14:00:00Z: null -> 8.0
WorldDiffKind::added 2026-10-26T15:00:00Z: null -> 8.0
node 36865nodeTime
WorldDiffKind::added 2026-10-26T12:00:00Z: null -> 192.0
WorldDiffKind::added 2026-10-26T13:00:00Z: null -> 192.0
WorldDiffKind::added 2026-10-26T14:00:00Z: null -> 192.0
WorldDiffKind::added 2026-10-26T15:00:00Z: null -> 192.0
node 40960node
WorldDiffKind::changed null: EngineConfig{mode:"diesel",max_rpm:1500} -> EngineConfig{mode:"eco",max_rpm:1200}
The scenario is fully described: two series extended with predicted points, and one configuration changed. The vessels nodeIndex and the Vessel object are not listed, since the scenario did not modify them.
Naming the changed nodes
WorldChangesReader returns node handles. To present a diff to a user, map them back to your domain. Node handles compare by identity, so a Map keyed by node does it:
var v = vessels.get("trawler")!!;
var labels = Map<any, String> {};
labels.set(v.speed, "speed");
labels.set(v.fuel_rate, "fuel_rate");
labels.set(v.config, "config");
var nodes = WorldChangesReader {};
while (nodes.can_read()) {
var n = nodes.read();
var label = labels.get(n) ?? "${n}";
// ...
}
n == v.speed works too, for a single node.
Summarising a scenario
Most of the time, a user does not want every point but a summary: how many points the scenario predicts, and how much it changes. The function below turns the diff into one line per series:
/// One series of a scenario against the real data.
type SeriesDelta {
series: String;
/// points the scenario predicts beyond the data
added: int;
/// points where the scenario disagrees with the data
changed: int;
removed: int;
/// sum of the scenario's values minus the base's, over the differing points
delta: float;
}
/// What the current world changed in a vessel's series, one line per series.
@expose
fn scenario_summary(name: String): Array<SeriesDelta> {
var v = vessels.get(name)!!;
var labels = Map<any, String> {};
labels.set(v.speed, "speed");
labels.set(v.fuel_rate, "fuel_rate");
var out = Array<SeriesDelta> {};
var nodes = WorldChangesReader {};
while (nodes.can_read()) {
var n = nodes.read();
var label = labels.get(n);
if (label == null) {
continue; // not a series of this vessel
}
var s = SeriesDelta { series: label, added: 0, changed: 0, removed: 0, delta: 0.0 };
var diff = WorldDiffReader { node: n };
while (diff.can_read()) {
var d = diff.read();
if (d.kind == WorldDiffKind::added) {
s.added++;
s.delta = s.delta + (d.world as float);
} else if (d.kind == WorldDiffKind::removed) {
s.removed++;
s.delta = s.delta - (d.base as float);
} else {
s.changed++;
s.delta = s.delta + (d.world as float) - (d.base as float);
}
}
out.add(s);
}
return out;
}
curl -H "world: slow" -X POST -d '["trawler"]' http://localhost:8080/project::scenario_summary
# [{"series":"speed","added":4,"changed":0,"removed":0,"delta":32.0},
# {"series":"fuel_rate","added":4,"changed":0,"removed":0,"delta":768.0}]
Comparing scenarios
A task sees one world, so two scenarios are compared through their common base: compute the summary of each world against main, then compare the summaries. The client does it with two calls:
curl -H "world: slow" -X POST -d '["trawler"]' http://localhost:8080/project::scenario_summary
curl -H "world: normal" -X POST -d '["trawler"]' http://localhost:8080/project::scenario_summary
On the server, spawn the same function in each world with Job.world and collect the results, as Scenario Search does with KPIs.
For two worlds where one is based on the other (battery and battery.route-north), run the diff in the derived world: its base is the other scenario, so the diff shows exactly what the second hypothesis adds to the first.
Paging a large diff for a frontend
A diff on a large nodeIndex or a long series can hold many entries. WorldDiffReader.last holds the last entry read, and a new reader given it resumes after it. Expose a page-by-page API:
@expose
fn fuel_diff_page(last: WorldDiff?, size: int): Array<WorldDiff> {
var v = vessels.get("trawler")!!;
var diff = WorldDiffReader { node: v.fuel_rate, last: last };
var page = Array<WorldDiff> {};
while (diff.can_read() && page.size() < size) {
page.add(diff.read());
}
return page;
}
The client sends null first, then the last entry of each page to get the next one:
curl -H "world: slow" -X POST -d '[null, 2]' http://localhost:8080/project::fuel_diff_page
# [{"kind":"added","key":1793016000000000,"world":192.0},
# {"kind":"added","key":1793019600000000,"world":192.0}]
The resume is exact when neither the world nor its base committed between two pages. A reader whose task was suspended and restored resumes after last the same way. (WorldChangesReader resumes after pos nodes.)
Drift: prediction against reality
The base is read as it is now, not as it was when the world was created. In an overlay world, this turns the diff of a kept scenario into a drift detector: as the real measurements arrive in main for the hours the scenario predicted, the predicted points move from added to changed, with the measurement as base and the prediction as world.
Fork a world at 11:00 and predict four hours at 8 knots, as above. Then main receives two measurements:
// in main
v.speed.setAt(time::parse("2026-10-26T12:00:00Z", null), 7.2); // slower than predicted
v.speed.setAt(time::parse("2026-10-26T13:00:00Z", null), 8.0); // as predicted
The diff of v.speed in the scenario world now reads:
WorldDiffKind::changed 2026-10-26T12:00:00Z: 7.2 -> 8.0 (measured -> predicted: drift)
WorldDiffKind::added 2026-10-26T14:00:00Z: null -> 8.0 (not reached yet)
WorldDiffKind::added 2026-10-26T15:00:00Z: null -> 8.0
13:00 is not listed: the prediction was exact, the two values are deep-equal. The changed entries are exactly the errors of the prediction, and nothing has to be copied or aligned to compute them. Averaged over the scenarios a system keeps, these errors measure the accuracy of its digital twin.
Rules to know
- Committed data only. Both readers compare what was committed when the task started, not the task’s own pending writes. Write the scenario in one task and read its diff in another: a task that writes then reads the diff sees nothing.
- Not in main. Both fail in a task running in main, which has no base.
WorldDiffReaderalso fails whennodeis not a node. - Base read now. In a snapshot world, the base’s later changes show up as differences. In an overlay world, a node the world never modified follows its base and gives nothing; a node it modified shows the base’s later changes, which is what makes the drift above work.
- Rewritten trees.
WorldChangesReaderskips anode<T>set back to the base’s value, but lists anodeTime,nodeIndex,nodeListornodeGeowhose tree the world rewrote, even when it holds the base’s values again.WorldDiffReaderthen lists nothing for it. - New nodes. A node created in the world lists every key as
added. Anode<T>module variable exists in main from the start, holdingnull, so a world’s first set of it readschanged, withnullas the base value. - Standalone worlds have no base: every node is listed and every key is
added. - JSON. Over HTTP, a
timekey of anodeTimediff is sent as microseconds since the epoch.
Cost
A diff loads every internal block of the node once, and only the leaves that differ. It steps over the leaves both sides share at the same address. A few edits to a million-key index cost a few dozen blocks. In an overlay world, a node whose tree the world never wrote costs its head blocks only, whatever its size. A diff is cheap enough to run on every scenario of a search, or on every request of a dashboard.