Skip to main content

kalix (Python)

Python interface for Kalix.

Current functionality:

  • run simulations from INI model files (in-process, no separate CLI binary)
  • run parameter optimisations from config files (in-process)
  • read and write Pixie files (.pxt, .pxb)
  • load, inspect, edit and run models in memory (a stateful Model class)

Planned:

  • richer optimisation control (in-memory configs)

Design

The package binds the Rust engine directly via PyO3 rather than driving the engine's stdio session protocol (which exists to serve KalixIDE). This keeps the Python surface free to grow in whatever direction suits Python workflows, independent of the GUI's needs.

The basic functions are deliberately stateless and mirror the two main CLI subcommands — simulate and optimise — so that moving from the command line to Python feels the same: point at files, get results back.

Above them sits the stateful Model class — load → inspect → mutate → run, all in memory. It loads from a file or a string, edits the model from INI snippets (validated before anything is accepted), accepts pre-declared inputs straight from DataFrames, runs, and returns results and mass balance as DataFrames. Nothing has to touch the disk between load and result, and nothing is hidden: what a Model holds is the same INI text you would write by hand, and to_string() gives it back.

Install

pip install kalix

Usage

Stateless functions

Run a simulation

import kalix

kalix.simulate("model.ini", output_file="results.pxb")
df = kalix.read_pixie("results.pxb")
print(df.head())

The Python equivalent of kalix sim model.ini -o results.pxb, but in-process — no separate CLI binary required. Output format is inferred from the extension (.pxb for the Pixie pair, .csv for CSV).

Both outputs are keyword-only, and at least one is required:

# Outputs + mass-balance report
kalix.simulate("model.ini", output_file="results.pxb", mass_balance="mb.txt")

# Mass-balance only
kalix.simulate("model.ini", mass_balance="mb.txt")

Run an optimisation

import kalix

result = kalix.optimise("calibration.ini", model_file="initial_model.ini", save_model="final_model.ini")

print(result["best_objective"])      # lower is better
print(result["parameters"])          # {"node.my_sac.uzfwm": 42.7, ...}
print(result["optimised_model_ini"]) # string copy of the final model ini

The Python equivalent of kalix optimise calibration.ini, but in-process. The config .ini defines the algorithm, calibration terms, objective expression, parameter bounds, and termination criteria. Unlike simulate, optimise returns a result dictionary:

key meaning
best_objective best objective value found (lower is better)
n_evaluations number of function evaluations performed
success whether the optimiser terminated successfully
message the optimiser's termination message
parameters optimised parameters as {target: physical_value}
optimised_model_ini the optimised model serialised back to an INI string

Two keyword-only options mirror the CLI's flags:

# Override the config's model_file (CLI: positional [model_file])
kalix.optimise("calibration.ini", model_file="other_model.ini")

# Also write the optimised model to disk (CLI: -s/--save-model)
kalix.optimise("calibration.ini", save_model="tuned.ini")

