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

Labinetix

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/&lt;package&gt;-&lt;version&gt;-&lt;arch&gt;" --> 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

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: 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 yield y_xs is the one given coefficient; oxygen, CO2, water and ammonia follow from the elements.
  • aerobic_fed_batch.lock.json, the content hash and version;
  • generated/, what labinetix define sync writes from it: the IR, a ModelingToolkit view and the raw package, a crate with its header and descriptor. The kernel itself goes to crates/kernels/core-models/generated/.
  • scenarios/ and estimators/, 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:

structmemberstaktwerk causality
lowpass_paramsy0, time_factorparameter: fixed at init
lowpass_inputsuinput
lowpass_tunablestau (s)tunable: changeable while it runs
lowpass_outputsy, e = u − youtput

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

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:

  1. Your first model (this page): author, check, generate, simulate.
  2. Composition and balances: build a model from species and reactions, and let the elemental balances check it.
  3. Estimation and identifiability: ask what data can tell you about the parameters, and estimate them.
  4. 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:

TableWhat 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_std Rust 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). Its constant-feed scenario 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). Its luus-20-stages scenario 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). Its table-1-idp-sqp scenario 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:

TableWhat 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_s stays 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 of models/<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 as untracked (water, ammonia).

  • models/overflow_yeast: the Sonnleitner–Käppeli bottleneck, three reactions on one catalyst; the fermentation’s oxygen is given as 0, 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 organism organisms/e4b.toml (E. coli with acetate overflow and re-uptake, four reactions) and the vessel vessels/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_max and y_xs dominate; k_s matters 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:

  1. Weighted least squares in the logarithm of the parameters, so they stay positive and within their bounds: minimize χ² = Σ ((y − value) / σ)².
  2. 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.
  3. 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.
  4. 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_max and y_xs are determined: 0.495 ± 0.022 and 0.4995 ± 0.0024, within a fraction of a standard error of the truth.
  • k_s is not: 0.023, with a standard error of 0.37, sixteen times the value. Its correlation with mu_max is 0.996: a smaller k_s with a slightly smaller mu_max fits the hourly samples as well. At the fit the collinearity is 16, and k_s drops out of the identifiable subset. Section 1 found all three identifiable at k_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_s runs from the lower bound 0.001 to 0.124 and contains the truth; those of mu_max and y_xs are narrow. Where the Fisher information and the posterior disagree, as for k_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; raise starts.
  • chi2 = 9.1 for n − k = 10 degrees of freedom is what a correct model with the right σ gives. A χ² far above n − k means 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_kla is 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 define and analyze command.

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

CommandSummary
labinetixDefine, analyze and build models into taktwerk packages
labinetix defineAuthor, verify and release the models of the model store (crates/studio/define-cli)
labinetix define syncRegenerate views and lockfiles for one or more entities
labinetix define verifyRecompute hashes and compare against lockfiles without writing files
labinetix define reindexRebuild the aggregate store index store.lock.json from the entity lockfiles
labinetix define checkCI 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 doctorPreflight: every declared emitter is one this binary links; the aggregate lock parses
labinetix define listList every entity row in the aggregate lockfile (tab-separated, script-friendly)
labinetix define showShow one entity’s aggregate row and key lockfile fields (tab-separated key/value lines), then what it pins and what pins it
labinetix define balancePrint 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 instancePrint 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 resolveResolve 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 getResolve a generated view path for a consumer (fail-closed hash verify by default)
labinetix define diffSemantic IR diff between two git refs of a model (diagnostic; not the release baseline)
labinetix define newScaffold a new model: authoring skeleton, synced views and lockfile, aggregate row
labinetix define publishAuthor happy path: sync → verify → classify → version → commit
labinetix define tagTag the current authoring contract_version at HEAD (no SemVer bump)
labinetix define releaseDeclare a contract version, enforce the minimum SemVer bump, refresh lockfiles, and optionally tag
labinetix analyzeSimulate and estimate models on the host (crates/studio/analyze-*)
labinetix analyze simulateSimulate a model over a model scenario (models/<id>/scenarios/<name>.toml)
labinetix analyze synthesizeDraw a synthetic dataset from a model scenario’s prediction
labinetix analyze identifiabilityThe practical identifiability of parameters over a model scenario, before any data
labinetix analyze estimateEstimate a model’s parameters from an experiment (models/<id>/experiments/<name>.toml)
labinetix analyze import-recordImport a taktwerk record as a run of a dataset of runs
labinetix analyze replayRe-run a taktwerk record’s inputs on the host and compare every recorded output bit for bit
labinetix buildBuild the raw model packages per target (crates/studio/build-xtask)
labinetix build targetValidate the target files and print one target
labinetix build raw-packageCross-build every raw model package’s library for one target into its package
labinetix serveShow 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

SubcommandSummary
defineAuthor, verify and release the models of the model store (crates/studio/define-cli)
analyzeSimulate and estimate models on the host (crates/studio/analyze-*)
buildBuild the raw model packages per target (crates/studio/build-xtask)
serveShow the model store read-only in the taktwerk workbench (crates/studio/serve)

Options

OptionDescription
-h, --helpPrint help
-V, --versionPrint version

labinetix define

Author, verify and release the models of the model store (crates/studio/define-cli)

Usage: labinetix define <COMMAND>

Subcommands

SubcommandSummary
syncRegenerate views and lockfiles for one or more entities
verifyRecompute hashes and compare against lockfiles without writing files
reindexRebuild the aggregate store index store.lock.json from the entity lockfiles
checkCI integrity gate: every entity re-emitted and byte-compared with its committed views and generated files, then its lockfile pins and references verified
doctorPreflight: every declared emitter is one this binary links; the aggregate lock parses
listList every entity row in the aggregate lockfile (tab-separated, script-friendly)
showShow one entity’s aggregate row and key lockfile fields (tab-separated key/value lines), then what it pins and what pins it
balancePrint 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
instancePrint 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
resolveResolve 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
getResolve a generated view path for a consumer (fail-closed hash verify by default)
diffSemantic IR diff between two git refs of a model (diagnostic; not the release baseline)
newScaffold a new model: authoring skeleton, synced views and lockfile, aggregate row
publishAuthor happy path: sync → verify → classify → version → commit
tagTag the current authoring contract_version at HEAD (no SemVer bump)
releaseDeclare a contract version, enforce the minimum SemVer bump, refresh lockfiles, and optionally tag

Options

OptionDescription
-h, --helpPrint help

labinetix define sync

Regenerate views and lockfiles for one or more entities

Usage: labinetix define sync [OPTIONS] [SELECTOR]

Arguments

ArgumentDescription
[SELECTOR]Entity selector <type>/<id> (for example model_scaffold/chemostat). Mutually exclusive with --all

Options

OptionDescription
--allRegenerate every entity in the store, in dependency-safe order, and rebuild store.lock.json from that walk
--no-commitSkip 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, --helpPrint help

labinetix define verify

Recompute hashes and compare against lockfiles without writing files

Usage: labinetix define verify [OPTIONS] [SELECTOR]

Arguments

ArgumentDescription
[SELECTOR]Entity selector <type>/<id>

Options

OptionDescription
--allVerify 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, --helpPrint 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

OptionDescription
--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, --helpPrint 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

OptionDescription
--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, --helpPrint 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

OptionDescription
--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, --helpPrint 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

ArgumentDescription
[WHAT]What to list instead of the store’s entities — one of roles.

Options

OptionDescription
--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, --helpPrint 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

ArgumentDescription
<SELECTOR><type>/<id>, organism/<id>, vessel/<id>, scenario/<model>/<name>, experiment/<model>/<name>, catalog/<kind>/<id>, vocabulary/roles, or role/<name> — required.

Options

OptionDescription
--used-byOnly what pins it, transitively, as an indented tree (an organism: its models, their scenarios, parameter sets and processes)
--jsonIts 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, --helpPrint 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

ArgumentDescription
<SELECTOR>Entity selector model_scaffold/<id> — required.

Options

OptionDescription
--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, --helpPrint 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

ArgumentDescription
<SELECTOR>Entity selector model_scaffold/<id> — required.

Options

OptionDescription
--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, --helpPrint 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

ArgumentDescription
[SELECTOR]process/<id> or plant/<id>

Options

OptionDescription
--path <DIR>A process directory, <repo>/processes/<id>, with its plants in <repo>/plants/
--writeWrite the resolved view and the process’s and plant’s views and locks
--treePrint 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)
--jsonPrint 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, --helpPrint 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

