Skip to content

Markov Decision Processes

Mock Machines’ core loop — states, probability matrices, condition-gated events, counters — is already most of a Markov Decision Process substrate. This article maps each MDP and POMDP concept (state, action, transition, reward, discount, observation) onto scenario YAML, names the patterns that make the mapping work, and is honest about what the engine cannot express in-config and how to work around it. Four scenarios serve as worked examples: CliffWalking, RecyclingRobot, TigerPOMDP, and Thermostat.

An MDP state in Mock Machines is the product of two layers:

  • The FSM state (states:) — the discrete control state. Use it for the qualitative component of the MDP state (battery High/Low, Listening/Deciding) and for sequencing within a step (see the two-stage pattern below).
  • Fields — counters (int), measures (float), taxonomy (categorical), dimensions (strings) — the factored, quantitative component (temperature, inventory, accumulated evidence).

A third encoding is references-as-position: CliffWalking’s Walker holds its grid position not as coordinates but as a current_cell reference into a graph of GridCell entities, and moves by re-pointing it (set_reference with an indirect value path like self.current_cell.north). Use this when the state space has structure worth materialising as entities.

The events of the current FSM state are the action set A(s). State-dependent action spaces (A(s) ⊆ A) are covered in depth in the companion article State-Dependent Action Spaces: pre-selection conditions mask ineligible events and the engine renormalizes the remaining probability mass.

How the action gets chosen is the policy, and there are four encodings:

EncodingPolicy classExample
The state’s probabilities: rowsOne fixed stochastic policy π(a|s), age-indexedEvery example scenario
policy_tables: + policy_fieldPer-entity stochastic policies keyed by a taxonomy value — a small population of competing policies in one runCliffWalking (Safe/Cautious/Risky), RecyclingRobot (Bold/Timid)
decision: first_valid_winsDeterministic rule policy: condition order is priority orderThermostat’s threshold rule, TigerPOMDP’s open-or-listen
--request <event> --target <Machine> (and the facade’s targeted-request run)An external controller injecting one action per turn — the hook for driving an entity from an outside policy or learnerCLI stepping; REST POST .../run with a request

A probability row chooses an event; an event’s transition action names one next state. So a single state cannot express “action a leads to s′ with probability 0.7 and s″ with 0.3”. The pattern that does is the two-stage action→outcome encoding:

Action state (the agent chooses) Outcome state (the environment answers)
High: ForageHigh:
policy_table: high_policy events:
events: - found_cans_stay_high → High (+5)
- search → ForageHigh - found_cans_battery_low → Low (+5)
- wait (+2, stay) probabilities:
- [0.0, 0.7, 0.3] # P(s'|High, search)

The outcome state’s age-0 row is the row of the transition matrix for that (s,a) pair, and the reward rides on the outcome events. RecyclingRobot is the worked example. Three refinements:

  • Conditional kernels: conditions on outcome events make P(s′|s,a) depend on fields — the engine masks failing events and renormalizes the rest (the same machinery as SDAS pre-selection).
  • Time-inhomogeneous dynamics: probability rows are age-indexed (one row per turn spent in the state, last row clamps), so P can depend on sojourn time — a semi-Markov flavour. Age resets on every transition.
  • Step-rate accounting: a two-stage step takes two engine turns. When comparing policies inside one scenario this cancels; when comparing across scenarios, normalise by turns.

Rewards are update actions with operator: add on a counter or measure (CliffWalking’s total_reward). Keep distinct reward channels in distinct fields (Thermostat’s comfort_reward vs energy_cost) so objectives can be reweighed offline.

Partial observability is one engine feature (bounded history, next section) plus two config disciplines:

  • Hidden state = fields that policy events never condition on. Environment events (emissions, payoffs) may read them. In TigerPOMDP, tiger_position is sampled at spawn and read only by the emission events and the door-outcome split. For stronger separation, hold hidden state on a separate entity and reach it via references or messages — but the single-entity form reads better.
  • Emission distributions O(o|s) = a stochastic state whose events are condition-gated on the hidden value. TigerPOMDP’s Listening row [0, .425, .075, .425, .075] crossed with tiger_position conditions renormalizes to 0.85/0.15 per hidden value; the selected event writes an observation code into an obs counter. The same shape generalises to any finite observation alphabet.

