Skip to main content

Continuous active inference for Python — the continuous-state sibling of pymdp.

Project description

cpomdp

Continuous active inference for Python. The continuous-state sibling of pymdp.

pymdp is great, but it speaks in discrete states. A lot of the world isn't discrete. Positions, velocities, temperatures, the kinds of things you'd actually want an agent to track and steer, don't come in neat little categories. cpomdp fills that gap. You hand it a linear-Gaussian model of how the world moves and what you can see of it, and you get back an agent that perceives and acts in the same infer_states / sample_action loop pymdp users already know.

That's the whole idea: keep the pymdp muscle memory, swap the discrete machinery underneath for continuous.

Example

A bacillus seeking food — the continuous-state answer to pymdp's mouse-seeking-cheese. The body sits at the true hidden state; the orange + is where the agent believes it is (belief.mean); the blue ellipse is its uncertainty (belief.cov), wide at first and shrinking as the Kalman filter locks on; the star is the food (the goal the LQR controller steers toward). It perceives, acts, and arrives.

A bacillus navigating to food via continuous active inference

Reproduce it with examples/bacillus_seeking_food.py (pip install matplotlib pillow).

Install

pip install cpomdp

Or the latest from source:

pip install git+https://github.com/DanBoringName/cpomdp

That's all you need for normal use. There's also an optional RxInfer (Julia) backend that the test suite leans on as a correctness oracle. You almost certainly don't need it, but if you want it:

pip install "cpomdp[rxinfer]"

It pulls in a Julia bridge and bootstraps itself the first time you use it.

Quickstart

Here's an agent steering a point mass to a target. It can push the mass and it can see where the mass is, but it never sees the velocity. The filter has to work that out from how the position moves.

import jax.numpy as jnp
from cpomdp import Agent, Belief, LinearGaussianModel

# State is [position, velocity]. A push changes velocity, velocity carries
# position along, and we only ever observe position (through a noisy sensor).
dt = 0.1
model = LinearGaussianModel(
    dynamics=[[1, dt], [0, 1]],          # velocity carries position along
    control=[[0], [dt]],                 # a push nudges velocity
    sensor_model=[[1, 0]],               # we observe position only
    dynamics_noise=jnp.eye(2) * 1e-6,
    sensor_noise=[[1e-2]],
    prior=Belief(mean=[0, 0], cov=jnp.eye(2)),
)

# Tell it where to go: sit still at position 1.
agent = Agent(model, goal=[1.0, 0.0])

true_state = jnp.array([0.0, 0.0])
for _ in range(100):
    obs = model.sensor_model @ true_state            # what the agent gets to see
    agent.infer_states(obs)                           # perceive
    action = agent.sample_action()                    # act
    true_state = model.dynamics @ true_state + model.control @ action

print(jnp.round(agent.belief.mean, 3))   # ≈ [1, 0]

Run that and the belief lands on [1, 0]. The agent worked out it was at position 1 and sitting still, which is exactly where we asked it to go, and it did it without ever seeing the velocity it had to control.

The pymdp parallel

If you've used pymdp, this table is basically the whole API:

pymdp (discrete) cpomdp (continuous) what it is
Agent Agent the stateful thing you drive
qs belief the posterior over the state
infer_states infer_states fold in an observation
sample_action sample_action pick an action
C goal + goal_precision the state you prefer, how sharply
D model.prior belief before you've seen anything

One honest difference. sample_action here is deterministic, not a sample from a policy posterior. For a linear-Gaussian sensor the action that minimises expected free energy turns out to be exactly the LQR optimum, so there's a single best action and that's what comes back. Same loop, exact answer. The reasoning is in DECISIONS.md (ADR-003) if you want it.

Just want to track, not act?

Leave the goal out and you get a pure tracker. infer_states still folds in observations and sharpens the belief, but sample_action will stop you, because there's nothing to steer toward.

agent = Agent(model)                  # no goal
belief = agent.infer_states([0.5])    # perceiving is fine
agent.sample_action()                 # ValueError: this Agent has no goal

What's in the box

Right now (v0.1) cpomdp handles linear-Gaussian models: Kalman filtering for perception, steady-state LQR for action. That already covers a fair bit of ground, roughly anything you'd reach for a Kalman filter to do, but it's the foundation rather than the finished house. Nonlinear models and proper epistemic (information-seeking) action are the obvious next steps.

You can swap the inference engine if you want to. KalmanBackend is the default and does the real work; RxInferBackend re-derives the same answers through Julia and exists mainly so the fast path has something independent to check itself against. Both sit behind the InferenceBackend protocol, so you can write your own.

This is pre-alpha. The API works and the maths is tested against that independent oracle, but expect things to move before 1.0.

Project details


Download files

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

Source Distribution

cpomdp-0.2.0.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

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

cpomdp-0.2.0-py3-none-any.whl (24.1 kB view details)

Uploaded Python 3

File details

Details for the file cpomdp-0.2.0.tar.gz.

File metadata

  • Download URL: cpomdp-0.2.0.tar.gz
  • Upload date:
  • Size: 1.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cpomdp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f5045cfd4bc09ff29ce7f678e2dd0f85eb217cfca50b9273972f61129d7e440a
MD5 1019a24e26d8c47939715041edf7a258
BLAKE2b-256 c2b6d5d4f81c8157474f3c87f7b93ab8749720d92f047ae83ea1e5ed71e1f2ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for cpomdp-0.2.0.tar.gz:

Publisher: publish.yml on DanBoringName/cpomdp

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

File details

Details for the file cpomdp-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: cpomdp-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 24.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cpomdp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2f14fb3a3d541a7be4387cc3e09dfc7172f698929eae483209822dd8b0d90686
MD5 55024d772143110b3e8b273176bd3063
BLAKE2b-256 843abbc0d1cc26024e848c1fcb08dbd6bc0052bc0df74236ad0dd841b40bafc0

See more details on using hashes here.

Provenance

The following attestation bundles were made for cpomdp-0.2.0-py3-none-any.whl:

Publisher: publish.yml on DanBoringName/cpomdp

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page