Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Simulation runs

The engine that runs a project on a controller in real time also runs it as a simulation: the same project, the same model packages, the same fixed-step semantics, as fast as the models compute. taktwerk solves no equations itself. Each model advances itself by one step at every tick it is due, and the engine exchanges values at tick boundaries, as in real time.

Pace

--speed decides how ticks follow the wall clock:

taktwerk run project.toml                  # real time: one tick per tick_ms
taktwerk run project.toml --speed 120      # deadlines 120 times closer together
taktwerk run project.toml --speed max --no-connectors --cycles 100000
speedticksengine clockoverruns
1on absolute deadlines every tick_mswall clockcounted
<factor>on absolute deadlines every tick_ms / factorengine timecounted
maxeach as soon as the previous one is doneengine timenone

Model time is start_time + tick × tick_ms at every speed; a model sees the same step size and the same inputs. What changes is how long a tick takes on the wall clock.

Engine time. Off real time the engine’s clock is engine time: an input’s age is measured in model time, and a write by a connector (a tunable set from the TUI, say) is stamped with the engine time of the tick it lands in. So max_age_ms means the same at every speed.

Recorders lose no rows at max. In a paced run a file sink that falls a ring behind loses rows (see Recording); unpaced, nothing is late, so the cycle waits for its file sinks instead. A buffer sink is not waited for.

Connectors. --no-connectors starts none: the run serves nothing and binds no port, so many runs can share a machine. An opcua-client exchanges values with a device in wall time and is refused off real time. The OPC UA server may stay; a client sees the run go by at its speed and can still set tunables, which are then stamped and logged at the engine’s tick.

Same project, same inputs: the same rows, at any speed. A run whose inputs only come from its models (no connector writes while it runs) records byte-identical parts at --speed 1, 120 and max, except for taktwerk.overruns, which counts the machine’s missed deadlines.

Stop conditions

A run ends on SIGINT or SIGTERM, on a model error, or at the first of these:

taktwerk run project.toml --speed max --no-connectors \
  --cycles 1000000 --until-time 86400 \
  --stop-when 'tank.level > 2.0' --stop-when 'ctrl.alarm'
  • --cycles N: after N ticks.
  • --until-time S: before the first tick whose engine time is at or after S seconds, so the last tick is the last one before S; with start_time 0 and a 0.1 s tick, --until-time 60 runs 600 ticks.
  • --stop-when CONDITION: after the first tick at which the condition holds; that tick is the last recorded one. A condition is <signal> <op> <number> with >, >=, <, <=, == or !=, or a bare signal, which holds when the value is not zero (a flag set). name[i] takes element i of an array. true and false are 1 and 0. Any numeric signal works, read after the tick: outputs, inputs, tunables, system signals. NaN satisfies only !=. The option repeats; the first condition that holds ends the run.

Conditions are parsed against the plan at start, so a typo fails the run before the first tick. The stop event in events.csv names what ended the run: cycle limit reached, time limit reached, or condition tank.level > 2.0.

Overrides

One project serves many runs. --set, --model and --record-dir change a run without editing the project file:

taktwerk run project.toml --speed max --no-connectors --until-time 3600 \
  --set ctrl.kp=2.5 --set plant.tau='[0.2, 0.5]' --set plant.dims.n=2 \
  --set engine.start_time=0 --set plant.u=1.0 \
  --model plant=../packages/plant-v2 --record-dir out/run-7
keywhat it sets
<instance>.<parameter>the parameter’s value, like parameters in the project
<instance>.<tunable>the tunable’s start value
<instance>.<input>the initial value of the external signal the input reads
<instance>.dims.<dimension>the dimension’s length
<instance>.everythe instance’s period in ticks
engine.tick_ms, engine.start_time, engine.input_max_age_msthe engine setting
<signal>a tunable bound to that signal (its start value), or an external input (its initial value)
  • The value is a TOML value: 0.5, 3, true, [1.0, 2.0], "model2.txt". Text that is not valid TOML is taken as text, so --set ctrl.mode=fast works without quotes. A model’s data file, read by the library from a text parameter, is just such a parameter.
  • Overrides apply in order once the models are loaded, so a later one wins, and they are checked like the project file: an unknown instance, variable or signal, an output, a wired input, or a value of the wrong shape fails the run before it starts.
  • An initial value is written into the image before the first tick. Nothing writes the input again in a run without connectors, so it holds for the whole run unless a scenario event changes it.
  • --model ID=PATH points model ID at another package or FMU of the same kind. --record-dir DIR replaces the dir of every file recorder, so the run lands in DIR/<project>/<recorder>/<run-id>/. Both paths are relative to the working directory.

