Reference

0.80.80.80.80.20.20.20.201234rightleft
The quick-start model: the action right in green and the action left in grey, with transition probabilities. State 4 is terminal.

Approximate Numerics in Applied Transition Environments

This document lists every public call in aniate, with its arguments and return values. For guidance on which call suits a task, see tree.md.

aniate works with finite models whose states can be listed in memory. It solves them exactly, simulates them, and tests external simulators against them.

pip install aniate            # numpy and scipy
pip install 'aniate[vis]'     # adds matplotlib, required for plots
pip install 'aniate[gym]'     # adds gymnasium, required for aniate.gym

Verifying the installation: ant

ant(stream=None)

Prints the Fibonacci drawing, the installed versions of Python, numpy, scipy, matplotlib, pytest, hypothesis and gymnasium, and the result of a self-test. The self-test builds, solves, simulates and checks a five-state model, and writes a PDF plot to a temporary directory when matplotlib is installed. Returns True when every required component works. stream defaults to standard output. python -m aniate and the aniate command run the same check and exit with status 0 or 1.

from aniate import ant
ant()

Complete workflow

import aniate as an
from aniate import nmdp, vis

# 1. define
m = an.from_functions(
    states=range(5), actions=["left", "right"],
    transition=lambda s, a: {max(s - 1, 0): 1.0} if a == "left" else {min(s + 1, 4): 0.8, s: 0.2},
    reward=lambda s, a, s_next: 10.0 if s_next == 4 else -1.0,
    gamma=0.9, terminal=[4], initial=0,
)
print(m.summary())
m.describe()

# 2. run
env = m.env(seed=0)
obs, info = env.reset()
obs, reward, terminated, truncated, info = env.step("right")
runs = an.simulate(m, policy=None, episodes=10_000)     # uniform random policy

# 3. check, against the model's own environment or an external simulator passed as env=
an.check(m)

# 4. solve
sol = m.solve()

# 5. plot
vis.save(vis.overview(an.simulate(m, sol, episodes=10_000)), "overview.pdf")

1. Defining a model

MDP(P, R, gamma, *, states=None, actions=None, initial=None, terminal=None, horizon=None, validate=True)

argumentmeaning
Pa list of A matrices of shape S x S (dense or sparse), or an array of shape (A, S, S); P[a][s, s2] is the probability of s2 given s and a
Ran array of shape (S, A) or (S, A, S), or a list of A matrices shaped like P; rewards are attached to transitions
gammadiscount factor in [0, 1]
states, actionslabels, which may be any hashable values such as "ok", 3 or (2, 1); the defaults are s0, s1, … and a0, a1, …
initial{state: weight} or an array; the default is uniform
terminalstates that end an episode; they must be absorbing with zero reward; inferred if omitted
horizona finite number of steps, or None
validateTrue raises on an invalid model, "warn" builds it and logs the problems, False skips validation

Builder(gamma=0.95, horizon=None)

b = an.Builder(gamma=0.9)
b.transition("ok", "run", "ok", prob=0.9, reward=10)      # one call per outcome
b.transition("ok", "run", "broken", prob=0.1, reward=10)
b.transition("ok", "sell", "done", reward=50)
b.transition("broken", "run", "broken", reward=0)
b.transition("broken", "sell", "done", reward=5)
b.terminal("done")
b.initial("ok")                       # or {"ok": 0.7, "broken": 0.3}
m = b.build(missing="error")          # missing="stay" makes undefined actions leave the state unchanged

from_functions(states, actions, transition, reward=None, gamma=0.95, *, initial, terminal, horizon)

transition(s, a) returns a dict {s2: probability}. reward(s, a, s2) or reward(s, a) returns a number. terminal may be a list of states or a predicate.

Model attributes and methods

attribute or methoddescription
m.S, m.A, m.states, m.actions, m.gamma, m.horizon, m.initial, m.terminalthe definition
m.P, m.Rsparse transition matrices and the expected reward of shape (S, A)
m.transitions(s, a){s2: probability}
m.reward(s, a, s2=None)the transition reward, or the expected reward when s2 is omitted
m.summary()the complete model as text
m.describe()the model as a JSON-compatible dict
m.reportvalidation results; m.report.describe() returns them as a dict
m.env(), m.solve(), m.evaluate(policy)described in the sections below

