Statechart¶
A statechart is the policy drawn as a flowchart: named states, arrows
labelled with events, and an optional proposed action on each state.
You build one directly with Statechart when you need payoffs,
priorities or environment-owned states (environment=True) that the
text policy cannot express; otherwise
Module.from_expr builds it for you. The game modules here run once,
when a Module is built, to compute the "can the workflow keep going?"
check.
Public here: Statechart (top level). Transition, GameGraph,
compile_game, even_wins and max_mean_payoff are importable from
the submodules but mainly matter to contributors. What the game check
does and does not catch:
Can the workflow keep going?;
solver internals: Games and solvers.
machine ¶
Statecharts compiled from explicit states or from a DFA.
A statechart is the encapsulated control object: named states, the transitions between them, and what each state proposes. Charts are flat (no nested or parallel states), so the compiled game stays a single graph.
Statechart
dataclass
¶
Statechart(
name: str,
initial: str = "start",
_states: dict[str, Verdict | None] = dict[
str, Verdict | None
](),
_actions: dict[str, str | None] = dict[
str, str | None
](),
_transitions: list[Transition] = list[Transition](),
_parent: dict[str, str | None] = dict[
str, str | None
](),
_payoff: dict[str, float] = dict[str, float](),
_priority: dict[str, int] = dict[str, int](),
_environment: dict[str, bool] = dict[str, bool](),
)
Mutable builder; freeze with validate() before use.
state ¶
state(
name: str,
*,
initial: bool = False,
verdict: Verdict | None = None,
action: str | None = None,
parent: str | None = None,
payoff: float = 0.0,
priority: int = 0,
environment: bool = False,
) -> Statechart
Add a state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
State name, unique in this chart. |
required |
initial
|
bool
|
Make this the start state. |
False
|
verdict
|
Verdict | None
|
What the state means when reached ( |
None
|
action
|
str | None
|
The action this state proposes. |
None
|
parent
|
str | None
|
Recorded for nesting-aware callers; charts stay flat. |
None
|
payoff
|
float
|
Mean-payoff weight earned on entering the state. |
0.0
|
priority
|
int
|
Parity priority for the progress check. Even means looping through this state forever is fine for the service; odd means it is not. |
0
|
environment
|
bool
|
The environment, not the service, picks the next event here (for example, waiting on a customer). The progress check then assumes the worst choice. |
False
|
Source code in src/grounded_decision/statechart/machine.py
is_environment ¶
dfa_to_statechart ¶
dfa_to_statechart(name: str, dfa: DFA) -> Statechart
Build a statechart from a compiled policy.
State s{i} is DFA state i. The DFA's reject sink (the state
reached when the policy has no continuation for an event) is left out:
an event the policy does not allow in the current state is "not
handled", exactly like an event the policy never names. The entity
stays where it is and nothing is proposed.
Priorities for the progress check: states that propose an action and states with no way out (final states) get 2 (even); every other state gets 1 (odd). The game is won by the largest priority seen infinitely often, so the policy passes the progress check only if, from the start, it can keep reaching states that propose an action or reach a final state. A policy that can loop forever without proposing anything fails.
Source code in src/grounded_decision/statechart/machine.py
game ¶
Compile a statechart to a graph game.
Vertices are states. Edges are transitions. Parity is the state's priority (even = the controller can loop forever; odd = the environment wins the loop). Payoff is the state's mean-payoff contribution.
Only states with no outgoing transitions (final states) get a self-loop, so a play never gets stuck. Other states get no idle loop: waiting in a state forever counts only if that state's own transitions allow it.
parity ¶
Zielonka's algorithm for parity games on modest graphs.
A parity game is played by two players moving a token along the edges of a graph. Every vertex has a priority (a small integer). The controller (player Even) wins an infinite play if the largest priority that appears infinitely often is even; the environment (player Odd) wins otherwise. Each vertex is owned by one player, who chooses the next edge there.
The controller wins from a vertex if it has a strategy that wins every
play starting there, whatever the environment does. When the controller
owns every vertex (the default in charts compiled by
:func:grounded_decision.statechart.game.compile_game; states marked
environment=True belong to the environment), that is the same as:
some path from the vertex reaches a cycle whose largest priority is even.
The solver is the classical recursive algorithm (Zielonka 1998). It returns both players' winning regions, which partition the vertices.
CONTROLLER
module-attribute
¶
Player index of the controller (Even); matches GameGraph.owner 0.
ENVIRONMENT
module-attribute
¶
Player index of the environment (Odd); matches GameGraph.owner 1.
even_wins ¶
even_wins(
game: GameGraph, limits: Limits = DEFAULT_LIMITS
) -> tuple[bool, ...]
For each vertex, whether the controller (Even) wins from it.
Source code in src/grounded_decision/statechart/parity.py
attractor ¶
attractor(
universe: set[int],
target: set[int],
*,
player: int,
game: GameGraph,
successors: list[list[int]],
) -> set[int]
Vertices of universe from which player can force a visit to target.
A vertex joins when its owner is player and some move leads into
the set, or when its owner is the opponent and every move does (an
opponent with no move left inside universe is stuck and joins too).
Source code in src/grounded_decision/statechart/parity.py
mean_payoff ¶
One-player mean-payoff via Karp's minimum/maximum mean cycle.
Two-player tropical elimination (Truffet) is banned: Lean records a finite counterexample (family 104). For 1-player compiled statecharts Karp is exact.
max_mean_payoff ¶
max_mean_payoff(
game: GameGraph, limits: Limits = DEFAULT_LIMITS
) -> float
Maximum mean weight of a reachable cycle from the start (Karp).
Raises:
| Type | Description |
|---|---|
GameTooLarge
|
The game has more than |
Source code in src/grounded_decision/statechart/mean_payoff.py
min_mean_payoff ¶
min_mean_payoff(
game: GameGraph, limits: Limits = DEFAULT_LIMITS
) -> float
Minimum mean weight of a reachable cycle from the start (Karp).
Computed as minus the maximum mean of the same game with every weight negated, so there is a single implementation of Karp's recurrence.
Raises:
| Type | Description |
|---|---|
GameTooLarge
|
The game has more than |
Source code in src/grounded_decision/statechart/mean_payoff.py
nonnegative_mean ¶
nonnegative_mean(
game: GameGraph, limits: Limits = DEFAULT_LIMITS
) -> bool
Zero-threshold winning set test used by the Lean 104 scope.
Raises:
| Type | Description |
|---|---|
GameTooLarge
|
The game has more than |