Every applied override is recorded in run.json under invocation, with the project’s value it replaced (was, absent when the project left it to the model), next to the speed, the stop conditions and whether connectors ran. With the project file’s hash, a record describes its run completely:

"invocation": {
  "speed": "max",
  "connectors": false,
  "until_time": 3600.0,
  "overrides": [
    { "key": "models.plant", "value": "/home/me/packages/plant-v2", "was": "plant" },
    { "key": "ctrl.kp", "value": 2.5, "was": 1.0 }
  ]
}

Seeds

--seed N gives a run one seed, and every instance that draws random numbers its own seed derived from it:

taktwerk run project.toml --speed max --no-connectors --until-time 600 --seed 7
  • Which variable. An instance takes its seed through a numeric scalar parameter or tunable named seed, when its model has one. A model that names it differently is pointed at it in the project, seed_var = "rng_seed" on the [[instance]]; seed_var = "" takes none. A model needs no change for this: it already reads its seed from a parameter, now the run sets it.
  • Which value. Instance id gets splitmix64(N xor fnv1a(id)), reduced to what the variable holds: 31 bits for a signed 32-bit integer, 32 for an unsigned one, 53 for a float, 63 for a 64-bit integer. Instances draw independent streams, and a renamed instance draws another.
  • What wins. The run seed replaces a seed the project file sets; --set <instance>.seed=… replaces the run seed. Without --seed nothing changes.
  • Recorded. run.json’s invocation holds seed and seeds, the value each instance got.

Same project, same overrides, same seed: byte-identical parts, at any speed.

Scenario events

A scenario file says what happens to a run while it runs: timed events, and the overrides, stops and seed that go with them. One project, many scenarios:

taktwerk run project.toml --speed max --no-connectors --scenario disturbance.toml
seed = 3

[set]                            # overrides, as --set takes them
"ctrl.sp" = [1.0]

[models]                         # model packages, relative to this file
# plant = "../packages/plant-v2"

[stop]                           # as --cycles, --until-time and --stop-when
time_s = 900.0
when = ["plant.level > 2.5"]

[[event]]                        # a step of the disturbance inflow at 300 s
at_s = 300.0
add = "plant.q_dist"
value = 0.2

[[event]]                        # the level sensor freezes for a minute
at_s = 400.0
fault = "plant.level_m"
hold = true
for_s = 60.0

[[event]]                        # a set-point ramp
at_s = 600.0
ramp = "ctrl.sp[0]"
to = 1.5
over_s = 100.0

[[event]]                        # from 800 s the sensor reads 0.1 m high, to the end
at_s = 800.0
fault = "plant.level_m"
offset = 0.1

An event applies at the first tick whose engine time is at or after at_s, or at tick cycle, before the models step; events of one tick apply in file order. Each has one action:

actionvaluesdoes
set = "<target>"valuesets a tunable or external input; a number sets every element (or name[i] one), an array the whole signal
add = "<target>"valueadds value: a step
ramp = "<target>"to, over_smoves linearly from the value at the event to to, one step per tick, reaching it over_s later
fault = "<signal>"one of the faults below, for_schanges what the signal’s readers see, for for_s seconds or until cleared
clear = "<signal>"ends the fault on the signal
faultreaders see
hold = truethe value the signal had when the fault started
value = <x>x (nan allowed: a lost reading)
offset = <x>the value plus x: a bias
scale = <x>the value times x: a gain error
noise_sd = <x>the value plus normal noise of standard deviation x
drop = truean external input no longer written: its last value, and with an age limit it goes stale like a lost connection
  • A target of set, add and ramp is a tunable or an external input. A fault acts on an output or an external input, element name[i] or whole, for every model that reads it. A new fault on a signal replaces the one in force.
  • Faults act where models read: a recorded output stays what its model produced. A recorded external input and a recorded composed input are what their model received. An input wired whole to an output has no signal of its own (its column would be the output), so to see what a model received there, compose it: inputs = { pv = ["plant.y"] } records as ctrl.pv.
  • Noise draws from a stream of the run seed and the event’s index, so a seeded run repeats its noise.
  • Every applied, started and ended event is a scenario event in events.csv, with the signal as its source: fault plant.level_m hold for 60 s, and later end: fault plant.level_m hold for 60 s.
  • Events are checked against the plan at start: an unknown signal, a target of the wrong kind, a missing or misplaced value fails the run before the first tick.

The file’s overrides apply before --set, its models before --model, and --seed, --cycles, --until-time win over the file’s; its stop conditions and those of the command line all apply. run.json’s invocation keeps the file whole: its path, hash and text.