Skip to content

The engine protocol: mokili-step/2

How any engine — a simulator, a script, a solver, control code on a processor or a bench — takes part in a Mokili experiment. An engine is a program that reads one JSON object per line on its standard input and answers one per line on its standard output. Nothing else is required: no library, no compiled interface, no language.

This specification is licensed under the Apache License 2.0, apart from the rest of Mokili, so an engine may be written without reference to Mokili's licence.

Why not only FMI

FMI, the Modelica Association's standard, is how Mokili runs compiled models, and it stays the first choice where a tool exports an FMU. It asks a model to be a binary per platform, loaded into the caller's process through a C interface. That is fast, and it is also why a model built on one platform does not run on another, why a script or a model from another tool needs a C shim to take part, and why a crash in the model takes its caller down. mokili-step/2 takes the engine out of the process instead, and adds what an experiment needs and FMI does not say: a determinism contract, the code's own account of its decisions, and a record of every exchange. The two meet: meka serve-fmu serves any FMU over this protocol, and meka pack-fmu packs any engine of this protocol as an FMU.

Messages

Mokili sends; the engine answers each message with exactly one line.

describe

{"op": "describe"}
{"ok": "describe", "name": "Tank", "protocol": "mokili-step/2",
 "determinism": "deterministic", "capabilities": ["advance"],
 "parameters": [{"name": "area", "unit": "m2", "default": 1.0}],
 "inputs": [{"name": "pump_command", "unit": "1"}],
 "outputs": [{"name": "h", "unit": "m"}]}

  • determinism: deterministic (the same inputs give the same outputs), seeded (the same for the same seed), or external (it depends on something Mokili cannot rerun). Mokili refuses replications of a deterministic engine and records a seeded one without a seed as unseeded.
  • capabilities: advance (it steps), batch (it runs to the end in one advance and returns series), decisions (it reports them).
  • default: optional; a packed FMU takes it as the parameter's start value.

Mokili checks the answer against the model record: every declared parameter, input and output must be there, with the same unit, and the determinism must be the declared one. A mismatch fails the run.

init

{"op": "init", "parameters": {"area": 1.2}, "seed": 42, "start": 0.0, "stop": 1200.0}
{"ok": "init", "outputs": {"h": 1.0}}

The outputs before the first step. seed is null for an unseeded run.

advance

{"op": "advance", "from": 0.0, "step": 0.1, "to": 0.1, "inputs": {"pump_command": 1.0}}
{"ok": "advance", "at": 0.1, "outputs": {"h": 1.002}, "next_event": null,
 "decisions": [{"rule": "keep the pump as it is between the levels",
                "where": "pump_control.adb:41", "because": {"level": 0.83}}]}

  • from, step and to are the master's own numbers. An engine that steps uses from and step as they are: recomputing the step as to minus the previous time does not give it back exactly in floating point, and a run would drift in its last digits.
  • next_event: optional; the time of the engine's next event, if it knows it.
  • decisions: optional; what the code decided during the step, where in its source (file:line), and from what. A violated requirement is traced to them: the report names the rule, the line, the reading, and the function that holds the line.
  • A batch engine answers one advance to the horizon with its series: {"ok": "advance", "at": 1200, "series": {"h": [[0, 1.0], [1, 0.98], ...]}}.

stop

{"op": "stop"}
{"ok": "stop"}

Then Mokili closes the engine's input.

Errors

Any message may be answered with {"error": "<what went wrong>"}; the run fails with that sentence. An answer that is not JSON, answers another operation, or omits an output fails the run too, as does an engine that does not answer within the run's time budget.

Where the engine runs

The model record (kind: engine) says it, never how to start it:

runtime.kind Started as
python the record's program, a stored Python script, run by the Python that runs Mokili
native the record's program, a stored executable, run directly
emulated-processor the record's program, built for runtime.architecture, under qemu-<architecture>
bench the command the operator maps to runtime.bench in the file named by MOKILI_BENCHES

A program runs with the rights of the user running Mokili, as an FMU does.

The transcript

Mokili keeps every line, both ways, prefixed > (sent) or < (received), and hashes each with the hash before it:

h0 = sha256:000…0
hi = sha256( hi-1 || "\n" || direction || " " || line )

The run record keeps the transcript's digest, the last hash and the number of messages; the evidence bundle carries the file. Reproducing a run compares the hashes and, when they differ, names the first message that differs. lakisa transcript RUN --against MODEL replays the recorded messages against another version of the engine: a regression test that needs neither the rest of the system nor the scenario.

A minimal engine

import json, sys

for line in sys.stdin:
    m = json.loads(line)
    if m["op"] == "describe":
        a = {"ok": "describe", "name": "echo", "protocol": "mokili-step/2", "determinism": "deterministic",
             "capabilities": ["advance"], "parameters": [], "inputs": [{"name": "u", "unit": "1"}],
             "outputs": [{"name": "y", "unit": "1"}]}
    elif m["op"] == "init":
        a = {"ok": "init", "outputs": {"y": 0.0}}
    elif m["op"] == "advance":
        a = {"ok": "advance", "at": m["to"], "outputs": {"y": m["inputs"].get("u", 0.0)}}
    else:
        print(json.dumps({"ok": "stop"}), flush=True)
        break
    print(json.dumps(a), flush=True)

Worked engines: examples/pump-tank-valve/tank_engine.py (a plant, in Python) and examples/pump-tank-valve/controller-engine/ (control code in Ada, reporting its decisions).

Compatibility with mokili-step/1

Targets (kind: target) speak mokili-step/1: numbers on a line, no description, no decisions. They keep working. An engine announces /2 in its answer to describe.