Paths inside the config (model_file, each term's observed_file) are resolved relative to the current working directory, exactly as the CLI does. If the config specifies an output_file, a results summary is written there too.

Read / write Pixie files

df = kalix.read_pixie("results.pxb")
print(df.head())

kalix.write_pixie("out.pxb", df)

The DataFrame index is a UTC DatetimeIndex; each column is one timeseries.

read_pixie accepts either extension (or no extension) and finds both files:

kalix.read_pixie("results.pxb")  # same as
kalix.read_pixie("results.pxt")  # same as
kalix.read_pixie("results")

write_pixie prefers a tz-aware DatetimeIndex, but will try to coerce other inputs (emitting a UserWarning when it does):

  • a naive DatetimeIndex is localised to UTC;
  • a default RangeIndex(0, n, 1) triggers promotion of the zeroth column to the index (and drops it from the body);
  • any other non-DatetimeIndex is passed through pd.to_datetime(..., utc=True).

Integer and float dtypes are never auto-interpreted as datetimes (this would silently misread values as epoch nanoseconds). Set a DatetimeIndex explicitly in those cases.

Model API

This section provides a practical overview. Please review docstrings for information regarding errors and method call signatures.

Creating and running a Model instance

import kalix
import pandas as pd

# A model is instantiated from load_* or Model.from_*
model: kalix.Model
model = kalix.load_file("model.ini")
model = kalix.load_string("...")           # same as
model = kalix.Model.from_file("model.ini") # same as
model = kalix.Model.from_string("...")     # same as

model.run()

# Outputs must be declared in the model's [outputs] section
df: pd.DataFrame
df = model.get_outputs()  # every declared output
df = model.get_outputs(["node.a.dsflow", "node.b.dsflow"])
df = model.get_outputs("node.a.dsflow")

mass_balance: pd.DataFrame = model.get_mass_balance()

# Most methods return self, so calls chain:
df = kalix.load_file("model.ini").run().get_outputs()

The DataFrame output from the model API follows the same conventions as the Pixie file reader — the index is a UTC DatetimeIndex, and each column is one timeseries, labelled as it was declared in the model's [outputs] section.

A model may be run more than once — each run() resets node and account state, so repeated runs are independent.

In-memory editing of Models

Models are edited in memory via patch(). It accepts a model snippet (a string or a dictionary, both shown below) and one of three modes. A patch that is malformed, or that would result in an invalid model, leaves the Model exactly as it was — the patch is applied to a copy and swapped in only on success.

The following patch adds a fictitious new node node.c, adds its dsflow to [outputs] and hooks it in to [node.b].

model = kalix.load_file("model.ini")

model.patch("""
[node.b]
ds_1 = node.c

[node.c]
type = gauge
loc = 0, 0

[outputs]
node.c.dsflow
""")

df = model.run().get_outputs()

Model.patch() also accepts dictionaries with structure {section: {property: value}}, where a blank value emits a bare property line.

model.patch({"node.b": {"ds_1": "node.c"},
             "node.c": {"type": "gauge",
                        "loc":  "0, 0"},
             "outputs": {"node.c.dsflow": ""}
})

The mode is keyword-only and defaults to merge:

mode effect
merge set the named properties, leaving everything else on the section untouched
replace replace each named section wholesale — properties the patch omits are dropped
delete remove each named section wholesale; property-level deletion is not supported

A section the model doesn't yet have is appended under merge and replace. Under delete, a section that isn't there is an error unless missing_ok=True is passed. Because delete needs only section names, it also accepts a plain list of them — the one form the other modes reject:

model.patch(["node.c", "node.d"], mode="delete")
model.patch("[node.c]\n[node.d]\n", mode="delete")       # the same, as INI text
model.patch(["node.c"], mode="delete", missing_ok=True)  # otherwise throws

Supplying inputs from memory

set_input() supplies data for a [data] alias from a DataFrame (or a bare Series, taken as a one-column frame) instead of from a file. The alias must already be declared — either bare, or pointing at a file, in which case the supplied data takes precedence. set_input() fills a declaration; it does not create one, so declare it first (via patch()) if it isn't there.

The following snippet indicates how this method should be used.

model = kalix.load_string("""
...
[data]
obs
...
""")

index = pd.date_range("2000-01-01", periods=5, freq="D")
model.set_input("obs", pd.DataFrame({"flow": [1.0, 2.0, 3.0, 4.0, 5.0]}, index=index))

df = model.run().get_outputs()

The frame must carry a DatetimeIndex with a regular step equal to the simulation timestep — the same requirement as write_pixie() — and a naive index is assumed to be UTC. Columns are addressable by name (data.obs.by_name.flow, with the standard sanitisation) or by 1-based position (data.obs.by_index.1), exactly as if a file had been loaded under the alias. Values are coerced to float64.

Like patch(), supplying new input data invalidates any prior run's results, so run() again before reading outputs.

Inspecting the model

The model's INI document is readable without serialising it back out:

model.sections()                 # ["kalix", "data", "node.a", ...], in file order
model.has_section("node.a")      # True
model.get_section("node.a")      # {"type": "gauge", "loc": "0, 0", ...}
model.get("node.a.loc")          # "0, 0"

get() takes a dotted "<section>.<property>" designation and splits on the last dot, so section names that themselves contain dots — as node sections do — resolve correctly. Values come back as strings, exactly as written in the INI; parse them yourself if you want numbers.

get_section() returns a snapshot, not a live view: mutating the returned dict does not touch the model, and patch() remains the only write path. A list-style section such as [data] or [outputs] comes back with each bare line as a key mapped to an empty string.

get_section() and get() are strict about lookups that miss — use has_section() to probe first, or catch KalixKeyError.

There is no write-through counterpart — no put(), and assigning into the returned dict changes nothing. Read-modify-write means handing the edited snapshot back to patch():

section = model.get_section("node.a")   # {"type": "gauge", "loc": "0, 0"}
section["loc"] = "10, 20"
model.patch({"node.a": section}, mode="replace")

The two halves are deliberately symmetric: what get_section() hands out is exactly what patch() takes back, down to the bare-line convention, so [outputs] and [data] round-trip the same way. replace above swaps the section for the snapshot wholesale; merge would apply only the keys the snapshot still carries.

Serialisation and copying

ini_text = model.to_string()   # round-tripped INI
model.save("edited_model.ini") # ...or straight to disk

variant = model.copy()         # deep, independent copy

to_string() preserves the original formatting of properties the model hasn't changed, so a load → edit → save round trip produces a readable diff rather than a wholesale rewrite. save() writes that same text to a file, and returns self so it can sit mid-chain.

copy() is a deep copy: the definition and any data supplied via set_input() come with it, and thereafter the two models are fully independent — patching one cannot leak into the other. Run results do not come with it; a fresh copy starts out unrun.

Building from source

Requires Rust (1.70+) and maturin.

# Editable install for development
cd python
maturin develop --release
# uv run maturin develop --release --uv

# Build a wheel
maturin build --release
# uv run maturin build --release -- uv

Tests

Running tests directly

maturin develop
pytest python/tests
# uv run maturin develop --uv
# uv run pytest

Running tests with tox

Tox is configured to test against multiple Python versions (3.9-3.14):

# Run tests in development environment (fast, uses current Python)
uv run tox -e dev

# Run tests against all configured Python versions
uv run tox

# Run tests for specific Python version
uv run tox -e py312

# List all available test environments
uv run tox list

# Run tests in parallel
uv run tox -p auto

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kalix-0.4.1.tar.gz (4.2 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

kalix-0.4.1-cp39-abi3-win_amd64.whl (862.5 kB view details)

Uploaded CPython 3.9+Windows x86-64

kalix-0.4.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (977.7 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

kalix-0.4.1-cp39-abi3-macosx_11_0_arm64.whl (863.6 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file kalix-0.4.1.tar.gz.

File metadata

  • Download URL: kalix-0.4.1.tar.gz
  • Upload date:
  • Size: 4.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for kalix-0.4.1.tar.gz
Algorithm Hash digest
SHA256 496775a6ed7fdd16c36abad77f2eb4258c505ca3678eebb4bd65656eaca250c5
MD5 91e8f42f127f71911616f321d680d277
BLAKE2b-256 3f6703a9e5e81695005d98d298773ea9b84c4d4a809a201640530e6bdfbbb4cf

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.1.tar.gz:

Publisher: release.yml on chasegan/Kalix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kalix-0.4.1-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: kalix-0.4.1-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 862.5 kB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for kalix-0.4.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 ceb5ca1bdd74d35f0ccc651c3eec8c5665fe362408b0625d214a0ccf3b45fda8
MD5 08ccb1d7d57d821c7ff3d3d3b9e79970
BLAKE2b-256 eb6da46555d721a53c5364c2d7cbaf512146aaceb7c1ed4b8e560bc3568a8940

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.1-cp39-abi3-win_amd64.whl:

Publisher: release.yml on chasegan/Kalix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kalix-0.4.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for kalix-0.4.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 05f51269f7c7813e79f328c6d2999c0bda94de06ed5a4a5907f58c75b81eb54c
MD5 1eb15d2f5d23c73f4d04ff41fa049082
BLAKE2b-256 3ad703503cce563a8690e359064d7d7cbef53028551e26381b5fb57015d42ec9

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on chasegan/Kalix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kalix-0.4.1-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: kalix-0.4.1-cp39-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 863.6 kB
  • Tags: CPython 3.9+, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for kalix-0.4.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a90b54716535e3db092a62b2fde1d5648694830bf568da6e4e0c2187ce5ac64d
MD5 86df92ed19b47e3b8743d0359acc9bba
BLAKE2b-256 bbd01b1b3e1293bb02844bb86bb8e1e710fd384606f87d25731dd602b9fe3224

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.1-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on chasegan/Kalix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.4

4 files

0.4.3

4 files

0.4.2

4 files

This release

0.4.1 This release

4 files

0.4.0

4 files

0.3.6

4 files

0.3.5

4 files

0.3.3

4 files

0.3.2

4 files

0.3.1

4 files

0.3.0

4 files

0.2.9

4 files

0.2.8

4 files

0.2.7

4 files

0.2.6

4 files

0.2.5

4 files

0.2.3

4 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page