8.4.614-stable Switch to dev

Simulation

Simulation is what worlds were designed for. A world is a change scenario: a set of alternative values for many nodes of the graph, written on top of the real data without touching it. Most of the time, these changes are new points in nodeTime series: the predicted future of the system under one hypothesis. Running your functions inside the world then computes a conditional future: what the system would do if the hypothesis held.

 main   ──●──●──●──●──●──●  (measured until now)
                         │
 slow                    └──○──○──○──○   8 knots, eco mode
 normal                  └──○──○──○──○   10 knots
 fast                    └──○──○──○──○   12 knots
                          predicted future, one per world

A first scenario

The example uses the vessel model of the overview. Main holds the measured speed and fuel rate until 11:00. The function below writes the next hours of a plan in the current world and returns its KPIs:

type Kpi {
    world: String;
    /// litres
    fuel: float;
    hours: float;
}

/// Writes the next `hours` of the vessel at `speed` knots, in the current world.
fn plan(speed: float, hours: int): Kpi {
    var v = vessels.get("trawler")!!;
    var t = v.speed.lastTime()!!;
    var fuel = 0.0;
    for (var h = 1; h <= hours; h++) {
        var rate = 300.0 * (speed / 10.0) * (speed / 10.0);
        v.speed.setAt(t + h * 1hour, speed);
        v.fuel_rate.setAt(t + h * 1hour, rate);
        fuel = fuel + rate;
    }
    if (speed < 9.0) {
        v.config.set(EngineConfig { mode: "eco", max_rpm: 1200 });
    }
    return Kpi { world: World::current(), fuel: fuel, hours: hours as float };
}

@expose
fn fork(name: String) {
    World::create(name, WorldMode::overlay, null);
}

@expose
fn run_plan(speed: float): Kpi {
    return plan(speed, 4);
}

Fork a world, then run the plan in it:

curl -X POST -d '["slow"]' http://localhost:8080/project::fork
curl -H "world: slow" -X POST -d '[8.0]' http://localhost:8080/project::run_plan
# {"world":"slow","fuel":768.0,"hours":4.0}

In slow, the vessel’s series now run until 15:00 and its engine is in eco mode. In main, nothing changed: the series still stop at 11:00, the engine is still diesel. The two coexist, and any function, chart or API call shows one or the other depending on the world header.

Nothing in plan is specific to worlds. This is the point: the same code computes the present in main and the possible futures in the worlds.

Calling the models

The functions that predict the future are the models of your system: physical models, machine learning models, simulators. They run inside the world like any other function, so their inputs are the world’s data and their outputs are written to the world:

  • models written in GCL run as they are
  • Python models run in the server through the python library, a bodyless native fn implemented in project.py
  • FMU co-simulation models run through the fmi library
  • a remote model is called over HTTP from the world’s task, and its answer is written to the world

Live data and the scenario

An overlay world reads its base for every node it has not modified. The base’s later commits keep reaching those nodes. A node the world modified stays as the world left it.

This matters for time series. Once slow wrote its predicted points into v.speed, that series belongs to the world: the measurements main receives afterwards for v.speed do not show in slow. Every series the scenario did not write to, a sea state series for instance, keeps following main.

Two designs follow from this:

  • Predict into the measured series, as plan does. The world shows one continuous series, measured then predicted, and the World Diff compares the prediction with the measurements as they arrive.
  • Predict into series of their own (predicted_speed next to speed). The world then keeps seeing every new measurement, and a function running in the world compares the two series directly.

Use a snapshot world instead when the simulation must see main exactly as it was when it started, whatever arrives afterwards.

Scenarios of scenarios

A world can be based on another world. Use it to layer hypotheses:

// a hardware change the vessel does not have yet
World::create("battery-400kwh", WorldMode::overlay, null);
// ... a task in "battery-400kwh" changes the powertrain configuration ...

// routes evaluated on top of that hardware
World::create("battery-400kwh.route-north", WorldMode::overlay, "battery-400kwh");
World::create("battery-400kwh.route-south", WorldMode::overlay, "battery-400kwh");

Each route world sees the battery world’s changes and adds its own. A chain holds at most 8 overlay worlds. A world that is the base of another cannot be removed before it.

Re-running a scenario

World::reset(name) empties a world of what its tasks wrote and starts it again from its base as the base is now. The world keeps its name and id, the users allowed to use it, and the worlds based on it stay valid. A periodic simulation resets the same worlds every cycle instead of removing and creating them:

fn cycle() {
    for (_, w in World::list()) {
        if (w.name.startsWith("forecast-")) {
            World::reset(w.name);
        }
    }
    // ... spawn the evaluation of each forecast world ...
}

Keeping the results

Worlds cannot be merged back into main. A scenario leaves its results in one of three ways:

Way When
The function evaluated in the world returns its KPIs, and the caller stores them in main Search: only the scores travel back (see Scenario Search)
The world is kept and shared Others explore the scenario with the usual API (see Namespaces)
The scenario’s predicted series are written to main as data, a Scenario record with its own nodeTime The scenario must outlive its world, to be compared with reality for months

In every case, World Diff gives the exact list of what the scenario changed.