Every public call accepts labels. An integer is interpreted as a position only when no label is an integer.

Validation

The following conditions are errors. The model is not built, and the message names every affected state and action.

codecondition
row_suma row of transition probabilities does not sum to 1
dead_enda state-action pair has no successor
negative_probability, non_finitea probability is negative, or a value is not finite
terminal_not_absorbinga terminal state moves to another state or pays a reward
unbounded_valuegamma = 1 and no terminal state is reachable

The following conditions produce logged warnings: unreachable_state, rewarding_absorbing_state, reward_on_impossible_transition.

These checks, together with the test suite, verify the stated properties at runtime. They are not formal proofs.


2. History-dependent problems

In these problems, the reward or a constraint depends on the history of the episode, for example "visit A before B" or "refuel at most three times". The user declares events and an automaton. aniate builds the exact product model, and its memory states keep the names given by the automaton.

world  = an.problems.gridworld(rows=5, cols=5, goals=((0, 4),), start=(4, 0))
labels = nmdp.Labels(world).at("r4c4", "A").at("r0c4", "B")
task   = an.NMDP(world, labels, nmdp.Ordering(["A", "B"], violation_penalty=-1))
pi     = task.solve()

Labels(world)

callthe event occurs when the agent
.at(state, *events)arrives in state
.on(state, action, *events)takes action in state
.action(action, *events)takes action in any state
.where(predicate, *events)arrives in any state whose label satisfies predicate

The events of a step s -a-> s2 are the union of on(s, a) and at(s2). No events are read at time 0 or after the episode has ended.

Automata

calldescription
nmdp.Ordering(["A", "B"], violation_penalty=-1)memory states before_A, A_visited_B_pending, violated
nmdp.Budget("refuel", max=3, violation_penalty=-1)memory states refuels_used=0 to 3
nmdp.Deadline(steps=50, expiry_penalty=-1, stop_on=None)memory states step=0 to 50
nmdp.Mealy(name, states, initial, events, transition, output, accepting, meaning)a general automaton, with transition(q, events) -> q2 and output(q, events) -> reward
nmdp.compose([m1, m2])runs several automata in lockstep; passing a list to NMDP has the same effect

Every automaton provides .summary(), .describe(), .step(q, events) and .run(trace).

NMDP(world, labels, spec, *, combine="add", violation_terminal=False)

  • combine="add" adds the world reward and the automaton output. combine="replace" uses the automaton output alone.
  • violation_terminal=True ends the episode when a constraint can no longer be satisfied.
attribute or methoddescription
task.states, task.actions, task.memory, task.machine, task.productthe definition; product is an ordinary MDP
task.env(hide_memory=True)an environment in which the agent observes world states and info["memory"] reports the memory state
task.solve()returns a MemoryPolicy
task.evaluate(policy), task.simulate(policy, episodes), task.check(env, policy)as for an MDP
task.summary(), task.describe()text and dict summaries

MemoryPolicy

attribute or methoddescription
pi.reset(), pi.step(state)call step with each world state; it returns an action label and updates the memory state
pi.memory, pi.explain()the current memory state and its meaning
pi.action(state, memory)a lookup that does not update the memory state
pi.solution, pi.start_value, pi.describe()values, error bound, summary
task.product.policy_disagreements(pi.solution)world states in which the chosen action depends on the memory state

3. Running

Env(model, *, seed=None, max_steps=None, labels=True, hide_memory=False, log=True)

The environment follows the Gym interface and samples from the model.

env = an.problems.gridworld().env(seed=0)
obs, info = env.reset()                                        # or reset(seed=..., state=...)
obs, reward, terminated, truncated, info = env.step("north")   # an action label or position
env.render()        # 't=1  r3c0 --north--> r2c0  reward -0.04  return -0.04'

info contains episode, t, state, action, return (discounted, accumulated so far) and discount (gamma to the power t). History-dependent models add world_state, memory and events. labels=False returns integer observations.

simulate(model, policy=None, episodes=100, *, max_steps=None, seed=0, start=None)

Runs all episodes in parallel and returns an Episodes object.

