8.4.614-stable Switch to dev

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. WorldDiffReader also fails when node is 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. WorldChangesReader skips a node<T> set back to the base’s value, but lists a nodeTime, nodeIndex, nodeList or nodeGeo whose tree the world rewrote, even when it holds the base’s values again. WorldDiffReader then lists nothing for it.
  • New nodes. A node created in the world lists every key as added. A node<T> module variable exists in main from the start, holding null, so a world’s first set of it reads changed, with null as the base value.
  • Standalone worlds have no base: every node is listed and every key is added.
  • JSON. Over HTTP, a time key of a nodeTime diff 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.