mixle.experimental.active_causal module

P15 (experimental) – active causal discovery: buying the interventions that matter.

A chain X0->X1->X2, the reverse X2->X1->X0, and the fork X0<-X1->X2 share the skeleton 0-1-2 (one Markov equivalence class). They are parameterized as factorizations of the same observational joint Gaussian, so observation cannot orient them. Interventions break the tie decisively – do(X1) moves X2 under the chain, X0 under the reverse, and both under the fork. This module chooses which intervention to run by expected information gain (EIG) over a posterior on candidate causal structures, and identifies the true structure with fewer experiments than random or observation-only selection.

Everything is exact linear-Gaussian, so the structure likelihood is closed form and the synthetic ground truth is known – active causal discovery can be graded exactly, which the field mostly cannot do.

Exploratory mixle.experimental code (P15 card).

class LinearGaussianSCM(name, n_nodes, parents, weight=0.9, noise=1.0, edge_weights=None, noise_scales=None)[source]

Bases: object

A linear-Gaussian structural causal model over n_nodes variables.

parents[j] lists node j’s parents; each contributes weight to j’s mean. Nodes with no parents are exogenous N(0, noise^2).

Parameters:
log_likelihood(x, intervention=None)[source]

Total log-density of rows x under this SCM given the intervention regime.

Parameters:
Return type:

float

observational_covariance()[source]

Return the exact observational covariance implied by the acyclic SCM.

Return type:

ndarray

class StructurePosterior(candidates, log_w=None)[source]

Bases: object

Posterior over a fixed list of candidate SCMs, updated by experiment likelihoods.

Parameters:
expected_information_gain(posterior, regime, *, n_batch, rng, n_outcomes=6)[source]

EIG of running regime once: current entropy minus the expected posterior entropy.

Outcomes are simulated from each candidate weighted by the current posterior (the posterior-predictive), so no ground truth leaks into the design.

Parameters:
Return type:

float

default_regimes(n_nodes, value=2.0)[source]

Observation plus do(node = value) for each node.

Parameters:
Return type:

list[tuple[int, float] | None]

class DiscoveryResult(identified: 'int', correct: 'bool', n_experiments: 'int', final_probs: 'list[float]')[source]

Bases: object

Parameters:
active_discovery(true_scm, candidates, *, strategy='eig', n_batch=8, threshold=0.95, max_experiments=40, seed=0, value=2.0)[source]

Run the act-observe-update loop until the posterior concentrates or the budget runs out.

strategy="eig" picks the max-EIG regime each step; "random" picks uniformly; "obs" only ever observes. Returns which structure was identified and how many experiments it took.

Parameters:
  • true_scm (LinearGaussianSCM)

  • candidates (list[LinearGaussianSCM])

  • strategy (str)

  • n_batch (int)

  • threshold (float)

  • max_experiments (int)

  • seed (int)

  • value (float)

Return type:

DiscoveryResult

markov_equivalent_triple(weight=0.9, noise=1.0)[source]

The chain / reverse-chain / fork over 3 nodes – observationally equivalent, do()-separable.

Parameters:
Return type:

list[LinearGaussianSCM]