dualis for Python
Coupled physics whose conservation audit is an exception you can catch.
pip install dualis # once it is on PyPI; until then, see Building below
import dualis
sim = dualis.Simulation(schedule="multirate", conservation_tolerance=1e-9)
sim.add_heater("element", watts=2.0, reserve_j=6.0)
sim.add_bar("bar", length_m=0.020, cells=41, area_m2=1e-4)
for _ in range(8):
sim.advance(0.5) # raises dualis.Violation if the books do not close
print(sim.temperature("bar")) # 294.39 K
print(sim.profile("bar")[:3]) # warmer at the fed end
Several bodies and the drop between them
add_lump gives you one temperature for a whole assembly. The number that decides whether a
motor survives is the winding, which is hotter than the case by however much the joint
between them resists — so for that, a network:
sim = dualis.Simulation(schedule="staggered")
sim.add_heater("losses", watts=6.0, reserve_j=5_400.0)
sim.add_network("motor",
nodes=[{"name": "winding", "material": "copper", "volume_m3": 18e-6,
"thickness_m": 2e-3, "initial_k": 298.15},
{"name": "case", "material": "aluminium", "volume_m3": 220e-6,
"thickness_m": 4e-3, "initial_k": 298.15,
"ambient_k": 298.15, "area_m2": 0.042}], # both keys, or neither
links=[{"from": "winding", "to": "case", "w_per_k": 0.9}],
absorbing="winding")
for _ in range(900):
sim.advance(1.0)
dict(sim.node_temperatures("motor")) # {'winding': 311.2, 'case': 304.9}
sim.heat_flow_w("motor", "winding", "case") # 5.585 W crossing the joint
Materials: copper, aluminium, electrical_steel, fr4, pla. A node with ambient_k and
area_m2 loses heat to still air; a node with neither is interior. Giving one without the other
is refused rather than quietly producing a node that looks like it is cooling and is not.
Names are resolved once, when the network is built, so a link to a node that does not exist
raises before any stepping happens and lists the nodes that do. That matters more here than it
looks: a link adds +q to one node and −q to another in the same sum, so a missing or
misdirected link passes the conservation audit at machine precision and simply reports the wrong
temperature forever.
temperature() refuses a network rather than averaging it — the reason to build one is that its
nodes differ, so a mean would describe no part of it.
Why this rather than numpy
Write physics in numpy and a wrong model runs happily, producing plausible output. There is no signal; you find out when somebody notices the answer is silly.
Every advance here either moves the clock or raises, and the exception is addressable rather
than a sentence to parse:
try:
sim.advance(0.5)
except dualis.Violation as v:
v.quantity # 'energy'
v.site # 'bus (published but not consumed)'
v.before, v.after, v.scale, v.tolerance
sim.time # unchanged — a refused step does not advance the clock
Be clear about what it catches. Quantities appearing or vanishing, amounts left unclaimed on
the bus, and a domain refusing a step it cannot take. It does not catch a model that is
internally consistent and physically wrong. For that, check against something the library did not
compute — which is what aluminium_heat_capacity_j_per_k is exported for, and what
tests/test_dualis.py does throughout.
What crosses the boundary, and what does not
Values cross as SI floats with the unit in the parameter name: length_m, watts,
initial_k. dualis's dimensional types are a compile-time thing — Length + Time does not
compile in Rust — and Python cannot have that. A runtime Quantity wrapper would cost an
allocation and a check per operation to catch, at run time, a class of error a Python caller
mostly does not make, because it passes values in and reads them out rather than doing algebra on
them. So the types stay on the Rust side and the units stay in the names.
What crosses instead is the audit, which is the part worth having.
What this cannot do
You cannot write a domain in Python. That means calling back into the interpreter from inside the step loop, holding the GIL across it, and deciding what an exception raised mid-sweep does to a half-advanced simulation. All three are answerable and none cheaply, so this binds the physics the library has and says so rather than shipping a subtly wrong version of that.
So: enough to run and audit coupled physics from Python, not enough to extend it. The extending lives in Rust, and the workspace is where.
A Simulation is also bound to the thread that created it. Domain is not Send, and the
binding says so rather than asking the kernel for a bound it does not offer.
Building
Its own cargo workspace, deliberately: pyo3 brings about fifteen crates and links libpython, and the library's lockfile, licence allow-list and WebAssembly jobs are promises that should not have to accommodate a Python extension.
cd bindings/python
pip install "maturin>=1.7,<2.0"
maturin build --release # an abi3 wheel: one build serves 3.10 upward
pip install target/wheels/dualis-*.whl
python tests/test_dualis.py
maturin develop works too, in a virtualenv. CI builds the wheel, installs it and runs those
tests on every commit — because "it compiles" proves neither half of "pip install then import".
Metadata
Release files for dualis 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dualis-0.3.0.tar.gz | 530.6 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| dualis-0.3.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| dualis-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| dualis-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| dualis-0.3.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| dualis-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 2.1 MB
Release files / dualis-0.3.0.tar.gz
| Download URL | dualis-0.3.0.tar.gz |
|---|---|
| Size | 530.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9cb5c3857cb2e454654ec94eac583072f1826c58df704035c54798c25608d16a
|
|
BLAKE2b-256 checksum How to use checksums |
f2a3d0e77be67f8d78a3e7e7ba489c291eb4533c87dff42eff59a7d7b5f2e85d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / dualis-0.3.0-cp310-abi3-win_amd64.whl
| Download URL | dualis-0.3.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 220.4 kB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
cae1d0e5bf62cdf6150db7171e98722ceecdd700b9a182dab34a92340a90788c
|
|
BLAKE2b-256 checksum How to use checksums |
8255c47277139778b2f2f9399627391baa32845be6eb8e17730598e499d603cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / dualis-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | dualis-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 368.6 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
7b974d10c3f0f05e44cb0a11a4007e1c80d617f810e0dcaa9b80aaa143931fdb
|
|
BLAKE2b-256 checksum How to use checksums |
0156205ccfc6bbf36ab35f1ef6c6614127c7ea63893cf288ee14d4c99110e9ac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / dualis-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | dualis-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 359.1 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
9d00294122ad31d4e539955aa382f46733e651a573871df61f8f9c66fc683085
|
|
BLAKE2b-256 checksum How to use checksums |
a431525b13fe6303319cdce07074d8d507079b87d16527360b3713cf31508290
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / dualis-0.3.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | dualis-0.3.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 326.9 kB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2d88d506508de4f89423ec12ce7b549e73d0c1ad3181c91f2d99fc8bc1b34fba
|
|
BLAKE2b-256 checksum How to use checksums |
b66000b196a5c9c36c0a1261f3ea0770d057d64d780fe9418c44de1ec36b315f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / dualis-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | dualis-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 328.4 kB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
39aeb85ffb9dc5cc347bd667d4c65d5c8d0daf84dfecca80e5850181ed7af471
|
|
BLAKE2b-256 checksum How to use checksums |
2c4e9aed05507e17b47de984a545dfc063e5f6cd58c0b08496d23a04f477cc2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency log