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¶
{"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), orexternal(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 oneadvanceand 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¶
The outputs before the first step. seed is null for an unseeded run.
advance¶
{"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,stepandtoare the master's own numbers. An engine that steps usesfromandstepas they are: recomputing the step astominus 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
advanceto the horizon with its series:{"ok": "advance", "at": 1200, "series": {"h": [[0, 1.0], [1, 0.98], ...]}}.
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:
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.