Model Analysis and Visualization¶
These modules extract structured metadata from block models, analyze their decision structure, and render them as plate-notation diagrams.
Model Analyzer¶
ModelAnalyzer: Extracts structured metadata from scikit-agent DBlock/RBlock models.
The analyzer builds an annotated dependency graph (self.G) as its source of
truth and exposes it in two forms:
to_dict()– node metadata and classified edges for visualization (e.g., plate-notation drawing via ModelVisualizer).influence_graph()– the influence-diagram (SCIM) view consumed byskagent.relevancefor strategic-relevance analysis.
Key concepts: - instant edge: dependency within the same time period - lag edge: dependency from previous time period (including self-lag like p_{t-1} → p_t) - param edge: dependency from a calibration parameter - shock edge: dependency from an exogenous shock
- class skagent.model_analyzer.ModelAnalyzer(model, calibration, block_agent=None, discount=None)¶
- Analyze a scikit-agent DBlock or RBlock and extract:
node_meta: kind, agent, plate, observed for each variable
edges: instant / lag / param / shock dependencies
plates: the entity classes the block tree declares
- analyze()¶
Run the full analysis pipeline.
The annotated dependency graph
self.Gis the source of truth; the publicnode_meta/edges/platesare derived from it.
- influence_graph(dynamic=False)¶
Return the SCIM (influence-diagram) view for strategic-relevance analysis.
The graph
skagent.relevanceconsumes: chance / decision / utility nodes with the causal (instant + shock) edges between them. Parameter nodes are dropped – they are deterministic constants, not random variables, and leaving them in would open spurious d-connection paths (an un-conditioned forkA <- p -> B) that corrupt s-reachability. Lag edges are excluded here (single-period scope); cross-period reliance is handled by the unrolling machinery separately.- Parameters:
dynamic (bool, optional) – Make the diagram faithful to one period of a recurring problem, by splitting each reassigned variable’s arrival value into its own
<name>*node and adding a continuation-value utility node per deciding agent (skagent.influence.SCIM.with_lagged_arrivals(),skagent.influence.SCIM.with_continuation()). Without this a single-period projection is blind to payoffs arriving through the next period’s value, and conflates a variable’s arrival value with the value it is reassigned to. With it, a decision’s parents are its information set. Off by default, since it changes the node set existing callers see.- Return type:
- to_dict()¶
Return a JSON-serializable dict of the analysis.
Model Visualizer¶
Influence Diagrams¶
The structural view of a block – chance, decision and utility nodes with causal
edges – and the d-separation engine the graphical criteria below are posed
over, together with the transforms that pose them:
with_edge(), which adds an information link, and
without_edges(), which drops the links a reduction
finds inert.
The influence-diagram substrate the graphical criteria are posed over.
A block can be read two ways: executably, by running its dynamics, and structurally, by asking which symbol can influence which without evaluating anything. The second reading is what d-separation answers, and d-separation is only definable on a graph. This module owns that graph – the SCIM view: chance, decision and utility nodes with directed causal edges – together with the vocabulary and the traversal engine every criterion over it needs.
The name is Def. 4 of Everitt, Carey, Langlois, Ortega & Legg, “Agent Incentives: A Causal Perspective” (AAAI-21, 35(13):11487-11495; arXiv:2102.01685), where a structural causal influence model is an influence diagram whose mechanisms are structural functions of their parents rather than conditional probability tables. That is the form a block already takes.
The criteria themselves live in skagent.relevance. Construction of a
SCIM from a block lives in skagent.model_analyzer. This module
depends only on networkx, so the substrate and the criteria can be developed
and tested without constructing a model.
The engine is the Bayes-Ball / Reachable sweep (Shachter 1998; Koller &
Friedman Alg. 3.1) rather than an ancestral moral graph, because the query is
set-wide: SCIM.d_connected() answers “which nodes are d-connected to these
targets” in one traversal, and moralization is valid only over the ancestral
closure of {candidate} | targets | given, which differs per candidate.
Because the sweep is hand-rolled it is property-tested against
networkx.is_d_separator on random DAGs; treat that oracle test as
non-optional for any change to it.
Two limits every criterion over this substrate inherits:
d-separation is sound but not complete under determinism, and these models are largely deterministic functions of their parents. Criteria are therefore conservative: a route whose functional effect vanishes still counts. The standard repair is Geiger-Verma-Pearl D-separation, treating functionally determined nodes as observed.
A single-period diagram cannot express a payoff arriving through the next period’s value, nor distinguish a variable’s arrival value from the value it is reassigned to.
SCIM.with_continuation()andSCIM.with_lagged_arrivals()repair both.
- skagent.influence.CONTINUATION_PREFIX = '__continuation__'¶
Node-name prefix for the synthetic continuation utility.
- skagent.influence.DUMMY_PREFIX = '__hat__'¶
Node-name prefix for the synthetic parent of
SCIM.with_dummy_parent().
- skagent.influence.LAG_SUFFIX = '*'¶
Suffix marking a variable’s arrival value, as distinct from the end-of-period value the plain name carries.
- class skagent.influence.SCIM(graph, decisions, agent_utilities, decision_agent)¶
The influence-diagram view of a model.
- Parameters:
graph (networkx.DiGraph) – A DAG of chance / decision / utility nodes, each carrying a
kindattribute, with directed causal edges. Parameters must already be dropped: they are deterministic constants rather than random variables, and an un-conditioned fork through one opens spurious d-connection.decisions (iterable) – The decision nodes.
agent_utilities (mapping) –
agent_utilities[agent]is the utility nodes owned by agent.decision_agent (mapping) –
decision_agent[decision]is the agent that owns each decision.
Notes
Traversals are memoized per instance, so a criterion may query freely, and every transform returns a new instance rather than mutating this one. Both rest on graph not changing after construction; mutate it and the caches go stale.
- ancestors(nodes)¶
Every strict ancestor of any node in nodes.
One reverse multi-source traversal, rather than a
networkx.ancestors()call per node.
- context(decision)¶
The conditioning set
Pa(D) | {D}every criterion conditions on.What the decision-maker knows when choosing, plus the choice itself.
- d_connected(targets, given)¶
Every node d-connected to some node of targets, conditioning on given.
The complement is the d-separated set, so
node not in d_connected(...)certifies that no active trail carries influence fromnodeto any target. One traversal answers this for every node at once, so callers should ask once per decision and test membership rather than callingnetworkx.is_d_separator()per candidate.targets and given are excluded from the result.
- objectives(decision)¶
The utility nodes whose value decision is choosing over.
Utilities owned by the deciding agent and downstream of the decision. Includes the synthetic continuation node when
with_continuation()has been applied and the decision reaches it.
- parents(node)¶
The parents of node, which for a decision are its information set.
- utilities(decision)¶
Every utility node the agent deciding decision owns.
Wider than
objectives(), which keeps only the ones the decision reaches: a variable can be worth controlling for the sake of a payoff the decision itself has no route to.
- with_continuation(arrival_states)¶
Add a synthetic continuation-value utility node per deciding agent.
A single-period diagram cannot express that a decision’s payoff continues into the next period, so a shock reaching the objective only through the next period’s value appears irrelevant. This adds, per agent that decides, a utility node whose parents are the nodes named after the model’s arrival-state variables – for a variable that is also reassigned in-period, that node holds the reassigned value, which is what the next period arrives with.
- Parameters:
arrival_states (iterable) – The model’s arrival-state variable names.
- Return type:
- with_dummy_parent(node)¶
Add a fresh exogenous parent to node.
The device s-reachability is defined by: a decision node’s own value is not the object of interest, its decision rule is, and a synthetic parent stands in for that rule.
- Returns:
(scim, dummy)– a new SCIM, and the name of the added node.- Return type:
- with_edge(source, target)¶
Add the directed edge
source -> target.Both endpoints must already be nodes, and the edge must not close a cycle: a criterion posed over a graph is not answerable on a graph that is no longer a DAG.
- Return type:
- Raises:
ValueError – If either endpoint is absent, or the edge would create a cycle.
- with_lagged_arrivals(lag_dependencies)¶
Split each variable’s arrival value out into its own node.
A single-period diagram carries one node per symbol, but a symbol reassigned within the period holds two values: the one it arrives with and the one it is reassigned to. This adds a source node
<name>*for the arrival value, with an edge to each consumer that reads it. The plain node keeps its in-period parents and so denotes the end-of-period value, which is what the next period arrives with.A decision’s parents in the result are exactly its information set, so callers may read the conditioning set off the graph.
- Parameters:
lag_dependencies (iterable of (consumer, source) pairs) – Dependencies that read a source’s pre-period value – the edges a single-period projection would otherwise drop.
- Return type:
Relevance¶
What a decision must still account for, given what it already knows. One
d-separation test, read two ways: across decisions it is the Koller & Milch
s-reachability criterion, and the order the resulting relevance graph implies,
condensation(), is what
skagent.algos.tabular.TabularBestResponseSolver solves a block in (see
Algorithms). Run from a shock instead, it tells a solver whether to grid
that shock or integrate it inside the maximization.
Incentive criteria¶
The same substrate answers a third question, about a node that is neither a decision nor a shock: what one decision stands to gain from it, or does to it. These are the four criteria of Everitt, Carey, Langlois, Ortega & Legg, “Agent Incentives: A Causal Perspective” (AAAI-21; arXiv:2102.01685) –
admits_voi(): would observing the node raise the achievable payoff?admits_ri(): does every optimal policy respond to a change in it?admits_voc(): would setting it raise the achievable payoff?admits_ici(): does the decision reach its payoff through it?
Each is sound and complete for a diagram holding exactly one decision, and a
diagram with more is refused rather than answered. Three of the four run over
the minimal_reduction(), the diagram with every
observation is_requisite() rejects unwired.
The answers are properties of the graph, not of a calibration: False means the
incentive is absent under every parameterization the diagram admits, and True
means some parameterization has it, not that yours does.
What a decision must still account for, given what it already knows.
Every criterion here answers one question about a decision D in the
SCIM view of a model: conditioning on what D
observes, does some other node still reach D’s objective? What varies is the
node asked about, and what a positive answer means for a solver.
A decision – s-reachability, the criterion of Koller & Milch, “Multi-Agent Influence Diagrams for Representing and Solving Games” (IJCAI-01; Games and Economic Behavior 45(1), 2003), Defs. 7-8:
Decision D strategically relies on decision D’ iff D’ is s-reachable from D.
The relevance graph is a directed graph over decision nodes with an edge D -> D’ iff D relies on D’ (equivalently, D’ is s-reachable from D).
D’ is s-reachable from D iff there is a utility node U owned by D’s agent and descended from D such that, adding a fresh dummy parent to D’, there is an active path (d-connection) from the dummy to U given Pa(D) u {D}.
A decision node’s own value is not what matters – its decision rule is – so the test is run from a synthetic parent standing in for that rule. The reliance ordering a relevance graph implies is what a best-response sweep solves a block in.
A shock – the same test, run from the shock itself, since an exogenous node
is already its own synthetic source. Here what varies is the reading, because a
solver needs to know not just whether the shock matters but how to integrate it:
see OBSERVED, HIDDEN and MIXED.
A node that is neither – the four incentive criteria of Everitt, Carey, Langlois, Ortega & Legg, “Agent Incentives: A Causal Perspective” (AAAI-21, 35(13):11487-11495; arXiv:2102.01685), which ask what one decision stands to gain from a variable, or does to it:
admits_voi()(Def. 8, Thm. 9) – would observing X raise the achievable payoff?
admits_ri()(Def. 10, Thm. 12) – does every optimal policy respond to a change in X?
admits_voc()(Def. 15, Thm. 16) – would setting X raise the achievable payoff?
admits_ici()(Def. 17, Thm. 18) – does the decision reach its payoff through X?
Each is sound and complete, and each is stated for a diagram holding exactly one
decision; a diagram with more is refused rather than answered. Three of the four
run over the minimal_reduction(), the diagram with every observation
is_requisite() rejects unwired.
Completeness is a property of the graph. Under determinism d-separation
over-reports d-connection (see skagent.influence), and these mechanisms
are largely deterministic, so an incentive may be reported where the functional
effect vanishes – never the reverse. For a safety criterion that is the safe
direction of error, but it is a direction.
Each is a thin function over a SCIM, which owns the
graph, the conditioning-context and objective vocabulary, and the d-separation
engine. Construction of a SCIM from a scikit-agent Block lives in
skagent.model_analyzer; this module, like the substrate, depends only on
networkx so the criteria can be developed and tested in isolation.
- skagent.relevance.HIDDEN = 'hidden'¶
A shock the information set says nothing about; integrate inside the max.
- skagent.relevance.MIXED = 'mixed'¶
Partly informed and separately relevant; needs filtering, so refuse.
- skagent.relevance.OBSERVED = 'observed'¶
A shock the information set accounts for; may be gridded per node.
- class skagent.relevance.RelevanceGraph(graph)¶
A relevance graph over decision nodes (edge d1 -> d2 iff d1 relies on d2).
Wraps a
networkx.DiGraphbut never leaks it: all helpers return native Python types.- condensation()¶
SCCs in backward-induction (solve) order.
Returns a list of sets of decision nodes such that each component relies only on components appearing earlier in the list. Solving the game in this order (a la Koller & Milch Algorithm 1) means every decision an SCC relies on is already solved by the time the SCC is reached.
- draw()¶
Render the relevance graph to a
pydot.Dotobject.pydot is imported lazily so the core criterion has no hard dependency on the rendering stack.
- edges()¶
The reliance edges (d1, d2) meaning “d1 relies on d2”, as a list.
- classmethod from_scim(scim)¶
Build the relevance graph by testing s-reachability over all ordered pairs of
scim’s decisions.
- is_acyclic()¶
True iff the relevance graph has no cycles.
- nodes()¶
The decision nodes, as a list.
- relies_on(first, second)¶
True iff decision
firststrategically relies onsecond.
- sccs()¶
Strongly connected components, as a list of sets of decision nodes.
- skagent.relevance.admits_ici(scim, decision, node)¶
Does
nodeadmit an instrumental control incentive (Def. 17)?True when the decision reaches a payoff through
node, so an agent optimizing the decision has reason to move it. The criterion (Thm. 18) is a directed pathdecision -> ... -> node -> ... -> utilityin the diagram as declared – not in theminimal_reduction(), since what the decision can influence does not depend on what it observes.The decision itself, and any utility it reaches, lie on such a path trivially and so admit the incentive.
- Parameters:
scim (skagent.influence.SCIM) – A diagram with exactly one decision.
decision (hashable) – That decision.
node (hashable) – Any node of the diagram.
- Return type:
- Raises:
ValueError – If the diagram holds more than one decision.
- skagent.relevance.admits_ri(scim, decision, node)¶
Does
nodeadmit a response incentive fordecision(Def. 10)?True when every optimal policy responds to a change in
node. The criterion (Thm. 12) is a directed path fromnodeto the decision in theminimal_reduction()– a route by which the decision must hear about it, once the links that carry nothing are gone.A response incentive on a sensitive attribute means every optimal policy is counterfactually unfair in the sense of Kusner et al. (2017), by Thm. 14 of the same paper.
- Parameters:
scim (skagent.influence.SCIM) – A diagram with exactly one decision.
decision (hashable) – That decision.
node (hashable) – Any node other than the decision.
- Return type:
- Raises:
ValueError – If the diagram holds more than one decision, or if
nodeis the decision, which the definition excludes.
- skagent.relevance.admits_voc(scim, decision, node)¶
Does
nodeadmit positive value of control (Def. 15)?True when being able to set
node– rather than take it as it comes – could raise the achievable payoff. The criterion (Thm. 16) is a directed path fromnodeto a utility the deciding agent owns, in theminimal_reduction(). The path may run through the decision.- Parameters:
scim (skagent.influence.SCIM) – A diagram with exactly one decision.
decision (hashable) – That decision.
node (hashable) – Any node other than the decision.
- Return type:
- Raises:
ValueError – If the diagram holds more than one decision, or if
nodeis the decision, which the definition excludes: a decision is already under the agent’s control.
- skagent.relevance.admits_voi(scim, decision, node)¶
Does
nodehave value of information fordecision(Def. 8)?True when observing
nodecould raise the achievable payoff. The criterion (Thm. 9) is thatnodeis a requisite observation in the diagram with the information linknode -> decisionadded.- Parameters:
scim (skagent.influence.SCIM) – A diagram with exactly one decision.
decision (hashable) – That decision.
node (hashable) – A node outside
Desc(decision) u {decision}.
- Return type:
- Raises:
ValueError – If the diagram holds more than one decision, or if
nodeis the decision or descends from it – there is no diagram in which such a node is observed, since the added information link would close a cycle.
- skagent.relevance.is_requisite(scim, decision, node)¶
Is the observation
noderequisite fordecision?Requisite means the decision rule may need to read it:
nodeis still d-connected to some objective given everything else the decision observes, plus the decision itself. A nonrequisite observation (Def. 7; Lauritzen & Nilsson 2001) is one for whichXis independent ofU_DgivenPa(D) u {D} \ {X}, so its information link carries nothing.- Parameters:
scim (skagent.influence.SCIM) – A diagram with exactly one decision.
decision (hashable) – That decision.
node (hashable) – An observation of
decision– one of its parents.
- Return type:
- Raises:
ValueError – If the diagram holds more than one decision, if
nodeis not an observation ofdecision, or if the deciding agent owns no objective downstream of the decision.
- skagent.relevance.is_s_reachable(scim, d1, d2)¶
Is decision
d2s-reachable from decisiond1?Equivalently: does
d1strategically rely ond2(edge d1 -> d2 in the relevance graph)?- Parameters:
scim (skagent.influence.SCIM) – The influence-diagram view the decisions live in.
d1 (hashable) – Decision nodes in
scim.d2 (hashable) – Decision nodes in
scim.
- Return type:
- skagent.relevance.minimal_reduction(scim)¶
The diagram with every nonrequisite information link removed (Def. 11).
Also known as the requisite graph, the d-reduction, or the trimmed graph. What is left of the decision’s parents is what an optimal decision rule can depend on, which is why the response-incentive and value-of-control criteria are posed over this graph rather than the declared one.
The links are found once and dropped together: with a single decision there is nothing for a second pass to find, since removing a link that carries no information cannot make another link stop carrying any.
- Parameters:
scim (skagent.influence.SCIM) – A diagram with exactly one decision.
- Returns:
A new diagram; the input is untouched.
- Return type:
- skagent.relevance.shock_roles(scim, shocks, decisions=None)¶
Classify every shock for every decision.
Each shock takes one of three roles, per decision:
OBSERVEDEvery route from the shock to the objective is intercepted by the information set. Conditioning on the information set therefore leaves nothing about the shock for the objective to depend on, and a solver may grid the shock over its discretization nodes and solve per node.
HIDDENThe shock reaches the objective around the information set, which carries no information about it. An expectation over it belongs inside the maximization.
MIXEDThe shock reaches the objective around the information set, and the information set is partly informative about it. Computing the declared problem then requires the conditional law of the shock given the information set, so neither per-node solving nor integrating inside the maximization applies and a solver should refuse. This most often indicates that a reward or transition touches a shock the control’s information set claims not to see, which is a modeling error rather than a solver limitation.
The test is on the diagram, not on the syntax of the information set: a shock that appears in no information set may still be accounted for, because it feeds a derived pre-decision variable that an information set does contain.
- Parameters:
scim (skagent.influence.SCIM) – The influence-diagram view, after
with_lagged_arrivals()andwith_continuation(), so that a decision’s parents are its information set.shocks (iterable) – Shock variable names.
decisions (iterable, optional) – Decisions to classify for. Defaults to every decision in scim.
- Returns:
{decision: {shock: role}}.- Return type:
- Raises:
ValueError – If a decision has no objective nodes. Nothing is then reachable, so every shock would classify
OBSERVED– the one direction that must not be silent, since it invites a solver to condition on a shock the agent cannot see. It means the deciding agent owns no reward downstream of its own decision.
Notes
The classification is per decision and may legitimately differ between two controls in one period: a shock accounted for by a rich information set is hidden to a control that conditions on less. A solver that represents a shock one way for the whole period must check that the roles agree across controls.
Errors fall toward
HIDDENorMIXED, never toward wrongly reporting a shock as accounted for.One traversal per decision, then membership tests, so the cost is linear in the graph per decision rather than per (shock, decision) pair.