attribute or methoddescription
returns, lengths, terminated, truncatedone entry per episode
states, actions, rewardspadded arrays of shape (episodes, steps), filled with -1 or 0 after an episode ends
mean_return, stderr, model_valuethe sample mean, its standard error, and the exact value of the policy
partial_returns()the return accumulated before each step
martingale()G_<t + gamma^t V(s_t); its mean equals model_value at every step when the model and the simulation agree
occupancy()the fraction of time spent in each state
truncation_biasan upper bound on the shift in the mean caused by truncated episodes
describe()counts, mean, standard error, 5th, 50th and 95th percentiles of the return, and the model value
runs[i]a single Episode with .states, .actions, .rewards, .memory and .table()

Accepted policy forms

A policy may be None (uniform random), a Solution, an array of action indices of shape (S,) or (T, S), a probability array of shape (S, A), a dict {state: action}, a function f(state) -> action, a MemoryPolicy, or any object with a step(state) method. The last form can be simulated but not evaluated exactly.


4. Checking a simulator

check(model, env=None, policy=None, *, episodes=300, max_steps=None, seed=0, state=None, action=None, alpha=1e-3)

Runs episodes through a simulator, tests every step against the model, and returns a CheckReport.

codecondition
impossible_transitionthe simulator reached a successor whose model probability is 0
transition_frequencya chi-square test for a state-action pair rejects the model, with a Bonferroni correction
reward_mismatchthe reward for (s, a, s2) differs from the model
termination_mismatchan episode ended where the model continues, or continued where the model ends
impossible_start, start_frequencythe start states disagree with initial
return_mismatchthe mean return lies outside the confidence band around the exact value
unknown_observationan observation is not a model state; pass state= to translate it
  • env= accepts any object with the Gym interface. step may return 5 or 4 values, and reset() may return obs or (obs, info).
  • state=lambda obs: label translates observations, and action=lambda position: ... translates actions.
  • For a history-dependent model, the simulator observes world states and aniate tracks the memory state with the automaton.

The report provides report.ok, report.issues, report.stats and report.describe().


5. Solving

calldescription
m.solve(method="auto")backward induction if horizon is set, policy iteration if gamma < 1, value iteration if gamma = 1
an.evaluate(m, policy)the exact value of any policy, of shape (S,)
an.mdp.values_by_time(m, policy, T)values of shape (T + 1, S), indexed by the number of elapsed steps
Solution attributedescription
policy, valuevalue is always the exact value of policy
boundno state loses more than this amount relative to the optimal policy
start_value, action(state, t=0), as_dict(), describe()convenience accessors
solver, iterations, wall_time, residualsolver diagnostics

6. Plots

Plots use black Computer Modern text, the default LaTeX typeface, on a white background, without titles. Every plot accepts ax= and returns the Axes, and no function calls show().

callcontent
vis.graph(model, policy=None)transition diagram, for models with at most 30 states
vis.automaton(machine)memory states and events
vis.grid(model, solution, values=, policy=, memory=, episode=)gridworld values and policy in greyscale; values=runs.occupancy() shows visit frequencies
vis.returns(runs)return distribution, normal fit and model value
vis.paths(runs)accumulated return along sample episodes, with the 10–90% band and the mean
vis.martingale(runs)value martingale paths, their mean with a 95% band, and the model value
vis.convergence(runs)running mean return with a 95% band
vis.overview(runs)the four preceding plots in one 2 × 2 figure
vis.save(figure_or_ax, "name.pdf")writes a PDF with embedded fonts; other formats are rejected

7. Logs

Every operation writes one line to standard error:

aniate | model    | 5 states | 2 actions | gamma 0.9 | horizon inf | 14 transitions | valid
aniate | solve    | policy_iteration | 4 iterations | 2.8 ms | start value 3.20876 | bound 8.9e-15
aniate | simulate | 50% of 10,000 episodes finished by step 5
aniate | simulate | 10,000 episodes | 49,768 steps | 4 ms | mean return 3.23989 +/- 0.014 | 10,000 terminated | 0 truncated
aniate | check    | PASSED | 300 episodes | 1,505 steps | 4 pairs tested | return 3.1863 vs model 3.2088
aniate | env      | episode 20 done | 6 steps | return 3.439 | terminated | mean return 3.12
aniate | vis      | saved overview.pdf
calldescription
an.verbosity("quiet" | "warning" | "info" | "debug")sets the level; debug adds one line per environment step; the ANIATE_LOG environment variable has the same effect
with an.log.capture() as lines:collects log lines instead of printing them