ArgumentDescription
<SELECTOR>Entity selector <type>/<id> (for example model_scaffold/chemostat) — required.

Options

OptionDescription
--view <VIEW>View name from the entity lockfile / adapter (for example ir or julia) — required.
--verify-hashVerify view bytes against the entity lockfile (documented consume path)
--no-verifySkip 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, --helpPrint 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

ArgumentDescription
<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

OptionDescription
--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, --helpPrint 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

ArgumentDescription
<CONTRACT_TYPE>Contract type: model_scaffold — required.
<ID>Entity id (for example my_model) — required.

Options

OptionDescription
--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, --helpPrint 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

ArgumentDescription
<SELECTOR>Entity selector <type>/<id> — required.

Options

OptionDescription
--set <SET>Override the proposed minimum SemVer (must be >= classified minimum)
--dry-runPrint the plan only; never mutate disk, commits, or tags
--create-tagCreate a local annotated tag <type>/<id>/vX.Y.Z. Off by default
--push-tagPush 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, --helpPrint 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

ArgumentDescription
<SELECTOR>Entity selector <type>/<id> — required.

Options

OptionDescription
--pushPush 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, --helpPrint 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

ArgumentDescription
<SELECTOR>Entity selector <type>/<id> — required.

Options

OptionDescription
--set <SET>Human-declared SemVer for this release (must be >= classified minimum) — required.
--create-tagCreate a local annotated tag <type>/<id>/vX.Y.Z. Off by default
--push-tagPush the created tag to origin (requires --create-tag). Off by default
--no-commitSkip the scoped git commit (still mutates authoring/lockfiles). Prefer --dry-run
--dry-runPrint 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, --helpPrint 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

SubcommandSummary
simulateSimulate a model over a model scenario (models/<id>/scenarios/<name>.toml)
synthesizeDraw a synthetic dataset from a model scenario’s prediction
identifiabilityThe practical identifiability of parameters over a model scenario, before any data
estimateEstimate a model’s parameters from an experiment (models/<id>/experiments/<name>.toml)
import-recordImport a taktwerk record as a run of a dataset of runs
replayRe-run a taktwerk record’s inputs on the host and compare every recorded output bit for bit

Options

OptionDescription
-h, --helpPrint 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

ArgumentDescription
<SCENARIO>The model scenario file, models/<id>/scenarios/<name>.toml — required.

Options

OptionDescription
--out <OUT>Write the prediction to this file instead of standard output
-h, --helpPrint 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

ArgumentDescription
<SCENARIO>The model scenario file, models/<id>/scenarios/<name>.toml — required.

Options

OptionDescription
--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
--writeWith --run: write the run; without, print what would be written
-h, --helpPrint 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

ArgumentDescription
<SCENARIO>The model scenario file, models/<id>/scenarios/<name>.toml — required.

Options

OptionDescription
--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, --helpPrint 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

ArgumentDescription
<EXPERIMENT>The experiment file, models/<id>/experiments/<name>.toml — required.

Options

OptionDescription
--writeWrite 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, --helpPrint 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

ArgumentDescription
<RECORD>The run directory, <dir>/<project>/<recorder>/<run-id>/ — required.

Options

OptionDescription
--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
--writeWrite the run; without, print what would be written
-h, --helpPrint 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

ArgumentDescription
<RECORD>The run directory, <dir>/<project>/<recorder>/<run-id>/ — required.

Options

OptionDescription
--project <PROJECT>The project file the run was started from — required.
--instance <INSTANCE>Replay this instance only
-h, --helpPrint help (see more with ‘–help’)

labinetix build

Build the raw model packages per target (crates/studio/build-xtask)

Usage: labinetix build <COMMAND>

Subcommands

SubcommandSummary
targetValidate the target files and print one target
raw-packageCross-build every raw model package’s library for one target into its package

Options

OptionDescription
-h, --helpPrint 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

ArgumentDescription
[TARGET_NAME]Target name from hardware/targets/, never a triple; omit to list every target

Options

OptionDescription
-h, --helpPrint 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

ArgumentDescription
<TARGET_NAME>Target name from hardware/targets/, never a triple — required.

Options

OptionDescription
-h, --helpPrint 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

ArgumentDescription
[REPO]The labinetix repository whose store to show — default ..

Options

OptionDescription
--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, --helpPrint help (see more with ‘–help’)