Labinetix
Models, compiler, analysis and estimation for process models: define → analyze → build.
Labinetix turns a model written down once, as data in a hashed store, into taktwerk model
packages (a C library with its header and descriptor; an FMI 3 FMU through taktwerk fmu-wrap), and reads taktwerk run records back. It never runs on a plant: taktwerk does.
flowchart LR
subgraph labinetix["labinetix (host)"]
define["define<br/>models/ store, sync, check"] --> analyze["analyze<br/>simulate, estimate, replay"]
define --> build["build<br/>raw-package per target"]
end
build -- "dist/<package>-<version>-<arch>" --> taktwerk["taktwerk<br/>engine, connectors, devices"]
taktwerk -- "run records" --> analyze
Kernels (crates/kernels/) and packages (crates/packages/, models/*/generated/raw) never
depend on the host crates (crates/studio/); a lint enforces it.
Getting started
cargo build -p labinetix && export PATH="$PWD/target/debug:$PATH"
labinetix define list
labinetix analyze simulate models/chemostat/scenarios/feed-step.toml | cmp - models/chemostat/scenarios/feed-step.prediction.json
labinetix build raw-package host-linux # needs zig and cargo-zigbuild
Documentation
- Getting started: the three stages on the host
- From a model to a released package: one model end to end, and back from a taktwerk record
- Modeling tutorials: your first model, composition and balances, estimation and identifiability
- CLI reference
Getting started
Fences tagged exec run in document order, in one shell session, from the root of the clone;
just tutorial runs them (tools/doc-exec). Untagged fences are prose.
In a few minutes you build the command line, read the model store, simulate a model and compare the result with the committed prediction, and build the packages for the machine in front of you. Nothing here talks to a device.
Before you start
A clone of this repository and the Rust toolchain it pins (rust-toolchain.toml; rustup
reads it). For the packages, zig and cargo-zigbuild
(crates/studio/build-xtask/scripts/install-zig.sh, install-cargo-zigbuild.sh).
1. Build the CLI
cargo build -p labinetix
export PATH="$PWD/target/debug:$PATH"
2. The three stages
labinetix --help
Define, analyze and build models into taktwerk packages
…
define …
analyze …
build …
define keeps the model store, analyze runs models on the host, build turns them into
packages taktwerk loads. Every command has --help; the
CLI reference lists them all.
3. define: the model store
models/ holds one directory per model: what you write under authoring/, what Labinetix
generates from it under generated/, and the model’s scenarios, estimators, datasets and
parameter sets. Every entity is hashed and locked.
labinetix define list
…model_scaffold chemostat …
define check re-generates the whole store and compares every file byte for byte; it prints
nothing when all is well:
labinetix define check
4. analyze: a model on the host
A model scenario pins the model’s content hash and says how to run it. Simulating it gives the same bytes as the prediction committed beside it:
labinetix analyze simulate models/chemostat/scenarios/feed-step.toml \
| cmp - models/chemostat/scenarios/feed-step.prediction.json && echo identical
identical
5. build: packages for a target
A target is a name from hardware/targets/: a triple and, for a device, the newest glibc its
libraries may need.
labinetix build target
…host-linux The build host
build raw-package compiles every package for a target and writes it into dist/: one
directory per package (descriptor, header, lib/<arch>/, package.toml with its version,
interface fingerprint and maturity), a tarball each and SHA256SUMS.
labinetix build raw-package host-linux
ls dist | grep chemostat
chemostat-…
Where to go next
- From a model to a released package: one model end to end, into taktwerk and back from its record.
- The modeling tutorials: your first model, composition and balances, estimation and identifiability.
From a model to a released package
One model end to end: from the model store to a package taktwerk loads, released, run, and
back from the run’s record. The model is aerobic_fed_batch, an aerobic fed-batch culture on
glucose, with offgas, an estimator that tracks it from the off-gas and the dissolved oxygen.
Both packages it ends in are generated; a third, a filter written by hand, shows what you write
when the model language is not the tool.
How to write a model is in the modeling tutorials, which this page does not repeat: your first model, composition and balances, estimation and identifiability.
Fences tagged exec run in document order, in one shell session, from the root of the clone;
just tutorial runs them after Getting started. Each chapter ends in a
command and the result to check.
Before you start
Getting started, cc, zig and cargo-zigbuild for the packages, and
taktwerk 0.11 or newer as taktwerk on your PATH:
cargo build --quiet -p labinetix
export PATH="$PWD/target/debug:$PATH"
taktwerk --version
taktwerk 0.…
1. The model in the store
models/aerobic_fed_batch/ is one entity of the store:
authoring/aerobic_fed_batch.toml, what you write: species with their formulas, two reactions (growth and maintenance) on biomass, a feed, oxygen transfer by a kLa correlation and a quasi-steady gas phase. The biomass yieldy_xsis the one given coefficient; oxygen, CO2, water and ammonia follow from the elements.aerobic_fed_batch.lock.json, the content hash and version;generated/, whatlabinetix define syncwrites from it: the IR, a ModelingToolkit view and the raw package, a crate with its header and descriptor. The kernel itself goes tocrates/kernels/core-models/generated/.scenarios/andestimators/, pinned to the model’s hash.
labinetix define list | grep aerobic_fed_batch
estimator offgas 1.0.0 sha256:…
model_scaffold aerobic_fed_batch 1.0.0 sha256:5572908c…
define check re-generates the whole store and compares every file byte for byte; silence is
success. An edit to authoring/ is followed by define sync, which moves the hash; every
scenario and estimator pinned to the old one is then refused until you review and re-pin it.
labinetix define check
The balance report says what the composition is and what the estimator’s instruments determine. The starred coefficients are derived; the electron residual of each reaction is zero to rounding. Off-gas O2 and CO2 and the biomass determine both reaction rates with one degree of redundancy, a balance test on the measurements:
labinetix define balance model_scaffold/aerobic_fed_batch --estimator offgas
off_gas_o2 measures oxygen
off_gas_co2 measures co2
do_pct measures oxygen
c_x_out measures biomass
parameter states: y_xs
…
growth 0.5000 -1.0000 -0.3835* 0.5722* 0.3805* -0.0692* …
maintenance . -1.0000 -1.0657* 1.4657* 0.6000* . …
…
rank 2 of 2 reactions, redundancy 1: every reaction rate is determined
determined directions: 2; balance tests on the measured rates: 1
2. Simulate a scenario
scenarios/batch-then-feed.toml pins the model’s content hash and says how to run it: RK4,
steps of 0.01 h in 200 substeps, 1600 steps, a row every tenth. The culture grows on 25 g/L
glucose, exhausts it at about 8 h, and from step 850 (8.5 h) a feed of 0.01 L/h keeps it
glucose-limited to 16 h. analyze simulate steps the generated kernel through
core-integrate, the same code the package runs; its output is the committed prediction,
byte for byte:
labinetix analyze simulate models/aerobic_fed_batch/scenarios/batch-then-feed.toml \
| cmp - models/aerobic_fed_batch/scenarios/batch-then-feed.prediction.json && echo identical
identical
3. The estimator and its EKF package
estimators/offgas/authoring/offgas.toml is an estimator entity: the model it is pinned to,
every state’s initial value and noise, the biomass yield as a random-walk parameter state, and
the measured outputs in row order:
[model]
entity = "model_scaffold/aerobic_fed_batch"
version = "1.0.0"
content_hash = "sha256:5572908c88754a75694b93f793aa22d0fb5a73c0b2142e90d35d3fe14c572d6a"
[[parameter_states]]
parameter = "y_xs"
q = 1e-8
p0 = 1e-2
min = 0.0
[[measured]]
output = "off_gas_o2"
r = 1e-8
define sync estimator/offgas writes an EKF package from it under its generated/raw/: the
model’s kernel with its exact Jacobians, core-filters’ EKF, behind
aerobic_fed_batch_ekf_offgas.h. Because y_xs is the model’s free yield, the derived
coefficients follow it through the balances: the filter cannot leave them. The package’s
inputs are the model’s inputs and one <output>_meas per measured row, NaN when a sample is
missing; Q, R and the χ² gate are tunables; the outputs are the estimate, the model’s outputs
at it, the variances and the update’s report (nis, rows, flags).
ls models/aerobic_fed_batch/estimators/offgas/generated/raw/package
aerobic_fed_batch_ekf_offgas.h
taktwerk-model.toml
The proof estimators/offgas/scenarios/batch-then-feed.toml runs the model scenario of
chapter 2 as the plant, the off-gas and the dissolved oxygen read every step, the biomass
once an hour, and starts the estimator 36 % high on biomass and at y_xs = 0.4 where the
plant’s is 0.5. The committed record beside it holds the hash of every row and the yield the
filter ends at:
grep -E '"(rows|bit_identical|final_parameter_states)"' \
models/aerobic_fed_batch/estimators/offgas/scenarios/batch-then-feed.taktwerk.json
"rows": 1600,
"bit_identical": true,
"final_parameter_states": { "y_xs": 0.500006… }
just raw-ekf-proof reproduces it: both packages wired in one taktwerk project, an offline row
a fault value of NaN cleared at each sample, against the host reference that steps the
packages’ own Rust in taktwerk’s order. Every row must be bit-identical (--write rewrites
the record; a test holds the committed one to its hashes without taktwerk):
just raw-ekf-proof aerobic_fed_batch
aerobic_fed_batch/offgas/batch-then-feed: bit-identical, 1600 rows × 24 values
4. A hand-written algorithm
What the model language does not hold, you write by hand: a filter, a controller, a plant with
its instruments. It takes two crates. The kernel, under crates/kernels/core-*, holds the
arithmetic: no_std, no unsafe, no allocation, no I/O. The package, under
crates/packages/raw-<name>, binds C structs to it and holds no math. The example is a
first-order low-pass filter, τ·ẏ = u − y.
crates/kernels/core-lowpass samples it exactly
for an input held over each step, y ← y + a·(u − y) with a = 1 − exp(−T/τ), so the
step response at the samples is 1 − exp(−k·T/τ) at any step size; τ = 0 passes the input
through. Its test compares the step response with that closed form.
crates/packages/raw-lowpass is liblowpass.so
behind package/lowpass.h:
| struct | members | taktwerk causality |
|---|---|---|
lowpass_params | y0, time_factor | parameter: fixed at init |
lowpass_inputs | u | input |
lowpass_tunables | tau (s) | tunable: changeable while it runs |
lowpass_outputs | y, e = u − y | output |
lowpass_init, lowpass_step and lowpass_terminate are the only unsafe code, a site
listed with its count in policy/unsafe-allowlist.toml; they read and write the engine’s
structs around Filter, the instance in safe Rust that the package’s tests step. The
descriptor package/taktwerk-model.toml is what taktwerk import-header --shape lowpass.h
reads from the header, and tests/header.rs compiles the header with cc and compares every
size and offset with the Rust structs.
A package has no evidence until a committed run proves it: raw-lowpass has no
evidence.toml, so it ships as experimental (chapter 5).
cargo test -p core-lowpass -p raw-lowpass 2>&1
just boundary-lint
test the_step_response_is_the_continuous_filter_sampled ... ok
…
test a_step_input_rises_with_the_time_constant ... ok
…
test header_layout_matches_the_rust_structs ... ok
5. A package per target
A target is a file under hardware/targets/: a triple and, for a device, the newest glibc its
libraries may reference. The host target writes no triple; it is the build machine:
labinetix build target host-linux
id host-linux
description The build host
build raw-package builds every package for a target, hand-written and generated, checks each
library against the target’s glibc floor, and writes dist/<name>-<version>-<arch>/ (with
-glibc<floor> for a device target): the descriptor, the header, lib/<arch>/lib<name>.so
and package.toml, plus one deterministic tarball each and dist/SHA256SUMS. A device
target is the same command with its name; the build cross-compiles for aarch64, x86_64 or
armv7 through zig.
labinetix build raw-package host-linux 2>&1 | grep -E 'aerobic_fed_batch|lowpass|packages for'
raw-package: dist/aerobic_fed_batch-…-….tar.gz interface sha256:69a901fd…
raw-package: dist/aerobic_fed_batch_ekf_offgas-…-….tar.gz interface sha256:4738e7f0…
raw-package: dist/lowpass-…-….tar.gz interface sha256:f67d7b1a…
raw-package: … packages for host-linux in dist/, SHA256SUMS
package.toml is for people and release tooling; taktwerk does not read it. It carries the
product version (one for every package of a release), the target, the interface
fingerprint (SHA-256 over the descriptor’s name, dimensions and each variable’s name,
causality, type and shape, in order: a changed default or unit moves nothing, a renamed or
reordered variable does) and the maturity with its evidence: simulated for a committed
proof in simulation, plant for a run on a real plant cited by run id and hash,
experimental otherwise.
export version=$(labinetix --version | cut -d' ' -f2) arch=$(uname -m)
cat dist/aerobic_fed_batch-$version-$arch/package.toml
grep maturity dist/aerobic_fed_batch_ekf_offgas-$version-$arch/package.toml \
dist/lowpass-$version-$arch/package.toml
name = "aerobic_fed_batch"
version = "…"
target = "host-linux"
interface = "sha256:69a901fd…"
maturity = "simulated"
record = "models/aerobic_fed_batch/scenarios/batch-then-feed.three-way.json"
dist/aerobic_fed_batch_ekf_offgas-…/package.toml:maturity = "simulated"
dist/lowpass-…/package.toml:maturity = "experimental"
An interface change that appends an input or tunable, off at its default with the old behaviour bit for bit, is minor; anything else is major.
The built filter runs under taktwerk as it is: a step from 0 to 1 with a time constant of
10 s, one tick a second. After ten ticks, one time constant, it is 1 − 1/e of the way:
rm -rf target/lifecycle && mkdir -p target/lifecycle
cat > target/lifecycle/lowpass.toml <<EOF
[engine]
tick_ms = 1000.0
[models.lowpass]
kind = "raw"
path = "../../dist/lowpass-$version-$arch"
[[instance]]
id = "lp"
model = "lowpass"
[instance.parameters]
y0 = 0.0
time_factor = 1.0
[[recorder]]
id = "step"
signals = ["lp.y", "lp.e"]
EOF
taktwerk run target/lifecycle/lowpass.toml --speed max --no-connectors --cycles 10 \
--set lp.u=1.0 --set lp.tau=10.0 --record-dir target/lifecycle/records 2>/dev/null
gzip -dc target/lifecycle/records/lowpass/step/*/part-0001.csv.gz | tail -1
9,9.0,0.6321205588285577,0.36787944117144233
The three-way proof
A generated package is proven by its model’s scenarios: just raw-three-way runs each through
analyze simulate, through the package under taktwerk run --speed max and through the
package’s FMU under taktwerk’s FMI adapter, and requires every recorded row identical in every
state, output, input and t. taktwerk records after each tick’s step, so the prediction’s
initial row has no recorded twin. --write commits the result beside the scenario as
batch-then-feed.three-way.json, the record the plant package’s maturity cites:
just raw-three-way aerobic_fed_batch
aerobic_fed_batch/batch-then-feed: bit-identical, 160 rows × 14 values, raw and fmi
Sidebar: the FMU
Any FMI 3 host runs a package as a co-simulation FMU: taktwerk fmu-wrap wraps a dist/
directory, the tunables’ start values as the FMU’s start values, --target for another
architecture. A release carries the package; an FMU is made from it.
taktwerk fmu-wrap dist/aerobic_fed_batch-$version-$arch \
-o target/lifecycle/aerobic_fed_batch.fmu --write 2>&1 | tail -1
taktwerk inspect --kind fmi target/lifecycle/aerobic_fed_batch.fmu | head -1
wrote target/lifecycle/aerobic_fed_batch.fmu
model aerobic_fed_batch (fmi)
6. Release and hand-over
dist/ is the hand-over. Build output is never committed: a release attaches one tarball per
package and architecture, <name>-<version>-<arch>[-glibc<floor>].tar.gz, and SHA256SUMS
over all of them. A tarball unpacks to the package directory taktwerk loads and is packed
deterministically (sorted entries, mtime 0, owner 0). Whoever receives it checks it first:
(cd dist && sha256sum -c SHA256SUMS)
aerobic_fed_batch-…-….tar.gz: OK
aerobic_fed_batch_ekf_offgas-…-….tar.gz: OK
lowpass-…-….tar.gz: OK
The receiver reads package.toml for what it is: version, target, interface fingerprint,
maturity and the evidence behind it. The model and its proofs stay here, in the store, pinned
by the hashes the evidence cites.
7. Continue in taktwerk
examples/tutorial-lifecycle/ is a taktwerk project of the
two generated packages: the model’s as the plant, the estimator’s reading the plant’s off-gas,
dissolved oxygen and biomass (scenario.toml is the model scenario batch-then-feed as
taktwerk events). One tick is one step of 0.01 h, 36 s of engine time. The plant’s
[[instance]] table is what labinetix define instance writes from the model’s defaults and
the scenario; parameter values are instance configuration, never baked into a library
(--parameter-set adds estimated ones):
diff <(labinetix define instance model_scaffold/aerobic_fed_batch --scenario batch-then-feed \
| sed -n '/^\[instance.parameters\]/,$p') \
<(sed -n '/^\[instance.parameters\]/,/^$/{/^$/q;p}' examples/tutorial-lifecycle/project.toml) \
&& echo "the plant's parameters are the scenario's"
the plant's parameters are the scenario's
just example stages the project with its packages from dist/ under
target/examples/tutorial-lifecycle/ and runs it there, as fast as the machine computes it.
The @io recorder keeps every input and output of every tick:
just example tutorial-lifecycle --scenario scenario.toml --speed max 2>&1 | tail -1
cycles 1600 published 1600 overruns 0 stale 0 … stop time limit reached
Without --speed max the same project runs in real time, its tunables open over taktwerk’s
OPC UA server. The workbench, devices and deployment are taktwerk’s: its tutorial
From a package to a running device
takes a package from here to an edge device and a plant.
The store in the workbench
labinetix serve shows this repository’s store read-only in the taktwerk workbench: entities
and their locks, each model’s interface, equations, composition and balances, scenarios with
their predictions, estimators with their proofs, and packages with their descriptors, evidence
and dist/ artefacts. It prints its own address and announces itself, so a taktwerk serve
on the same machine lists it under Add-ons; a package’s staged example opens in taktwerk from
there. The UI is built once with just workbench-ui (needs node), before cargo build:
just workbench-ui && cargo build -p labinetix
labinetix serve # in another terminal: taktwerk serve <folder>, then Add-ons → labinetix
8. Back from the plant
A record comes back as a run directory, <dir>/<project>/<recorder>/<run-id>/. analyze replay re-runs its inputs on the host through the packages’ own Rust, with the tunables of
its events.csv and the parameters of the project file it pins by hash, and compares every
recorded output bit for bit. It is how you know a record is what the package computes, and
not a fault, a skipped tick or a swapped library:
export run=$(ls -d target/examples/tutorial-lifecycle/records/project/io/*/)
labinetix analyze replay "$run" --project target/examples/tutorial-lifecycle/project.toml
plant (aerobic_fed_batch): 1600 rows × 11 outputs bit-identical
ekf (aerobic_fed_batch_ekf_offgas): 1600 rows × 20 outputs bit-identical
analyze import-record turns the record into a run of a dataset of the model: the outputs to
observe with their standard deviations, the inputs as recorded, each held from the tick that
used it, and the initial state from the project. Here the biomass and the off-gas CO2 once an
hour; the biomass at 8 h is the prediction’s of chapter 2, to the last digit. A dry run by
default; --write writes it under models/aerobic_fed_batch/datasets/, labinetix define sync dataset/<name> locks it, and estimation fits the model to it.
labinetix analyze import-record "$run" --model-dir models/aerobic_fed_batch \
--dataset tutorial-lifecycle --dataset-doc "The lifecycle tutorial's run under taktwerk" \
--run batch-then-feed --instance plant --inputs n_st,f_air,f_in \
--observe c_x_out=0.05,off_gas_co2=0.0001 --every 100 \
--project target/examples/tutorial-lifecycle/project.toml 2>&1
grep '"t": 8.0,' models/aerobic_fed_batch/scenarios/batch-then-feed.prediction.json
8.0,c_x_out,10.485139388858332,0.05
…
8.5,f_in,0.01
…
dry run: run `batch-then-feed` of dataset/tutorial-lifecycle is not written; --write writes it
{"t": 8.0, "x": [5.0,10.485139388858332,…
Where to go next
- taktwerk’s documentation: projects, scenarios with events and faults, recorders, campaigns, and From a package to a running device.
- CLI reference:
define instance,define balance,build raw-package,analyze replay,analyze import-record.
Modeling 1: your first model
In about fifteen minutes you write a process model as a TOML file, let Labinetix check it, generate its embedded kernel, and simulate it on the host. The model is a batch culture: biomass grows on one substrate with Monod kinetics. Nothing here talks to hardware.
This is the first of four tutorials:
- Your first model (this page): author, check, generate, simulate.
- Composition and balances: build a model from species and reactions, and let the elemental balances check it.
- Estimation and identifiability: ask what data can tell you about the parameters, and estimate them.
- From a model to a released package: the model’s generated package under taktwerk, bit-identical to the prediction, as an FMU, released, and back from its record.
Before you start
Do Getting started step 1 so that labinetix is on your PATH, and work
from the root of your clone:
cd "$REPO_ROOT"
export PATH="$PWD/target/debug:$PATH"
Two directories matter here. models/ holds every model, one directory per model:
models/<id>/authoring/<id>.toml is the file you write, models/<id>/generated/ is what
Labinetix writes from it. models/ is the store the define commands work on (--store,
default models, so commands run from the root of the clone need no flag).
1. Create the model
labinetix define new model_scaffold monod_batch
This writes models/monod_batch/authoring/monod_batch.toml with a placeholder model (a
first-order lag), and generates it once. Open the file and replace its content with the batch
culture:
# model_scaffold/monod_batch — a batch culture: biomass grows on one substrate with Monod
# kinetics and a constant yield, in a vessel that is neither fed nor drained.
[model]
name = "monod_batch"
contract_version = "0.1.0"
kind = "ode"
time_unit = "h"
doc = "Batch Monod growth: dx/dt = mu x, ds/dt = -mu x / y_xs, mu = mu_max s / (k_s + s)."
[parameters.mu_max]
unit = "1/h"
min = 0.0
default = 0.5
doc = "Maximum specific growth rate."
[parameters.k_s]
unit = "g/L"
min = 0.0
default = 0.1
doc = "Monod constant of the substrate."
[parameters.y_xs]
unit = "g/g"
min = 0.0
default = 0.5
doc = "Biomass yield on substrate."
[states.x]
unit = "g/L"
min = 0.0
doc = "Biomass concentration."
[states.s]
unit = "g/L"
min = 0.0
doc = "Substrate concentration."
[[locals]]
name = "mu"
unit = "1/h"
expr = "mu_max * monod(s, k_s)"
doc = "Specific growth rate."
[equations]
x = "mu * x"
s = "-mu * x / y_xs"
[outputs.biomass]
unit = "g/L"
expr = "x"
doc = "Biomass concentration, as measured."
[outputs.growth_rate]
unit = "1/h"
expr = "mu"
doc = "Specific growth rate."
What the tables mean:
| Table | What it declares |
|---|---|
[model] | Name, version, kind = "ode" (equations are derivatives) or "discrete" (next values), the unit of time |
[parameters.*] | Constants of the model, with a unit, optional bounds and a default |
[states.*] | The state. min and max are enforced after every integration substep |
[inputs.*] | Signals the caller sets each step (none here) |
[[locals]] | Named intermediate expressions, evaluated in order |
[equations] | One expression per state: dx/dt for an ode model, which may read the time t; the next value for a discrete one, which may read its step index k |
[outputs.*] | What the model reports, each an expression |
Declaration order matters: it is the order of the state vector and of the generated interface.
Units are checked: define parses every unit (g/L, 1/h, mmol/(1e8 cells d); g_X is
g, the tag names the species) and refuses a sum, a min/max, a comparison or a right-hand side
whose units disagree in dimension or scale, naming the entry and both units. Each equation is in
the state’s unit per time_unit. A bare number takes the unit its context needs (s + 1e-9); a
symbol without a unit is unchecked.
Expressions use a small operator set that every embedded target can run: + - * /, pow,
min, max, exp, log, sqrt, abs and ifelse(condition, a, b). Kinetic laws such as
monod(c, k), haldane(c, k, ki), contois(s, x, k), moser(c, k, n), teissier(c, k),
blackman(c, k) and inhibition(c, ki) are written as calls and expanded into that set:
monod(s, k_s) is exactly s / (k_s + s).
2. Generate
labinetix define sync model_scaffold/monod_batch
sync parses and validates the model, and writes:
models/monod_batch/generated/ir/monod_batch.json, the model as a canonical expression tree. Its hash is the model’s content hash: everything that pins this model pins that hash.models/monod_batch/generated/julia/monod_batch.jl, a ModelingToolkit view, as data.crates/kernels/core-models/generated/monod_batch.rs, the kernel:no_stdRust that computes the right-hand side, the outputs, and their exact Jacobians in the states, inputs and parameters, derived symbolically. This is the code that runs on a target.
It also adds the model to the kernels’ index, so the kernel compiles with every other one.
sync commits what it wrote (sync(model_scaffold/monod_batch): regenerate views). The
kernel’s right-hand side reads like the model:
#![allow(unused)]
fn main() {
fn rhs(&self, x: &[f64; NX], _u: &[f64; NU], p: &[f64; NP], _t: f64, dx: &mut [f64; NX]) {
let l_mu = p[0] * (x[1] / (p[1] + x[1]));
dx[0] = l_mu * x[0];
dx[1] = -l_mu * x[0] / p[2];
}
}
3. When the model is wrong
Labinetix refuses a model it cannot run, before anything is generated, and names the file, the
table and the symbol. Misspell the yield in the substrate equation (yxs for y_xs) and sync:
labinetix define sync model_scaffold/monod_batch
labinetix: define sync: invalid entity state: …/models/monod_batch/authoring/monod_batch.toml: [equations] `s`: references `yxs`, which no table declares
The same holds for an operator outside the set, a local used before it is defined, a state without an equation, or bounds that contradict each other. Fix the name and sync again.
define check verifies the whole store: every generated file is what its source generates
today, and every pin still matches. It prints nothing when all is well. Run it before you
commit by hand:
labinetix define check
4. A scenario
A scenario says how to run the model: the solver, the initial state, input values and changes,
and parameter values that differ from the defaults. It lives beside the model and pins the
model’s content hash, so it can never silently run a changed model. Find the hash in the kernel
(CONTENT_HASH) or the model’s lock file, and write
models/monod_batch/scenarios/batch.toml:
[scenario]
model = "monod_batch"
content_hash = "sha256:…" # the model's content hash
doc = "10 g/L substrate, 0.1 g/L biomass, 12 h."
[solver]
method = "rk4" # rk4 or euler: what a target runs; ros2 for stiff models
step = 0.1 # model time per step (h)
substeps = 4
steps = 120
record_every = 10
[initial]
x = 0.1
s = 10.0
Inputs go in [inputs] (their value at the start), changes in [[schedule]] (step, input,
value), overrides in [parameters]. A discrete model’s solver is method = "discrete" with
step = 1 and substeps = 1: one step of the model per step, so a row’s t is the step index.
5. Simulate
The simulator steps the compiled kernel, so rebuild labinetix after every define sync that
changed a model:
cargo build -p labinetix
labinetix analyze simulate models/monod_batch/scenarios/batch.toml
{
"kind": "labinetix_model_prediction",
"model": "monod_batch",
"content_hash": "sha256:8cd6…",
"scenario": "batch",
"scenario_hash": "sha256:b8cb…",
"columns": {"x": ["x","s"], "u": [], "y": ["biomass","growth_rate"]},
"rows": [
{"t": 0.0, "x": [0.1,10.0], "u": [], "y": [0.1,0.49504950495049505]},
{"t": 1.0, "x": [0.16405317914198603,9.871893641716031], "u": [], "y": [0.16405317914198603,0.4949859072111608]},
…
{"t": 12.0, "x": [5.100000000000007,3.1297485988535e-84], "u": [], "y": [5.100000000000007,1.56487429942675e-83]}
]
}
The culture grows at nearly mu_max until the substrate runs out, around 9 h, and ends at
0.1 + 0.5 · 10 = 5.1 g/L biomass. The simulation steps the generated kernel through the same
integrator a target uses, so this is what the target computes.
Save it beside the scenario as batch.prediction.json (--out) and commit both: the test
suite re-runs every committed scenario and requires the same bytes, so a change to the model or
the toolchain that moves a prediction cannot go unnoticed.
labinetix analyze simulate models/monod_batch/scenarios/batch.toml \
--out models/monod_batch/scenarios/batch.prediction.json
6. Change the model
Edit the authoring, raise contract_version as the change deserves (a new parameter appended
last is a minor change, a removed output a major one), sync and rebuild. Even a new parameter
default moves the content hash, so the scenario refuses to run until you re-pin it, after you
have looked at what changed:
labinetix analyze simulate models/monod_batch/scenarios/batch.toml
labinetix: scenario `batch` pins sha256:8cd6…, and model `monod_batch` is sha256:ad8f…: re-pin it after reviewing the change
7. Models ported from a paper
Some models in models/ are written without composition, as this one is, and reproduce numbers
their source prints. Each header cites the table and page of every default and names any
deviation from the printed equations; a test in crates/studio/analyze-model/tests/ checks the
committed prediction against a second implementation and against the printed numbers:
models/hybridoma_fed_batch: hybridoma cells on glucose and glutamine with lactate, ammonia, death and antibody (de Tremblay et al. 1992, re-simulated by Roubos et al. 1997). Itsconstant-feedscenario ends at Roubos’s 280.4 mg of antibody, within 0.2 %.models/penicillin_fed_batch: the penicillin benchmark of Lim et al. with Contois growth and Haldane-inhibited production (Ochoa 2016). Itsluus-20-stagesscenario replays Luus’s optimal feed (1993) and meets every state of his Table II to 2e-4 and his 87.95 g of penicillin.models/zymomonas_fed_batch: Z. mobilis ethanol fed-batch with threshold and power-law inhibition, in which growth consumes no glucose (Wang and Shyu 1997). Itstable-1-idp-sqpscenario replays the paper’s optimal feed to 78.635 kg of ethanol against the printed 78.6302 kg; its initial biomass is fitted to the paper’s Fig. 1 and labelled so.
Next
Composition and balances builds a model from species and reactions instead of writing every balance by hand, and lets Labinetix check that the reactions conserve atoms and electrons.
Modeling 2: composition and balances
Writing every balance of a bioprocess by hand is where errors hide: a yield that does not conserve electrons, a dilution term forgotten in one equation. In this tutorial you describe an aerobic batch culture by its species and its reaction instead, Labinetix writes the balances, derives the yields the chemistry determines, and refuses a reaction that would create atoms or electrons.
It continues Your first model; the commands are the same.
The idea
Every species follows the same balance (Bastin & Dochain’s general dynamical model), per liquid volume:
dc_i/dt = Σ_feeds D_k (c_in,ik − c_i) + Σ_reactions K_ij r_j + T_i
with D_k the dilution rate of feed k, K_ij the stoichiometric coefficient of species i
in reaction j, r_j the reaction’s rate and T_i the gas–liquid transfer. Everything but the
rates is generic. What is specific to your process is which reactions exist and how fast they
run; the rest follows from the species’ formulas.
1. The model
labinetix define new model_scaffold aerobic_batch
Replace models/aerobic_batch/authoring/aerobic_batch.toml with:
[model]
name = "aerobic_batch"
contract_version = "0.1.0"
kind = "ode"
time_unit = "h"
doc = "Aerobic batch growth on glucose, composed."
[parameters.mu_max]
unit = "1/h"
default = 0.4
doc = "Maximum specific growth rate."
[parameters.k_s]
unit = "g/L"
default = 0.1
doc = "Monod constant of glucose."
[parameters.y_xs]
unit = "g/g"
default = 0.5
doc = "Biomass yield on glucose."
[parameters.y_os]
unit = "g/g"
default = 0.4
doc = "Oxygen taken per glucose."
[parameters.v_r]
unit = "L"
default = 1.0
doc = "Liquid volume."
[parameters.kla]
unit = "1/h"
default = 300.0
doc = "Oxygen transfer coefficient."
[parameters.c_o2_sat]
unit = "g/L"
default = 0.007
doc = "Oxygen saturation concentration."
[states.c_x]
unit = "g/L"
min = 0.0
doc = "Biomass."
[states.c_s]
unit = "g/L"
min = 0.0
doc = "Glucose."
[states.c_o2]
unit = "g/L"
min = 0.0
doc = "Dissolved oxygen."
[liquid]
volume = "v_r"
[species.biomass]
formula = "CH1.8O0.5N0.2"
state = "c_x"
[species.glucose]
formula = "C6H12O6"
state = "c_s"
[species.oxygen]
formula = "O2"
state = "c_o2"
[species.co2]
formula = "CO2"
[species.water]
formula = "H2O"
[species.ammonia]
formula = "NH3"
[[reactions]]
name = "growth"
catalyst = "biomass"
rate = "mu_max / y_xs * monod(c_s_pos, k_s)"
stoichiometry = { glucose = -1, biomass = "y_xs", oxygen = "-y_os" }
balance = ["co2", "water", "ammonia"]
[[transfers]]
species = "oxygen"
kla = "kla"
saturation = "c_o2_sat"
[outputs.c_x_out]
unit = "g/L"
expr = "c_x_pos"
doc = "Biomass."
What is new:
| Table | What it declares |
|---|---|
[liquid] | The liquid volume: a parameter (constant volume) or a state (it grows by the feeds) |
[species.*] | A formula, optionally a molar_mass and the state that holds its concentration (g/L). A species without a state, like CO2 here, is not dissolved |
[[reactions]] | A rate, the catalyst it is specific to, the stoichiometry in g per unit of reaction, and the species to derive from the elements (balance) |
[[feeds]] | A flow into the liquid and the concentrations it carries (none here) |
[[transfers]] | Gas–liquid transfer kLa (c* − c) of a dissolved species |
[gas] | A quasi-steady gas phase: inlet flow and mole fractions, off-gas from the inert-gas balance |
The rate here is per gram of biomass (catalyst = "biomass") in grams of glucose: glucose has
coefficient −1, so the reaction consumes mu_max / y_xs · s/(k_s + s) g glucose per g biomass
per hour and makes y_xs g biomass from each.
A given coefficient is a mass ratio (g_X / g_S), and the unit check holds the generated
balances to the states’ units like any equation.
You write no [equations]: the composition writes the balance of every species state. The
generated locals c_x_pos, c_s_pos, … (a state read as zero when negative), r_growth (the
reaction’s rate per litre) and tr_oxygen are available to your own locals and outputs.
2. A reaction that does not balance
Sync:
labinetix define sync model_scaffold/aerobic_batch
labinetix: define sync: invalid entity state: …/aerobic_batch.toml: [reactions] `growth`: does not balance for every parameter value: per g of `biomass`, element residuals [C +2.2740e-2, H +5.6850e-3, N -1.7055e-2, O -1.1370e-2] mol/g; degree of reduction +1.7055e-1 mol e⁻/g. The elements fix 4 of its 6 coefficients and `balance` derives 3: derive 1 more, one of `biomass`, `oxygen` (a coefficient the elements determine is never a parameter)
Six species take part in the reaction and four elements constrain them, so two coefficients are
free, and the other four follow. You gave three (glucose, the biomass yield and the oxygen
yield) and derived three. With all three fixed, the
elements C, H, O and N cannot all close for every value of the yields, and the electrons
(degree of reduction) do not either. A fit could move y_os away from the one value that
conserves them, and the model would quietly create electrons: the error that a soft sensor
reading the off-gas turns into a wrong biomass.
The fix is to let the chemistry decide: keep the biomass yield, the one quantity the culture
chooses, and derive oxygen. Delete the y_os parameter and change the reaction:
stoichiometry = { glucose = -1, biomass = "y_xs" }
balance = ["oxygen", "co2", "water", "ammonia"]
labinetix define sync model_scaffold/aerobic_batch
Labinetix solves the element balances for the four listed species. Every derived coefficient is
a generated local nu_<reaction>_<species>, linear in the yields it depends on, which your
rates may read too:
#![allow(unused)]
fn main() {
let l_nu_growth_oxygen = -1.0656764137747288 + 1.3643097014167802 * p[2];
}
That is −1.066 g O2 per g glucose for full combustion, less 1.364 g for every g of biomass built: the electrons that end in biomass need no oxygen.
3. Read the structure
labinetix define balance model_scaffold/aerobic_batch --measured oxygen
model_scaffold/aerobic_batch — composition at the parameter defaults
species (atoms per formula unit, γ the degree of reduction):
M g/mol C H N O γ state
biomass 24.6263 1.000 1.800 0.200 0.500 4.200 c_x
glucose 180.1560 6.000 12.000 0.000 6.000 24.000 c_s
oxygen 31.9980 0.000 0.000 0.000 2.000 -4.000 c_o2
co2 44.0090 1.000 0.000 0.000 2.000 0.000 -
water 18.0150 0.000 2.000 0.000 1.000 0.000 -
ammonia 17.0310 0.000 3.000 1.000 0.000 0.000 -
stoichiometry (g per unit extent; * derived from the elements), electron residual (mol e⁻ per unit extent), degrees of freedom and the free yields (given coefficients that read parameters):
biomass glucose oxygen co2 water ammonia e⁻ dof free yields
growth 0.5000 -1.0000 -0.3835* 0.5722* 0.3805* -0.0692* -1.2e-16 2 biomass (y_xs)
reaction invariants of the states (2 of 3 states; concentrations, g/L):
+2.0000·c_x +1.0000·c_s
+0.7670·c_x +1.0000·c_o2
rates from the measured conversion rates of oxygen:
rank 1 of 1 reactions, redundancy 0: every reaction rate is determined
determined directions: 1; balance tests on the measured rates: 0
calculable conversion rates: biomass, glucose, co2, water, ammonia
not calculable: none
What it tells you:
- The stoichiometry, with the derived coefficients marked: growth takes 0.38 g O2 and makes 0.57 g CO2 per g glucose. The electron residual is zero to rounding.
- Degrees of freedom of each reaction: what you may choose (here two: the reference glucose and the biomass yield). The free yields are the given coefficients that read parameters: what an estimate fits; every derived one follows from them.
- Reaction invariants: combinations of states the reaction cannot change.
2 c_x + c_sstays constant in a batch: every gram of glucose consumed makes half a gram of biomass. These are what a kinetics-free observer can track without knowing any rate. - Which measurements determine the rates. With the oxygen uptake measured, the one reaction’s
rate is determined; a model with several reactions tells you which further species you must
measure (try
models/overflow_yeast, where oxygen and CO2 are not enough). Measuring more than the rates need leaves balance tests (the redundancy), and every species whose rate the measured ones fix is calculable. - An estimator’s instruments.
--estimator <name>reads the measured outputs ofmodels/<id>/estimators/<name>/(From a model to a released package) instead: each output that reads one species, an off-gas fraction, a concentration, the dissolved oxygen, counts as that species’ measured rate.
4. Gas phase, feeds and more reactions
The larger models in models/ show the rest:
-
models/aerobic_fed_batch: a growing volume fed with glucose ([liquid] volume = "v",[[feeds]]), growth and maintenance as two reactions, and a quasi-steady[gas]whose outputs are the off-gas O2 and CO2 fractions. Every species a reaction makes or takes without a state is listed either as a gas species (inlet) or asuntracked(water, ammonia). -
models/overflow_yeast: the Sonnleitner–Käppeli bottleneck, three reactions on one catalyst; the fermentation’s oxygen is given as0, so its ethanol and CO2 yields are derived. Its rates read the derived oxygen coefficients to turn a respiratory capacity into rates of glucose and ethanol. -
models/e4b_family_r: a model in modules. Its[model]names the organismorganisms/e4b.toml(E. coli with acetate overflow and re-uptake, four reactions) and the vesselvessels/pilot_fed_batch.toml(feed, titrant, oxygen transfer,[gas], and the[headspace]and[storage]declarations). The sections merge into one model with one hash.
Simulate them as in the first tutorial; their scenarios are in models/<id>/scenarios/. The
first two have an independent cross-check in crates/studio/analyze-model/tests/: the model
written a second time by hand, with closed-form yields, integrated by an adaptive solver.
e4b_family_r’s right-hand side is held to v1’s soft-sensor benchmark at sampled states
(crates/studio/analyze-fixtures/goldens/softsensor/family_r_rhs.json).
Next
Estimation and identifiability asks which parameters a measurement can determine, draws synthetic data, and estimates the parameters with their uncertainty. From a model to a released package takes a composed model from the store to a package taktwerk runs, and back from its record.
Modeling 3: estimation and identifiability
A model is only as good as its parameters, and data rarely determine all of them. In this tutorial you ask, before measuring anything, which parameters of the batch culture from Your first model a biomass measurement can determine; draw synthetic data; estimate the parameters with their uncertainty; and keep the result as a parameter set the store pins to the model and the data. Everything runs on the host, in Rust.
Do Your first model first: this page uses models/monod_batch and
its scenario batch.
1. Ask the design first
Suppose you can measure biomass once an hour with a standard deviation of 0.05 g/L. Which of
mu_max, k_s and y_xs can that determine?
labinetix analyze identifiability models/monod_batch/scenarios/batch.toml \
--params mu_max,k_s,y_xs --sigma biomass=0.05
parameter delta_msqr
mu_max 9.4561e1
k_s 1.5326e0
y_xs 5.6697e1
collinearity 5.557, determinant 4.5786e1
identifiable subset (collinearity <= 10): mu_max, y_xs, k_s
The measures are those of Brun, Reichert & Künsch (2001), from the sensitivities of the observed outputs to each parameter, scaled by the parameter and by the measurement’s standard deviation:
delta_msqr, the importance: how much the observations move when the parameter moves by its own size, in standard deviations.mu_maxandy_xsdominate;k_smatters about sixty times less, because it only shapes the few hours before the substrate runs out.- collinearity
γ: whether some combination of the parameters leaves the observations unchanged. Above 10 to 20, the data cannot tell them apart, whatever the noise. - determinant
ρ: the joint information of the set, a single number to compare designs. - identifiable subset: the parameters, taken in order of importance, whose collinearity
stays below the limit (
--gamma-max, 10 by default).
These values hold at the scenario’s parameters. Rerun with other values in the scenario’s
[parameters], other outputs in --sigma, or a scenario that samples more densely around
the end of the batch, and compare: that is designing the experiment.
2. Draw synthetic data
To test the estimation before real data exist, draw data from the model itself, with seeded noise, as a dataset entity of the store:
labinetix analyze synthesize models/monod_batch/scenarios/batch.toml \
--sigma biomass=0.05 --seed 1 --dataset batch-synthetic
labinetix define sync dataset/batch-synthetic
This writes models/monod_batch/datasets/batch-synthetic/: the CSV under data/
(t,output,value,sigma, one observation per line) and an authoring that records the
provenance and pins the CSV by its sha256. define sync locks it; define check refuses a
CSV that no longer matches. Real data take the same shape: put the CSV under data/ and
write the authoring with its hash. A dataset too large for git can leave the CSV out; the pin
stays.
# Synthetic: the prediction of models/monod_batch/scenarios/batch.toml with Gaussian noise (biomass=0.05), seed 1.
t,output,value,sigma
0.0,biomass,0.040829808447552234,0.05
1.0,biomass,0.11834098489341496,0.05
2.0,biomass,0.22948691093706208,0.05
…
3. A scenario to estimate on
Estimation integrates the model and its sensitivities to every parameter many times, at
parameter values far from the defaults. An explicit method like rk4 has a stability limit
that depends on the parameters: near the end of the batch the substrate decays at a rate of
mu_max · x / (k_s · y_xs), and at a small k_s that is fast enough to make the scenario’s
RK4 substeps unstable. The state survives, held at zero by its bound, but the sensitivities do
not, and the estimate refuses to report statistics computed from them:
labinetix: the sensitivities at the fit are not usable (standard errors [3.3997717618147165e-20, 3.0050020840821523e-21, 0.0]): an explicit solver step may be unstable at the fitted parameters; estimate on a scenario with method = "ros2", or with more substeps
ros2 is a stiff method (Verwer et al. 1999), stable at any rate. Copy the scenario to
models/monod_batch/scenarios/batch-ros2.toml and change its solver:
[solver]
method = "ros2"
step = 0.1
substeps = 4
steps = 120
record_every = 10
rk4 and euler are the methods a target runs; ros2 is the host’s, for analysis.
4. The experiment
An experiment says which scenario, which dataset, and which parameters to estimate within
which bounds. Write models/monod_batch/experiments/batch.toml:
[experiment]
scenario = "batch-ros2"
dataset = "batch-synthetic"
estimate = ["mu_max", "k_s", "y_xs"]
doc = "Growth rate, Monod constant and yield from hourly biomass samples."
[bounds]
mu_max = [0.05, 2.0]
k_s = [0.001, 5.0]
y_xs = [0.1, 1.0]
[fit]
starts = 20
seed = 1
[mcmc]
samples = 4000
burn_in = 1000
seed = 2
Parameters not listed keep the scenario’s values. An observation may be at any time within the scenario’s horizon: one between two steps adds an event step to it. The chain must keep at least ten samples per estimated parameter after the burn-in.
5. Estimate
labinetix analyze estimate models/monod_batch/experiments/batch.toml --write
labinetix define sync parameter_set/batch
Without --write the estimate is a dry run: it prints the parameter set and says whether it
differs from the committed authoring, which it leaves alone. Run again later, with nothing
changed, it reproduces the committed set:
dry run: parameter_set/batch is identical to the committed models/monod_batch/parameter_sets/batch/authoring/batch.toml; --write replaces it
The estimate takes about a minute and a half on a Raspberry Pi 5, most of it the MCMC.
What happens:
- Weighted least squares in the logarithm of the parameters, so they stay positive and
within their bounds: minimize
χ² = Σ ((y − value) / σ)². - Levenberg–Marquardt from the scenario’s values and from 19 random points within the bounds, with the exact sensitivities of the outputs to the parameters.
- The Fisher information at the best fit gives the standard errors and correlations (Cramér–Rao, σ known), and the identifiability of section 1, now at the fitted values.
- MCMC, because
[mcmc]is stated: adaptive Metropolis started at the fit with the Fisher covariance, a uniform prior within the bounds.
The result is the parameter set entity models/monod_batch/parameter_sets/batch/. Its
authoring holds everything, and pins what it came from:
[model]
entity = "model_scaffold/monod_batch"
version = "0.1.0"
content_hash = "sha256:8cd6…"
[dataset]
entity = "dataset/batch-synthetic"
version = "1.0.0"
content_hash = "sha256:9642…"
[experiment]
name = "batch"
content_hash = "sha256:d59a…"
[scenario]
name = "batch-ros2"
content_hash = "sha256:029a…"
[values]
mu_max = 0.4950322083020979
k_s = 0.02283696800910036
y_xs = 0.4995393236464814
[fit]
estimated = ["mu_max", "k_s", "y_xs"]
standard_errors = [0.022215099784811166, 0.3688345673900873, 0.0024160035599216454]
correlation = [[1.0, 0.996176238984639, 0.3735182442468857], [0.996176238984639, 1.0, 0.3756343622501496], [0.3735182442468857, 0.3756343622501496, 0.9999999999999999]]
chi2 = 9.076072903934577
n = 13
aic = 15.076072903934577
bic = 16.77092097631919
starts = 20
hits = 14
[identifiability]
parameters = ["mu_max", "k_s", "y_xs"]
delta_msqr = [70.74308698579979, 0.1967460725013617, 61.879206719127204]
collinearity = 16.184768558146317
determinant = 15.216725479842935
gamma_max = 10.0
subset = ["mu_max", "y_xs"]
[posterior]
samples = 4000
burn_in = 1000
acceptance = 0.1445
mean = [0.4955970456578878, 0.021451926187563686, 0.49794983324305864]
sd = [0.0027466628359791912, 0.029944138000744684, 0.002837483239080114]
q025 = [0.4917806897230583, 0.0011377876526486104, 0.49192451250360236]
q975 = [0.5045987365129262, 0.12445792927292575, 0.5031650284714936]
6. Read the result
The data were drawn at mu_max = 0.5, k_s = 0.1, y_xs = 0.5.
mu_maxandy_xsare determined: 0.495 ± 0.022 and 0.4995 ± 0.0024, within a fraction of a standard error of the truth.k_sis not: 0.023, with a standard error of 0.37, sixteen times the value. Its correlation withmu_maxis 0.996: a smallerk_swith a slightly smallermu_maxfits the hourly samples as well. At the fit the collinearity is 16, andk_sdrops out of the identifiable subset. Section 1 found all three identifiable atk_s = 0.1: identifiability depends on where in parameter space you are.- The posterior says the same without a linear approximation: the 95 % interval of
k_sruns from the lower bound 0.001 to 0.124 and contains the truth; those ofmu_maxandy_xsare narrow. Where the Fisher information and the posterior disagree, as fork_s, the posterior is the one to believe: the problem is not linear there. hits = 14: 14 of 20 starts ended at the same optimum (χ² within 0.1). Few hits mean a rugged cost; raisestarts.chi2 = 9.1forn − k = 10degrees of freedom is what a correct model with the right σ gives. A χ² far aboven − kmeans the model or the noise level is wrong.- AIC and BIC compare models fitted to the same data: lower is better, and the penalty for parameters keeps the comparison fair.
- A composed model’s set adds
[derived]: every coefficient the elements determine (nu_growth_oxygen = { value = …, standard_error = … }) at the fit, its standard error propagated from the estimated yields. Only the given coefficients are estimated.
The fix for k_s is a design question, back to section 1: sample more often near the end of
the batch, or measure the substrate.
7. The parameter set stays honest
The parameter set pins the model, the dataset, the experiment and the scenario. Change any of
them and define check refuses the parameter set until you re-estimate. Change the
experiment’s [fit] seed to 3 and check:
labinetix define check
labinetix: define check: invalid entity state: parameter_set/batch: …/models/monod_batch/experiments/batch.toml is sha256:aa05136844992e3a548a9f046809ac353fefb4c061626607e2154a102c27724b, and it pins sha256:d59aa9ab21903ac7a01441b8500e7548562535b48a04c1ee3bac4d2ff0fd306b: re-run `labinetix analyze estimate models/monod_batch/experiments/batch.toml --write` and sync it
Set the seed back to 1 and define check passes again.
Re-estimating with --write rewrites the authoring, keeping its version; raise contract_version by hand
when you publish a new set, and labinetix define tag parameter_set/batch tags it like any
entity. Committed parameter sets reproduce byte for byte on the same platform: the test suite
re-runs every experiment.
8. Several runs
Real data come in runs: cultivations with their own feed, their own start and their own
samples. A dataset of runs lists them, each with its observations, its recorded inputs
(t,input,value, each held from its time on), the state it started from, and where its data
come from: taktwerk (a record), pilot_plant (an export), literature (digitised, with a
citation), campaign (an earlier one) or synthetic. Provenance lives in the runs only; the
dataset’s doc says what the runs are for:
[dataset]
contract_version = "1.0.0"
model = "refolding"
doc = "Two fed-batch refolding runs with different feeds: native and aggregated protein."
[[run]]
name = "fed-a"
source = "taktwerk"
provenance = "taktwerk record 20261019T214133Z-ef645d5c of project `fed-a` …"
observations = { path = "data/fed-a.csv", sha256 = "sha256:4b87…" }
inputs = { path = "data/fed-a.inputs.csv", sha256 = "sha256:c992…" }
initial = { v = 1.0, c_u = 0.2, c_n = 0.0, c_a = 0.0 }
Each file is pinned by its sha256; a partner’s data can stay out of git, and an estimate then
asks for the file. An offline sample’s time is when it was drawn; an analysis delay belongs in
the run’s doc.
Two commands add runs, dry runs until --write; the first run of a new dataset also takes
--dataset-doc:
labinetix analyze import-record models/refolding/datasets/fed-refolding/record/fed-a/rows/<run-id> \
--model-dir models/refolding --dataset fed-refolding --run fed-a --inputs f_in \
--dataset-doc "Two fed-batch refolding runs with different feeds: native and aggregated protein." \
--observe c_n_out=0.01,c_a_out=0.01 --every 25 \
--project models/refolding/datasets/fed-refolding/record/fed-a.toml --write
labinetix analyze synthesize models/refolding/scenarios/fed-batch-late.toml \
--sigma c_n_out=0.01,c_a_out=0.01 --seed 7 --at 0.333,0.715,1.172,1.618,2.084,2.547,3.013,3.468,3.927 \
--dataset fed-refolding --run late-b --write
import-record reads a taktwerk run directory: the observed outputs every --every-th row,
the recorded inputs from the tick that used them (the recorder must keep every tick), and the
initial state from the project file the record pins by hash. synthesize --run records the
scenario’s initial state and inputs as the run’s own.
An experiment on a dataset of runs simulates each run with the scenario’s solver and
parameters, from the run’s initial state and with its recorded inputs in place of the
scenario’s schedule of them, and fits all runs at once. estimate is shared by every run;
estimate_per_run gives each run its own value of a parameter, estimate_initial its own
initial value of a state (bounds in [initial_bounds]). A parameter that a composed model’s
stoichiometric coefficient reads, such as a key yield, is refused per run: the derived
coefficients follow from one value of it:
[experiment]
scenario = "fed-batch"
dataset = "fed-refolding"
estimate = ["k_f", "k_a"]
estimate_per_run = ["c_u_in"]
estimate_initial = ["c_u"]
The parameter set names the run-specific estimates c_u_in[fed-a] and c_u(0)[fed-a] in
[fit], keeps the scenario’s value of a per-run parameter in [values], and adds a table per
run with its observations, its share of χ² and its values:
[runs.fed-a]
n = 32
chi2 = 0.5387681782036545
parameters = { c_u_in = 10.001911356841529 }
initial = { c_u = 0.20002006164613753 }
models/refolding holds this example: two fed-batch refolding runs, one a taktwerk record of
the model’s raw package, the other synthetic samples between the scenario’s steps.
Where to go from here
models/do_probe_klais the dynamic method of measuring kLa, with an experiment that recovers kLa, the uptake rate and the probe’s time constant, and a second scenario where a slow probe makes kLa and its lag impossible to separate.- Composition and balances: models whose yields cannot drift out of the elemental balances, so an estimation cannot produce one that creates electrons.
- From a model to a released package: a model’s generated package under taktwerk,
with a parameter set’s estimates as the instance’s values (
define instance --parameter-set). - CLI reference: every
defineandanalyzecommand.
CLI reference
The root is exactly the three stages — define analyze build — and later work adds leaves under a stage rather than a fourth root. Each stage calls the studio crate that owns it as an in-process library call. Hidden commands are tooling and are not listed here.
Commands
| Command | Summary |
|---|---|
labinetix | Define, analyze and build models into taktwerk packages |
labinetix define | Author, verify and release the models of the model store (crates/studio/define-cli) |
labinetix define sync | Regenerate views and lockfiles for one or more entities |
labinetix define verify | Recompute hashes and compare against lockfiles without writing files |
labinetix define reindex | Rebuild the aggregate store index store.lock.json from the entity lockfiles |
labinetix define check | CI integrity gate: every entity re-emitted and byte-compared with its committed views and generated files, then its lockfile pins and references verified |
labinetix define doctor | Preflight: every declared emitter is one this binary links; the aggregate lock parses |
labinetix define list | List every entity row in the aggregate lockfile (tab-separated, script-friendly) |
labinetix define show | Show one entity’s aggregate row and key lockfile fields (tab-separated key/value lines), then what it pins and what pins it |
labinetix define balance | Print a composed model’s structural report: elemental and stoichiometric matrices, electron balances, degrees of freedom, reaction invariants, and which reaction rates the --measured species determine |
labinetix define instance | Print a model’s raw package as one taktwerk [[instance]] table: every parameter of the package with its start value — the model’s defaults, then a model scenario’s, then a parameter set’s — and where each came from. A value nothing gives is refused, as is a scenario or parameter set whose model pin no longer holds |
labinetix define resolve | Resolve a soft-sensor process: check its process, plant and catalog files, and print for each enabled plugin the declaration, instrument, actuator or plugin that supplies each quantity it requires, down to the instrument, and every value with its source |
labinetix define get | Resolve a generated view path for a consumer (fail-closed hash verify by default) |
labinetix define diff | Semantic IR diff between two git refs of a model (diagnostic; not the release baseline) |
labinetix define new | Scaffold a new model: authoring skeleton, synced views and lockfile, aggregate row |
labinetix define publish | Author happy path: sync → verify → classify → version → commit |
labinetix define tag | Tag the current authoring contract_version at HEAD (no SemVer bump) |
labinetix define release | Declare a contract version, enforce the minimum SemVer bump, refresh lockfiles, and optionally tag |
labinetix analyze | Simulate and estimate models on the host (crates/studio/analyze-*) |
labinetix analyze simulate | Simulate a model over a model scenario (models/<id>/scenarios/<name>.toml) |
labinetix analyze synthesize | Draw a synthetic dataset from a model scenario’s prediction |
labinetix analyze identifiability | The practical identifiability of parameters over a model scenario, before any data |
labinetix analyze estimate | Estimate a model’s parameters from an experiment (models/<id>/experiments/<name>.toml) |
labinetix analyze import-record | Import a taktwerk record as a run of a dataset of runs |
labinetix analyze replay | Re-run a taktwerk record’s inputs on the host and compare every recorded output bit for bit |
labinetix build | Build the raw model packages per target (crates/studio/build-xtask) |
labinetix build target | Validate the target files and print one target |
labinetix build raw-package | Cross-build every raw model package’s library for one target into its package |
labinetix serve | Show the model store read-only in the taktwerk workbench (crates/studio/serve) |
labinetix
Define, analyze and build models into taktwerk packages
Usage: labinetix <COMMAND>
Subcommands
| Subcommand | Summary |
|---|---|
define | Author, verify and release the models of the model store (crates/studio/define-cli) |
analyze | Simulate and estimate models on the host (crates/studio/analyze-*) |
build | Build the raw model packages per target (crates/studio/build-xtask) |
serve | Show the model store read-only in the taktwerk workbench (crates/studio/serve) |
Options
| Option | Description |
|---|---|
-h, --help | Print help |
-V, --version | Print version |
labinetix define
Author, verify and release the models of the model store (crates/studio/define-cli)
Usage: labinetix define <COMMAND>
Subcommands
| Subcommand | Summary |
|---|---|
sync | Regenerate views and lockfiles for one or more entities |
verify | Recompute hashes and compare against lockfiles without writing files |
reindex | Rebuild the aggregate store index store.lock.json from the entity lockfiles |
check | CI integrity gate: every entity re-emitted and byte-compared with its committed views and generated files, then its lockfile pins and references verified |
doctor | Preflight: every declared emitter is one this binary links; the aggregate lock parses |
list | List every entity row in the aggregate lockfile (tab-separated, script-friendly) |
show | Show one entity’s aggregate row and key lockfile fields (tab-separated key/value lines), then what it pins and what pins it |
balance | Print a composed model’s structural report: elemental and stoichiometric matrices, electron balances, degrees of freedom, reaction invariants, and which reaction rates the --measured species determine |
instance | Print a model’s raw package as one taktwerk [[instance]] table: every parameter of the package with its start value — the model’s defaults, then a model scenario’s, then a parameter set’s — and where each came from. A value nothing gives is refused, as is a scenario or parameter set whose model pin no longer holds |
resolve | Resolve a soft-sensor process: check its process, plant and catalog files, and print for each enabled plugin the declaration, instrument, actuator or plugin that supplies each quantity it requires, down to the instrument, and every value with its source |
get | Resolve a generated view path for a consumer (fail-closed hash verify by default) |
diff | Semantic IR diff between two git refs of a model (diagnostic; not the release baseline) |
new | Scaffold a new model: authoring skeleton, synced views and lockfile, aggregate row |
publish | Author happy path: sync → verify → classify → version → commit |
tag | Tag the current authoring contract_version at HEAD (no SemVer bump) |
release | Declare a contract version, enforce the minimum SemVer bump, refresh lockfiles, and optionally tag |
Options
| Option | Description |
|---|---|
-h, --help | Print help |
labinetix define sync
Regenerate views and lockfiles for one or more entities
Usage: labinetix define sync [OPTIONS] [SELECTOR]
Arguments
| Argument | Description |
|---|---|
[SELECTOR] | Entity selector <type>/<id> (for example model_scaffold/chemostat). Mutually exclusive with --all |
Options
| Option | Description |
|---|---|
--all | Regenerate every entity in the store, in dependency-safe order, and rebuild store.lock.json from that walk |
--no-commit | Skip the scoped git commit (used by tests and dry runs) |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define verify
Recompute hashes and compare against lockfiles without writing files
Usage: labinetix define verify [OPTIONS] [SELECTOR]
Arguments
| Argument | Description |
|---|---|
[SELECTOR] | Entity selector <type>/<id> |
Options
| Option | Description |
|---|---|
--all | Verify every entity in the store |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define reindex
Rebuild the aggregate store index store.lock.json from the entity lockfiles.
The index is a walk of the store, so this runs no emitter and touches no entity. Placement is checked while walking: an entity that does not sit where its content puts it fails here exactly as it fails verify.
Usage: labinetix define reindex [OPTIONS]
Options
| Option | Description |
|---|---|
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define check
CI integrity gate: every entity re-emitted and byte-compared with its committed views and generated files, then its lockfile pins and references verified
Usage: labinetix define check [OPTIONS]
Options
| Option | Description |
|---|---|
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define doctor
Preflight: every declared emitter is one this binary links; the aggregate lock parses.
An emitter is a linked crate named by a builtin: token, so there is nothing to probe and nothing to install (DESIGN.md § Stages). For each adapters/<type>.toml that declares an [emitter], this asks the same resolver labinetix define sync dispatches on whether the declared token names an emitter this binary links and one that writes views for that contract type — so a token this accepts is a token sync can call. Then store.lock.json is read: a missing or unparseable aggregate lock fails.
emitter.version is reported, never compared for pass/fail: it records which emitter version wrote that type’s committed views, the number its locks pin.
Intended for CI / local checks before full drift regen (labinetix define check).
Usage: labinetix define doctor [OPTIONS]
Options
| Option | Description |
|---|---|
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define list
List every entity row in the aggregate lockfile (tab-separated, script-friendly).
Output format: one row per entity, columns separated by TAB: type, id, contract_version, content_hash, lock_path. Rows are sorted by type then id (as stored in store.lock.json).
define list roles lists the role vocabulary beside the catalog (catalog/roles.toml) instead: one row per role, role, its constraints (; -separated, or not checked: …), the catalog entries and plants that name it (,-separated), its meaning.
Usage: labinetix define list [OPTIONS] [WHAT]
Arguments
| Argument | Description |
|---|---|
[WHAT] | What to list instead of the store’s entities — one of roles. |
Options
| Option | Description |
|---|---|
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define show
Show one entity’s aggregate row and key lockfile fields (tab-separated key/value lines), then what it pins and what pins it.
Output format: one field per line as key<TAB>value. Includes aggregate index fields plus emitter, authoring, views, references, and source pins from the entity lockfile; a node the index has no row for (a module, a scenario, an experiment, a catalog entry, the role vocabulary) shows its type, id, path and content hash. Then one line per pin, pins.<i> and pinned_by.<i>: the other entity, what the pin is, and its state (fresh, stale when the hash it records is not the target’s now, missing). role/<name> shows a role of the vocabulary: its meaning, constraints, the catalog entries and plants that name it, each process that binds it and the species.
Usage: labinetix define show [OPTIONS] <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | <type>/<id>, organism/<id>, vessel/<id>, scenario/<model>/<name>, experiment/<model>/<name>, catalog/<kind>/<id>, vocabulary/roles, or role/<name> — required. |
Options
| Option | Description |
|---|---|
--used-by | Only what pins it, transitively, as an indented tree (an organism: its models, their scenarios, parameter sets and processes) |
--json | Its relations as JSON, both directions, transitively: what labinetix serve serves at /api/entities/<kind>/<id>/relations |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define balance
Print a composed model’s structural report: elemental and stoichiometric matrices, electron balances, degrees of freedom, reaction invariants, and which reaction rates the --measured species determine
Usage: labinetix define balance [OPTIONS] <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector model_scaffold/<id> — required. |
Options
| Option | Description |
|---|---|
--measured <MEASURED>... | Species whose conversion rates are measured, comma-separated |
--estimator <ESTIMATOR> | An estimator of the model, <store>/<id>/estimators/<name>/: its measured outputs, each that reads one species counted as that species’ measured conversion rate |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define instance
Print a model’s raw package as one taktwerk [[instance]] table: every parameter of the package with its start value — the model’s defaults, then a model scenario’s, then a parameter set’s — and where each came from. A value nothing gives is refused, as is a scenario or parameter set whose model pin no longer holds
Usage: labinetix define instance [OPTIONS] <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector model_scaffold/<id> — required. |
Options
| Option | Description |
|---|---|
--scenario <SCENARIO> | A model scenario, <store>/<id>/scenarios/<name>.toml: its solver settings, initial state, parameter overrides and inputs |
--parameter-set <PARAMETER_SET> | A parameter set, <store>/<id>/parameter_sets/<name>/: its estimated values |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define resolve
Resolve a soft-sensor process: check its process, plant and catalog files, and print for each enabled plugin the declaration, instrument, actuator or plugin that supplies each quantity it requires, down to the instrument, and every value with its source.
process/<id> lives beside the store (processes/<id>/, its plant in plants/); --path names a process directory elsewhere (a site’s repository, <repo>/processes/<id>), resolved against this store’s models and catalog; plant/<id> checks a plant alone. A dry run by default: it says whether the resolved view and the locks are current.
Usage: labinetix define resolve [OPTIONS] [SELECTOR]
Arguments
| Argument | Description |
|---|---|
[SELECTOR] | process/<id> or plant/<id> |
Options
| Option | Description |
|---|---|
--path <DIR> | A process directory, <repo>/processes/<id>, with its plants in <repo>/plants/ |
--write | Write the resolved view and the process’s and plant’s views and locks |
--tree | Print only the requirement tree: one line per quantity with ✓ or ✗ and what supplies it, declarations and tunables folded to counts; a process that does not resolve shows how far it got. Coloured when stdout is a terminal (NO_COLOR turns it off) |
--json | Print the resolution as JSON, as labinetix serve serves it: the view as far as it resolves, every refusal, stale files, the plant, the entries, each value’s file and line, and the block diagram (sources, a block per plugin, sinks, a wire per match) |
--plugin <KIND/ID> | Show one enabled plugin only, <kind>/<id> |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define get
Resolve a generated view path for a consumer (fail-closed hash verify by default).
Prints the absolute (or repo-resolved) view path on success. With --verify-hash (documented happy path; verification is on unless --no-verify), recomputes the view byte hash and compares it to the entity lockfile — mismatch exits non-zero. --out <path> copies the verified bytes to the destination.
Usage: labinetix define get [OPTIONS] --view <VIEW> <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector <type>/<id> (for example model_scaffold/chemostat) — required. |
Options
| Option | Description |
|---|---|
--view <VIEW> | View name from the entity lockfile / adapter (for example ir or julia) — required. |
--verify-hash | Verify view bytes against the entity lockfile (documented consume path) |
--no-verify | Skip lockfile hash verification (escape hatch; not the consume happy path) |
--out <OUT> | Copy verified view bytes to this path |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define diff
Semantic IR diff between two git refs of a model (diagnostic; not the release baseline)
Usage: labinetix define diff [OPTIONS] <SELECTOR> <REF_A> <REF_B>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector <type>/<id> — required. |
<REF_A> | First git ref (commit, tag, branch, or HEAD~N) — required. |
<REF_B> | Second git ref — required. |
Options
| Option | Description |
|---|---|
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define new
Scaffold a new model: authoring skeleton, synced views and lockfile, aggregate row
Usage: labinetix define new [OPTIONS] <CONTRACT_TYPE> <ID>
Arguments
| Argument | Description |
|---|---|
<CONTRACT_TYPE> | Contract type: model_scaffold — required. |
<ID> | Entity id (for example my_model) — required. |
Options
| Option | Description |
|---|---|
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help |
labinetix define publish
Author happy path: sync → verify → classify → version → commit.
Classifies against the release baseline (latest namespaced tag, else first-release from 0.0.0). Proposes the minimum SemVer unless --set overrides.
--dry-run prints the plan and never mutates authoring, lockfiles, commits, or tags.
Usage: labinetix define publish [OPTIONS] <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector <type>/<id> — required. |
Options
| Option | Description |
|---|---|
--set <SET> | Override the proposed minimum SemVer (must be >= classified minimum) |
--dry-run | Print the plan only; never mutate disk, commits, or tags |
--create-tag | Create a local annotated tag <type>/<id>/vX.Y.Z. Off by default |
--push-tag | Push the created tag to origin (requires --create-tag). Off by default |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define tag
Tag the current authoring contract_version at HEAD (no SemVer bump).
Use for first cut / backfill when authoring already carries the intended version.
A process/<id> or plant/<id> beside the store is tagged once its views and lock are what define resolve writes; its lock gets no source pin.
For a model record (a dataset, a parameter set, an estimator), the tag is also what earns the entity lockfile its source pin: once the tag exists, the lock is rewritten with the tag and the commit it names. sync never writes that pin — an untagged entity’s lock has no source block. The rewritten lock is left uncommitted, because the commit the tag names cannot carry a pin naming itself; commit it next. Refuses, before cutting anything, an entity whose lockfile is missing or records another contract_version than its authoring file.
Usage: labinetix define tag [OPTIONS] <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector <type>/<id> — required. |
Options
| Option | Description |
|---|---|
--push | Push the created tag to origin. Off by default |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix define release
Declare a contract version, enforce the minimum SemVer bump, refresh lockfiles, and optionally tag.
Classifies the semantic IR diff since the last namespaced tag (<type>/<id>/vX.Y.Z) and refuses under-bumps. Updates contract_version in authoring and lockfiles via sync. Prefer labinetix define publish for the author happy path.
--dry-run prints the plan and never mutates. --no-commit still writes authoring/lockfiles (tests / power users); do not use it as a dry-run.
Tag gate: git tags are never created unless you pass --create-tag. CI and just check do not invoke this command.
Usage: labinetix define release [OPTIONS] --set <SET> <SELECTOR>
Arguments
| Argument | Description |
|---|---|
<SELECTOR> | Entity selector <type>/<id> — required. |
Options
| Option | Description |
|---|---|
--set <SET> | Human-declared SemVer for this release (must be >= classified minimum) — required. |
--create-tag | Create a local annotated tag <type>/<id>/vX.Y.Z. Off by default |
--push-tag | Push the created tag to origin (requires --create-tag). Off by default |
--no-commit | Skip the scoped git commit (still mutates authoring/lockfiles). Prefer --dry-run |
--dry-run | Print the plan only; never mutate authoring, lockfiles, commits, or tags |
--store <DIR> | Model store root — the directory holding one directory per model and store.lock.json. Relative to the working directory: models from a clone root — default models. |
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze
Simulate and estimate models on the host (crates/studio/analyze-*).
simulate, synthesize, identifiability and estimate work on a model and a model scenario (models/<id>/scenarios/) (crates/studio/analyze-model); ros2 is a method of the host’s only.
Usage: labinetix analyze <COMMAND>
Subcommands
| Subcommand | Summary |
|---|---|
simulate | Simulate a model over a model scenario (models/<id>/scenarios/<name>.toml) |
synthesize | Draw a synthetic dataset from a model scenario’s prediction |
identifiability | The practical identifiability of parameters over a model scenario, before any data |
estimate | Estimate a model’s parameters from an experiment (models/<id>/experiments/<name>.toml) |
import-record | Import a taktwerk record as a run of a dataset of runs |
replay | Re-run a taktwerk record’s inputs on the host and compare every recorded output bit for bit |
Options
| Option | Description |
|---|---|
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze simulate
Simulate a model over a model scenario (models/<id>/scenarios/<name>.toml).
Steps the model’s generated kernel through core-integrate on the host, from the scenario’s initial point over its horizon, applying its input schedule, and writes the recorded rows as JSON: time, state, inputs and outputs. The scenario pins the model’s content hash; the same model and scenario give the same bytes, which is what the committed <name>.prediction.json beside the scenario is held to.
Usage: labinetix analyze simulate [OPTIONS] <SCENARIO>
Arguments
| Argument | Description |
|---|---|
<SCENARIO> | The model scenario file, models/<id>/scenarios/<name>.toml — required. |
Options
| Option | Description |
|---|---|
--out <OUT> | Write the prediction to this file instead of standard output |
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze synthesize
Draw a synthetic dataset from a model scenario’s prediction.
Every recorded row, every output named in --sigma with seeded Gaussian noise of that standard deviation, as the CSV t,output,value,sigma a dataset entity pins. Data for testing an estimation and designing an experiment, never a measurement.
--at observes at the times given instead, any time within the horizon. With --dataset and --run, the draw is added as a run of a dataset of runs, the scenario’s initial state and inputs recorded as the run’s own (a dry run without --write).
Usage: labinetix analyze synthesize [OPTIONS] --sigma <SIGMA> <SCENARIO>
Arguments
| Argument | Description |
|---|---|
<SCENARIO> | The model scenario file, models/<id>/scenarios/<name>.toml — required. |
Options
| Option | Description |
|---|---|
--sigma <SIGMA>... | Standard deviation per observed output, output=value, comma-separated — required. |
--seed <SEED> | The noise’s seed — default 1. |
--out <OUT> | Write the dataset to this file instead of standard output |
--dataset <DATASET> | Write it as the dataset entity models/<model>/datasets/<NAME>/ instead, its data pinned by sha256; then labinetix define sync dataset/<NAME> |
--at <AT>... | Observe at these times, comma-separated, instead of at every recorded row |
--run <RUN> | With --dataset: add the draw as run NAME of a dataset of runs |
--dataset-doc <DATASET_DOC> | With --run, for a new dataset: what its runs are for, its [dataset].doc |
--write | With --run: write the run; without, print what would be written |
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze identifiability
The practical identifiability of parameters over a model scenario, before any data.
The outputs in --sigma observed at every recorded row with those standard deviations: Brun et al.’s importance (delta_msqr) per parameter, collinearity and determinant of the set, and the largest subset whose collinearity stays below --gamma-max.
Usage: labinetix analyze identifiability [OPTIONS] --params <PARAMS> --sigma <SIGMA> <SCENARIO>
Arguments
| Argument | Description |
|---|---|
<SCENARIO> | The model scenario file, models/<id>/scenarios/<name>.toml — required. |
Options
| Option | Description |
|---|---|
--params <PARAMS>... | The parameters, comma-separated — required. |
--sigma <SIGMA>... | Standard deviation per observed output, output=value, comma-separated — required. |
--gamma-max <GAMMA_MAX> | The collinearity an identifiable subset may reach — default 10. |
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze estimate
Estimate a model’s parameters from an experiment (models/<id>/experiments/<name>.toml).
Weighted least squares in the logarithm of the parameters, Levenberg–Marquardt from the scenario’s values and from random starts within the bounds, with exact sensitivities; computes the parameter set: values, standard errors and correlations from the Fisher information, chi-square, AIC, BIC, and the identifiability at the fit. The set is the authoring of the parameter_set entity named after the experiment (models/<id>/parameter_sets/<name>/authoring/<name>.toml). A dry run by default: prints the set and says whether it differs from the committed authoring. --write writes it there; lock it with labinetix define sync parameter_set/<name>.
Usage: labinetix analyze estimate [OPTIONS] <EXPERIMENT>
Arguments
| Argument | Description |
|---|---|
<EXPERIMENT> | The experiment file, models/<id>/experiments/<name>.toml — required. |
Options
| Option | Description |
|---|---|
--write | Write the parameter set into the entity’s authoring, replacing the committed one |
--out <OUT> | Write the parameter set to this file instead of standard output |
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze import-record
Import a taktwerk record as a run of a dataset of runs.
Reads the run directory a taktwerk recorder wrote (run.json, part-*.csv.gz) and writes the run’s observations (the outputs in --observe, every --every-th row, with those standard deviations), its recorded inputs (--inputs, each held from the tick that used it) and its initial state (from --project, the project file the record pins by hash, and --initial) under models/<id>/datasets/<NAME>/data/, and appends the run to the dataset’s authoring, creating it if needed. A dry run by default; --write writes. Then lock it with labinetix define sync dataset/<NAME>.
Usage: labinetix analyze import-record [OPTIONS] --model-dir <MODEL_DIR> --dataset <DATASET> --run <RUN> --observe <OBSERVE> <RECORD>
Arguments
| Argument | Description |
|---|---|
<RECORD> | The run directory, <dir>/<project>/<recorder>/<run-id>/ — required. |
Options
| Option | Description |
|---|---|
--model-dir <MODEL_DIR> | The model’s directory, models/<id> — required. |
--dataset <DATASET> | The dataset of runs to add the run to — required. |
--dataset-doc <DATASET_DOC> | For a new dataset: what its runs are for, its [dataset].doc |
--run <RUN> | The run’s name — required. |
--instance <INSTANCE> | The instance whose signals are read; the model’s id by default |
--inputs <INPUTS>... | Model inputs to keep as recorded inputs, comma-separated |
--observe <OBSERVE>... | Standard deviation per observed output, output=value, comma-separated — required. |
--every <EVERY> | Keep every N-th row as a sample — default 1. |
--project <PROJECT> | The project file the record was run from, for the initial state |
--initial <INITIAL>... | Initial states, state=value, comma-separated; they win over the project’s |
--doc <DOC> | A note on the run, such as an offline sample’s analysis delay |
--write | Write the run; without, print what would be written |
-h, --help | Print help (see more with ‘–help’) |
labinetix analyze replay
Re-run a taktwerk record’s inputs on the host and compare every recorded output bit for bit.
Steps each instance of a generated model or estimator package through the package’s own Rust, with the inputs the record holds (a recorder with signals = ["@io"], every = 1), the tunables of events.csv and the parameters of --project, the project file the record pins by hash. Prints one line per instance; exits non-zero when a value differs. A run with scenario faults, skipped ticks or a swapped model is refused.
Usage: labinetix analyze replay [OPTIONS] --project <PROJECT> <RECORD>
Arguments
| Argument | Description |
|---|---|
<RECORD> | The run directory, <dir>/<project>/<recorder>/<run-id>/ — required. |
Options
| Option | Description |
|---|---|
--project <PROJECT> | The project file the run was started from — required. |
--instance <INSTANCE> | Replay this instance only |
-h, --help | Print help (see more with ‘–help’) |
labinetix build
Build the raw model packages per target (crates/studio/build-xtask)
Usage: labinetix build <COMMAND>
Subcommands
| Subcommand | Summary |
|---|---|
target | Validate the target files and print one target |
raw-package | Cross-build every raw model package’s library for one target into its package |
Options
| Option | Description |
|---|---|
-h, --help | Print help |
labinetix build target
Validate hardware/targets/<name>.toml and print one target: triple, glibc floor, zigbuild
target, CPU.
With no TARGET_NAME this validates every target file and lists them.
Usage: labinetix build target [TARGET_NAME]
Arguments
| Argument | Description |
|---|---|
[TARGET_NAME] | Target name from hardware/targets/, never a triple; omit to list every target |
Options
| Option | Description |
|---|---|
-h, --help | Print help (see more with ‘–help’) |
labinetix build raw-package
Cross-build the C library of every raw model package for TARGET: the hand-written
crates/packages/raw-* and the generated models/*/generated/raw.
A raw model package is a taktwerk model: a C header, a descriptor and one library per
architecture under package/lib/<arch>/, which this command fills. Every library is
checked against the target’s glibc pin.
Needs zig and cargo-zigbuild on PATH.
Usage: labinetix build raw-package <TARGET_NAME>
Arguments
| Argument | Description |
|---|---|
<TARGET_NAME> | Target name from hardware/targets/, never a triple — required. |
Options
| Option | Description |
|---|---|
-h, --help | Print help (see more with ‘–help’) |
labinetix serve
Show the model store read-only in the taktwerk workbench (crates/studio/serve).
Not a stage: it serves what the stages’ crates read (entities and locks, models, balances, scenarios, estimators, packages, the soft-sensor declarations and their resolution) on loopback, and announces itself so that taktwerk serve lists it under Add-ons. Prints the page’s address with its token. Writes nothing: a what-if on the declarations is resolved in memory.
Usage: labinetix serve [OPTIONS] [REPO]
Arguments
| Argument | Description |
|---|---|
[REPO] | The labinetix repository whose store to show — default .. |
Options
| Option | Description |
|---|---|
--listen <LISTEN> | A loopback address and port; port 0 picks a free one — default 127.0.0.1:0. |
--declarations <DIR>... | A site’s folder of declarations (<dir>/processes/, <dir>/plants/, for example a deployment repository’s deploy/declarations), shown beside the repository’s own and resolved against its models and catalog, as define resolve --path does. Read in place, never written. Repeatable |
-h, --help | Print help (see more with ‘–help’) |