Env logs completed episodes 1 to 10, then every 10th episode up to 100, then every 100th up to 1,000, and so on.


8. Built-in problems

an.problems.gridworld(rows, cols, goals, traps, walls, slip, gamma, start), chain(), river_swim(), inventory().

9. Worked examples

Each folder contains a model module, two tests, a plot.py script that writes PDF files, and a README with the derivation and results.

foldermodeltestsplots
tests/stsproute.py: a van, four customers, roads blocked at randomthe exact solution equals the minimum over all 24 tours; 20,000 simulated tours agree with itroute.pdf, overview.pdf
tests/gaussianplane.py: a Gaussian random walk between a pool and a pizzathe probability is 0.5 by symmetry and Monte Carlo agrees; with wind it is 0.956 and the martingale mean is constantplane.pdf, overview.pdf
tests/heavy_tailwarehouse.py: power-law jam durations, excess kurtosis about 25the value equals the closed form; the martingale mean is constant; an invalid model is rejected; an incorrect simulator is detectedtail.pdf, overview.pdf
pytest                         # all tests
python tests/stsp/plot.py      # writes tests/stsp/route.pdf and tests/stsp/overview.pdf

10. Import banner

import aniate prints a drawing to standard error once per process: squares of side 1, 1, 2, 3, 5 and 8 tiling a 13 × 8 rectangle, with the golden spiral drawn through them. python -m aniate, or the aniate command, prints it together with the version.

setting or namedescription
ANIATE_BANNER=0disables the banner on import
aniate.art.ART, aniate.art.banner(stream)the drawing, and a function that prints it

11. Notebook display

Evaluating one of the following objects as the last expression of a Jupyter cell displays it as an HTML table. The tables use inline styles and inherit the text colour of the notebook, and a table longer than 20 rows states how many rows were omitted.

objectcontent of the table
MDPsize, discount factor, horizon, validity, initial and terminal states, reward range, the first transitions
Solutionsolver, iterations, start value, bound, residual, and the action and value of each state
Episodesmean return with its standard error, model value, return quantiles, episode lengths
CheckReportthe result, coverage, the tests performed, the returns compared, and each issue with examples
ValidationReporterrors and warnings with examples
NMDP, Mealyworld, memory and product sizes, events, and the memory states with their meaning
MemoryPolicythe current memory state, the start value, and the world states in which memory changes the action

aniate.notebook.to_html(obj) returns the same markup as a string.


12. Gymnasium adapter: aniate.gym

The adapter requires pip install 'aniate[gym]'.

gym.make(model, *, max_steps=None, hide_memory=True, render_mode=None, log=False)

Returns an AniateEnv, a subclass of gymnasium.Env.

argumentmeaning
modelan MDP or an NMDP
max_stepsthe truncation limit; the default is the horizon, or the step at which gamma^t falls below 1e-6
hide_memoryfor an NMDP, observations are world states and info["memory"] reports the memory state
render_modeNone or "ansi"; with "ansi", render() returns a line describing the last step
logwrites log lines for finished episodes
attribute or methoddescription
observation_space, action_spaceDiscrete(S), counting world states when memory is hidden, and Discrete(A)
reset(seed=None, options=None)options={"state": label} fixes the start state; returns (observation, info)
step(action)takes an integer action position and returns the Gymnasium 5-tuple
observation_label(obs), action_label(a)the labels of integer observations and actions

Transitions are sampled from the model with the seeded generator of Gymnasium, so equal seeds give equal episodes. info carries the keys described for Env in section 3, except the episode counter.

gym.register(id, model, **kwargs)

Registers the model with Gymnasium, so that gymnasium.make(id) builds it. model is a model, or a function without arguments that returns one; kwargs are passed to AniateEnv.

© 2026 Kabir Murjani. MIT Licence.