One interface for every world model
Each world model family in Simulusion predicts the future its own way: Dreamer
and PlaNet roll a recurrent latent forward, IRIS and Genie generate tokens with
a Transformer, DIAMOND samples pixels with a diffusion model, and World Models
pairs a VAE with an MDN-RNN. The WorldModel protocol puts one interface over
all of them, so code that evaluates, plans with, or plays a world model works
with any family.
import torch
from simulusion.world_model import as_world_model, load_world_model
model = as_world_model(agent) # a model you trained in this process
# or: model = load_world_model("runs/dreamer/ckpts/61000_ckpt.pt", "dreamer")
state = model.encode(frames, actions) # real context -> model state
out = model.step(state, action) # one imagined step
out.reward, out.terminated # from the model's own heads
image = model.decode(out.state) # (B, C, H, W) in [0, 1]
torch.manual_seed(0)
traj = model.rollout(state, planned_actions) # many steps
traj.frames, traj.rewards, traj.terminated # (B, H, ...)The methods
| Method | Does |
|---|---|
encode(frames, actions=None) |
Turns real context frames, and the actions taken between them, into a model state. |
step(state, action) |
Imagines one step. Returns a Transition: the next state, plus reward and terminated where the model has those heads. |
decode(state) |
The current frame of a state, float (B, C, H, W) in [0, 1]. |
rollout(state, actions, decode=True) |
H steps at once; returns a Rollout of frames, rewards and terminations, each (B, H, ...). |
save(path) |
Writes the model to a directory that load_world_model reads back. |
spec |
A WorldModelSpec: what the model reads and predicts (below). |
Conventions
The same for every family, so you never need to know a model's internals:
- Frames are RGB, channel-first and batch-first:
(B, T, C, H, W), either float in[0, 1]oruint8in[0, 255]. A 4-D input is one sequence and a 3-D input one frame. Channel-last input and other sizes are converted to the model's own (spec.obs_shape). - Actions between context frames:
actions[:, t]is the action taken at framet, which led to framet + 1.Tframes come withT - 1actions. Discrete actions are integers(B, ...), continuous ones float(B, ..., A). A single action passed tostepis used for the whole batch. - State is a
dict[str, Tensor]with the batch on dim 0. It is plain data: keep a copy to branch a dream from the same point. - Randomness comes from PyTorch's global RNG.
torch.manual_seedbeforeencodeandrolloutmakes a dream repeatable. - Inference only: methods run without autograd and put the model in eval mode. Training stays with each family's trainer.
What each family provides
model.spec says what a model can do, so generic code can check instead of
special-casing families:
| Family | Actions | Context | Reward | Termination | Notes |
|---|---|---|---|---|---|
dreamer (V1, V2) |
continuous in [-1, 1] |
1+ frames, recurrent | yes | if trained with the discount head | |
planet |
continuous, unbounded | 1+ frames, recurrent | yes | no | frames are quantised to the trained bit depth |
iris |
discrete | up to transformer_timesteps frames |
yes, on the trained (sign) scale | yes | sample=False makes steps deterministic |
diamond |
discrete | exactly num_conditioning_frames |
yes, {-1, 0, 1} |
yes | sampling_steps= trades quality for speed |
genie |
discrete, latent (spec.action.latent) |
up to num_frames |
no | no | without actions, Genie labels the context itself |
world-models |
continuous, unbounded | 1+ frames, recurrent | yes | yes | temperature= as in the paper |
JEPA and DiT have no adapter: I-JEPA learns image representations without
actions and DiT generates images from a class label, so neither predicts what
an action does. as_world_model says so if you pass one.
Loading checkpoints
load_world_model(path) reads a directory written by save, which records its
family in world_model.json. It also reads each family's own training
checkpoints when you name the family:
load_world_model("runs/dreamer/ckpts/61000_ckpt.pt", "dreamer") # V1 or V2
load_world_model("runs/planet/rssm.pth", "planet") # bare RSSM weights
load_world_model("runs/iris/iris.pt", "iris")
load_world_model("runs/diamond/checkpoint_10.pt", "diamond") # no emulator needed
load_world_model("runs/genie/genie_sonic_final.pt", "genie")World Models trains its two halves separately, so it loads from both files:
from simulusion.world_model import MDRNNWorldModel
model = MDRNNWorldModel.from_checkpoints("vae/best.tar", "mdrnn/best.tar")Constructor options such as a sampling temperature are saved with the model and
can be overridden on load: load_world_model(path, temperature=0.5).
Models and agents
The protocol covers the model: what happens to the world under an action. The policy that chooses actions stays with each family's agent, and steppers remain the interface for acting in a real environment. A DIAMOND world model, for example, loads without the Atari emulator or the replay buffer its agent needs for training.
Your own model
Implement the methods above (and spec and device) and any code written
against WorldModel accepts it; isinstance(obj, WorldModel) checks the
shape at runtime. Subclassing simulusion.world_model.WorldModelBase gives
you input conversion, rollout and the save format for free: implement
_encode, _step, _decode, _save_weights and _load_weights.
Updated Oct 18, 2026