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)
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: loop-notation plates inferred from agents

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.

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

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_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

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

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.

Both are thin functions 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.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.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.