The policy then conditions on observations and their history, never on the hidden field — a finite-memory (window) policy, the standard practical proxy for the belief state.

A POMDP agent needs memory. Declaring history: K on a field keeps that entity’s last K end-of-turn values addressable, and lag: k on a condition reads the value as of the end of turn t−k:

fields:
counters:
- { name: obs, init: 0, history: 2 }
...
conditions:
- { field: obs, lag: 1, operator: is, value: 2 } # last turn's obs
- { field: obs, lag: 2, operator: is, value: 2 } # the turn before

Mechanics, in brief: the loader expands K hidden ring-slot fields (obs@h0…) plus one HistoryHead push counter per machine — ordinary archetype columns, so the columnar store, snapshots, checkpoints, and exports carry them with no special handling. At every turn open the engine copies each history field’s current value into slot head % K and increments the head; a lag-k read is slot (head−k) mod K. Rotation is index arithmetic over preallocated columns: nothing shifts, nothing allocates per turn. Before K pushes have happened a lag read returns the field’s init value (fail-closed-by-init). Depth is capped at 64; history on passive machines is rejected at load (it would never advance); history requires the tick clock — under the event clock an entity steps only when due, so the head would count steps rather than turns and lag: k would no longer mean “k turns ago”, so the combination is rejected at load (and a run-scoped --clock event override is ignored with a warning); lag applies to direct self fields only.

  • Terminal states: a state with no outgoing transitions ends the process (CliffWalking’s FellOffCliff/ReachedTarget); add passive_on_entry: true to also retire the entity from the step loop (TigerPOMDP’s Opened).
  • Finite horizon: --run <turns> bounds the run; age-indexed probability rows can also force progression after a fixed sojourn.
  • Continuing tasks: simply provide no terminal state (RecyclingRobot); compare policies by reward rate.
  • Episodic restart: transition back to the initial state to start a fresh episode inside one entity, or spawn a fresh entity per episode with new_entity.

Measures (float64 columns) give a continuous state space, and float conditions (atLeast/atMost) give threshold policies over it. Continuous dynamics come from distribution-valued updates — an update on a float field may draw its value per execution:

- { type: update, field: temperature, operator: add,
value: { distribution: normal, mean: 1.5, std: 0.3 } }
# also: { distribution: uniform, min: A, max: B }
# { distribution: exponential, rate: L } # mean 1/L, always positive

Each drawn value is recorded in the event log as the update’s resolved value, so log replay still reproduces the run exactly (the engine’s no-seed reproduction convention). Per-entity continuous initial state comes from seed CSVs (Thermostat’s rooms seed at spread-out temperatures).

One boundary to be clear about: action selection remains a choice among finitely many events. Continuous action effects (a draw per execution) are first-class; a continuum of actions (e.g. “set the heater to any u ∈ [0,1]”) must be discretised into events.

ScenarioClassDemonstrates
CliffWalkingDiscrete MDP, deterministic movesReferences-as-position, policy tables, pre-selection conditions + renormalization (the SDAS pattern), per-step and terminal rewards.
RecyclingRobotDiscrete MDP, stochastic transitions, continuing taskThe two-stage action→outcome encoding of P(s′|s,a); competing policy populations (Bold/Timid).
TigerPOMDPPOMDP, episodicHidden-state discipline, emission distributions via condition renormalization, history:/lag: as the belief proxy, terminal passivation.
ThermostatContinuous-state MDPFloat measures as state, threshold policies, normal-noise dynamics via distribution-valued updates, separable reward channels.

Each scenario’s own docs/index.html covers its theory and a suggested visualisation of its output data.

ReferenceRelevance
Sutton & Barto, Reinforcement Learning: An Introduction, 2nd ed., 2018 — §3 (Finite MDPs)The MDP formalism; the recycling robot (Example 3.3) and cliff walking (Example 6.6) used by the example scenarios.
Kaelbling, Littman & Cassandra, “Planning and acting in partially observable stochastic domains”, Artificial Intelligence 101, 1998The POMDP formalism and the tiger problem.
State-Dependent Action Spaces (this site)The A(s) ⊆ A masking-and-renormalization machinery this article builds on for conditional kernels and emissions.