latentplant
Learned world models for industrial process time series. An action-conditioned model of process dynamics that can be rolled forward in imagination, carries calibrated uncertainty, and supports what-if queries, planning and regime-change detection.
What a world model is here, and why it is not a forecaster
A forecaster maps history to a future. A world model maps history and a proposed action sequence to a distribution over futures:
rollout(obs_ctx, act_ctx, exo_ctx, act_fut, exo_fut, n_samples=64)
act_fut is an argument. That single difference is what makes counterfactual questions
expressible ("what if we raise the amine dose for the next four hours") and what makes planning
possible at all. A model that ignores its action input is a forecaster wearing a costume, so the
test suite asserts direction-of-effect on a system with known gain.
The disciplines baked in
Rollout-first evaluation. One-step teacher-forced error is not a world-model metric: on any
autocorrelated process series it flatters a model that has learned nothing. Everything in
latentplant.metrics is indexed by horizon, and break_even_horizon reports where the model
stops beating persistence. That integer belongs next to every headline number.
Two uncertainties, reported separately. uncertainty_split returns aleatoric (process and
sensor noise, irreducible) and epistemic (the model does not know this region) as different
arrays. A noisy tag and an unfamiliar operating point look identical in a single band and mean
completely different things; the epistemic band is what a regime-change alarm should watch.
Cadence is declared, not guessed. An hourly lab assay carried on 20-second rows repeats about
180 times. A model trained without masking those repeats learns to copy the previous row and
scores beautifully. PlantSchema takes update_seconds per tag and stale_mask marks the
repeats so the loss can ignore them.
Leakage-safe by construction. Episodes never cross an acquisition gap, windows never cross an episode, splits are chronological, and standardization is fitted on train only.
Install
pip install latentplant # core: numpy + torch
pip install "latentplant[onnx]" # ONNX export for a browser inference lane
pip install "latentplant[baselines]" # sysidentpy / pysindy baseline adapters
Quick start
import numpy as np
from latentplant import (PlantSchema, TagSpec, ProbabilisticEnsemble,
split_on_gaps, make_windows, chronological_split)
from latentplant.metrics import rollout_report
schema = PlantSchema(
tags=(
TagSpec("silica_conc", "target", "pct", 0.0, 10.0, update_seconds=3600.0),
TagSpec("pulp_level", "observation", "pct", 0.0, 100.0),
TagSpec("amina_flow", "action", "m3/h", 0.0, 800.0),
TagSpec("iron_feed", "exogenous", "pct"),
),
row_seconds=20.0,
)
episodes = split_on_gaps(schema, values, timestamps) # a plant stop ends an episode
windows = make_windows(schema, episodes, context=24, horizon=12)
train, val, test = chronological_split(windows)
wm = ProbabilisticEnsemble(n_obs=2, n_act=1, n_exo=1, n_members=5)
wm.fit(train, epochs=60, seed=0)
roll = wm.rollout(test.obs_ctx, test.act_ctx, test.exo_ctx,
test.act_fut, test.exo_fut, n_samples=64, seed=0)
lo, hi = roll.interval(0.9)
print(rollout_report(roll.samples, test.obs_fut, test.obs_ctx, test.mask_fut))
Counterfactual A/B from the same anchor:
more, less = wm.counterfactual(ctx_obs, ctx_act, ctx_exo, action_a, action_b, exo_fut)
lift = more.mean - less.mean
On real plant data this supports direction-of-effect claims only: logged actions come from a closed loop, so observational data does not identify interventions without assumptions. On a simulator the ground truth exists and the imagination-to-reality gap is measured instead.
Plan against the model, and measure what the plan was worth:
from latentplant import Planner, cvar_cost, target_tracking_cost, imagination_gap
planner = Planner(model=wm, bounds=action_bounds, horizon=24, method="cem")
plan = planner.plan(obs_ctx[:1], act_ctx[:1], exo_ctx[:1], exo_fut[:1],
cvar_cost(target_tracking_cost(target), alpha=0.2),
action_rate_penalty=2.0)
realized = env.execute(plan.actions) # only possible where truth exists
print(imagination_gap(plan, realized, persistence_from=obs_ctx[0]))
Fix an overconfident model's intervals without retraining it:
from latentplant import fit_conformal, calibration_report
conf = fit_conformal(cal_roll, cal.obs_fut, level=0.9, mask=cal.mask_fut)
print(calibration_report(conf, test_roll, test.obs_fut, mask=test.mask_fut))
Documentation
Full docs in docs/: concepts (what a world model is, the plant contract,
windows and leakage), models (probabilistic ensemble, RSSM, ensembled RSSM), evaluation (rollout
metrics, conformal calibration) and planning (imagination, the imagination-to-reality gap).
Status
0.03.000, alpha. Shipping now: the ingestion contract with the multi-rate observation operator, episodes/windows/leakage-safe splits, the probabilistic ensemble (PETS-class, TS-inf propagation), the vector RSSM, the ensembled RSSM with the aleatoric/epistemic split, the rollout metric suite, split-conformal interval calibration, and CEM/MPPI planning with the imagination-to-reality gap. On the roadmap: ONNX export for a browser inference lane, a Gymnasium imagination env, and baseline adapters.
Cite
Santibañez-Leal, F. (2026). Action-Conditioned Latent Dynamics for Mineral Processing: A World-Model Engine, and a Measured Account of Where It Earns Its Cost. Preprint v0.01, CC BY 4.0. 10.5281/zenodo.22135521 (concept DOI, always latest: 10.5281/zenodo.22135520).
The paper reports what this engine actually achieves on mineral-processing data, in three
directions: a recurrent latent tracks a PDE concentration field it never observes and is
overconfident doing it, a linear model wins on a near-linear grinding circuit, and on a real
flotation historian the whole class fails for a reason that is a property of the record rather
than of the method. Source in manuscripts/.
Not to be confused with
phenoforge, the sibling engine, which fits and
ensembles closed-form phenomenological equations. latentplant learns latent dynamics from
data. Same industry, different object.
References
The design follows: Chua et al. 2018, Deep RL in a Handful of Trials with Probabilistic Dynamics Models, arXiv:1805.12114 (probabilistic ensembles, TS-inf); Lakshminarayanan et al. 2017, Simple and Scalable Predictive Uncertainty Estimation using Deep Ensembles, arXiv:1612.01474; Hafner et al. 2019, Learning Latent Dynamics for Planning from Pixels, arXiv:1811.04551 (RSSM); Hafner et al. 2023, Mastering Diverse Domains through World Models, arXiv:2301.04104; Janner et al. 2019, When to Trust Your Model, arXiv:1906.08253 (rollout horizon and compounding error); Che et al. 2016, Recurrent Neural Networks for Multivariate Time Series with Missing Values, arXiv:1606.01865 (cadence and masking); Romano et al. 2019, Conformalized Quantile Regression, arXiv:1905.03222 (interval calibration); Williams et al. 2017, Information Theoretic Model Predictive Control, arXiv:1707.02342 (MPPI); Levine et al. 2020, Offline Reinforcement Learning: Tutorial, Review, and Perspectives, arXiv:2005.01643 (why the planner refuses to issue setpoints).
Developed by Felipe Santibanez-Leal. MIT licensed.
Release files for latentplant 0.11.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| latentplant-0.11.1.tar.gz | 158.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| latentplant-0.11.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 276.7 kB
Release files / latentplant-0.11.1.tar.gz
| Download URL | latentplant-0.11.1.tar.gz |
|---|---|
| Size | 158.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4a21048c6c7abdf0f796ffc14c862fa750f2d3a15ff1e661439685e777947076
|
|
BLAKE2b-256 checksum How to use checksums |
b78d69fdcc7413ab8f1ca86dc11903350125f6f9fc856573ee87bf4c14135c8b
|
| 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 Sep 26, 2026.
Transparency logRelease files / latentplant-0.11.1-py3-none-any.whl
| Download URL | latentplant-0.11.1-py3-none-any.whl |
|---|---|
| Size | 118.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1b679a078ea21f72ba9f6d78ecd715eeb1a9d6e2addb99ebccd19b7ed3f91e47
|
|
BLAKE2b-256 checksum How to use checksums |
9cfec1ea84ede38d373b91c2ee583dbc9faedb619513f5040a70d55bc83da934
|
| 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 Sep 26, 2026.
Transparency log