Reference
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.gymVerifying 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)
| argument | meaning |
|---|---|
P | a 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 |
R | an array of shape (S, A) or (S, A, S), or a list of A matrices shaped like P; rewards are attached to transitions |
gamma | discount factor in [0, 1] |
states, actions | labels, 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 |
terminal | states that end an episode; they must be absorbing with zero reward; inferred if omitted |
horizon | a finite number of steps, or None |
validate | True 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 unchangedfrom_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 method | description |
|---|---|
m.S, m.A, m.states, m.actions, m.gamma, m.horizon, m.initial, m.terminal | the definition |
m.P, m.R | sparse 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.report | validation 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.
| code | condition |
|---|---|
row_sum | a row of transition probabilities does not sum to 1 |
dead_end | a state-action pair has no successor |
negative_probability, non_finite | a probability is negative, or a value is not finite |
terminal_not_absorbing | a terminal state moves to another state or pays a reward |
unbounded_value | gamma = 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)
| call | the 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
| call | description |
|---|---|
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=Trueends the episode when a constraint can no longer be satisfied.
| attribute or method | description |
|---|---|
task.states, task.actions, task.memory, task.machine, task.product | the 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 method | description |
|---|---|
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 method | description |
|---|---|
returns, lengths, terminated, truncated | one entry per episode |
states, actions, rewards | padded arrays of shape (episodes, steps), filled with -1 or 0 after an episode ends |
mean_return, stderr, model_value | the 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_bias | an 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.
| code | condition |
|---|---|
impossible_transition | the simulator reached a successor whose model probability is 0 |
transition_frequency | a chi-square test for a state-action pair rejects the model, with a Bonferroni correction |
reward_mismatch | the reward for (s, a, s2) differs from the model |
termination_mismatch | an episode ended where the model continues, or continued where the model ends |
impossible_start, start_frequency | the start states disagree with initial |
return_mismatch | the mean return lies outside the confidence band around the exact value |
unknown_observation | an observation is not a model state; pass state= to translate it |
env=accepts any object with the Gym interface.stepmay return 5 or 4 values, andreset()may returnobsor(obs, info).state=lambda obs: labeltranslates observations, andaction=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
| call | description |
|---|---|
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 attribute | description |
|---|---|
policy, value | value is always the exact value of policy |
bound | no 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, residual | solver 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().
| call | content |
|---|---|
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| call | description |
|---|---|
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.
| folder | model | tests | plots |
|---|---|---|---|
tests/stsp | route.py: a van, four customers, roads blocked at random | the exact solution equals the minimum over all 24 tours; 20,000 simulated tours agree with it | route.pdf, overview.pdf |
tests/gaussian | plane.py: a Gaussian random walk between a pool and a pizza | the probability is 0.5 by symmetry and Monte Carlo agrees; with wind it is 0.956 and the martingale mean is constant | plane.pdf, overview.pdf |
tests/heavy_tail | warehouse.py: power-law jam durations, excess kurtosis about 25 | the value equals the closed form; the martingale mean is constant; an invalid model is rejected; an incorrect simulator is detected | tail.pdf, overview.pdf |
pytest # all tests
python tests/stsp/plot.py # writes tests/stsp/route.pdf and tests/stsp/overview.pdf10. 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 name | description |
|---|---|
ANIATE_BANNER=0 | disables 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.
| object | content of the table |
|---|---|
MDP | size, discount factor, horizon, validity, initial and terminal states, reward range, the first transitions |
Solution | solver, iterations, start value, bound, residual, and the action and value of each state |
Episodes | mean return with its standard error, model value, return quantiles, episode lengths |
CheckReport | the result, coverage, the tests performed, the returns compared, and each issue with examples |
ValidationReport | errors and warnings with examples |
NMDP, Mealy | world, memory and product sizes, events, and the memory states with their meaning |
MemoryPolicy | the 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.
| argument | meaning |
|---|---|
model | an MDP or an NMDP |
max_steps | the truncation limit; the default is the horizon, or the step at which gamma^t falls below 1e-6 |
hide_memory | for an NMDP, observations are world states and info["memory"] reports the memory state |
render_mode | None or "ansi"; with "ansi", render() returns a line describing the last step |
log | writes log lines for finished episodes |
| attribute or method | description |
|---|---|
observation_space, action_space | Discrete(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.