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 by skagent.relevance for 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.G is the source of truth; the public node_meta / edges / plates are derived from it.

influence_graph(dynamic=False)

Return the SCIM (influence-diagram) view for strategic-relevance analysis.

The graph skagent.relevance consumes: 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 fork A <- 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:

skagent.influence.SCIM

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:

  1. 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.

  2. 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() and SCIM.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 kind attribute, 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 from node to any target. One traversal answers this for every node at once, so callers should ask once per decision and test membership rather than calling networkx.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:

SCIM

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:

tuple

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:

SCIM

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:

SCIM

without_edges(edges)

Drop edges, an iterable of (source, target) pairs.

Edges that are not present are ignored, so the result is the diagram without any of them however many were there to begin with.

Return type:

SCIM

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 decisions-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.DiGraph but 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.Dot object.

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 first strategically relies on second.

sccs()

Strongly connected components, as a list of sets of decision nodes.

skagent.relevance.admits_ici(scim, decision, node)

Does node admit 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 path decision -> ... -> node -> ... -> utility in the diagram as declared – not in the minimal_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:

bool

Raises:

ValueError – If the diagram holds more than one decision.

skagent.relevance.admits_ri(scim, decision, node)

Does node admit a response incentive for decision (Def. 10)?

True when every optimal policy responds to a change in node. The criterion (Thm. 12) is a directed path from node to the decision in the minimal_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:

bool

Raises:

ValueError – If the diagram holds more than one decision, or if node is the decision, which the definition excludes.

skagent.relevance.admits_voc(scim, decision, node)

Does node admit 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 from node to a utility the deciding agent owns, in the minimal_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:

bool

Raises:

ValueError – If the diagram holds more than one decision, or if node is the decision, which the definition excludes: a decision is already under the agent’s control.

skagent.relevance.admits_voi(scim, decision, node)

Does node have value of information for decision (Def. 8)?

True when observing node could raise the achievable payoff. The criterion (Thm. 9) is that node is a requisite observation in the diagram with the information link node -> decision added.

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:

bool

Raises:

ValueError – If the diagram holds more than one decision, or if node is 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 node requisite for decision?

Requisite means the decision rule may need to read it: node is 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 which X is independent of U_D given Pa(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:

bool

Raises:

ValueError – If the diagram holds more than one decision, if node is not an observation of decision, or if the deciding agent owns no objective downstream of the decision.

skagent.relevance.is_s_reachable(scim, d1, d2)

Is decision d2 s-reachable from decision d1?

Equivalently: does d1 strategically rely on d2 (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:

bool

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.influence.SCIM

skagent.relevance.shock_roles(scim, shocks, decisions=None)

Classify every shock for every decision.

Each shock takes one of three roles, per decision:

OBSERVED

Every 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.

HIDDEN

The shock reaches the objective around the information set, which carries no information about it. An expectation over it belongs inside the maximization.

MIXED

The 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() and with_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:

dict

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 HIDDEN or MIXED, 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.