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.2.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.2-cp39-abi3-win_amd64.whl (891.1 kB view details)

Uploaded CPython 3.9+Windows x86-64

kalix-0.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.0 MB view details)

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

kalix-0.4.2-cp39-abi3-macosx_11_0_arm64.whl (882.3 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

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

File metadata

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

File hashes

Hashes for kalix-0.4.2.tar.gz
Algorithm Hash digest
SHA256 cbc3de6570f9c11c6a4688fb8ee59643eea0402146d35c5a87f4fe8be852d75a
MD5 f200a9c1e1bc7ac5f36dee39b53c4dd9
BLAKE2b-256 3a9da8f74d50f647ed4a88419485357592c7e7e86b426415cae808d78dcb0e61

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.2.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.2-cp39-abi3-win_amd64.whl.

File metadata

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

File hashes

Hashes for kalix-0.4.2-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 403d83656d22c97daaa424d98e00db18a7adc8194bc332939d156a7b717269a2
MD5 92c8a96eb986b7888b7e6ac57392dffa
BLAKE2b-256 26bcc877932ed7e454f5c4e50f0d26e0ec1d0067948464cb07618093067feb86

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.2-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.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for kalix-0.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a6a84698b3c427898426abef3a0fad6e283dc158910c2d44073788252dfbe083
MD5 9115b55608236084e3ca587f76df76f2
BLAKE2b-256 585e9e35805e83a4e2328fd023a6d158af6317d4357a14e2e3823f82e2a7d435

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.2-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.2-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

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

File hashes

Hashes for kalix-0.4.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f23f2b297962ce2b30f5ce295b39831183623e24b77cd9ac21454eed9bb01b60
MD5 5d0cf5d57da48d1d7c54961118c9ee47
BLAKE2b-256 ccca1fdf89d039e7f176503bdd1de5fb8a4ac8800ed8423394b8733db46bac57

See more details on using hashes here.

Provenance

The following attestation bundles were made for kalix-0.4.2-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

This release

0.4.2 This release

4 files

0.4.1

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