Actual causation#

Download this page as a Jupyter notebook Open in Colab

This tutorial shows how to use PyPhi to evaluate actual causation — what caused what in a particular observed transition. This is a distinct framework from the integrated information \(\Phi\) of a system in a state; it asks, given that the system went from one state to the next, which events were the actual causes and actual effects of which. The formalism is described in

Albantakis L, Marshall W, Hoel E, Tononi G (2019). What Caused What? A Quantitative Account of Actual Causation Using Dynamical Causal Networks. Entropy, 21 (5), 459. https://doi.org/10.3390/e21050459

The tools are in pyphi.actual. Turning off the progress bars keeps the output readable here; leave them on for real computations.

import pyphi

pyphi.config.progress_bars = False

from pyphi import actual, Direction

Actual causation needs no configuration: PyPhi’s defaults reproduce the 2019 paper.

The example network#

We work through the basic OR-AND network from Figure 1 of the paper. It is a two-unit substrate: an OR gate (unit 0) and an AND gate (unit 1), each receiving input from both.

substrate = pyphi.examples.actual_causation_substrate()
substrate
Substrate2 units · OR, AND
UnitsOR, AND
State spacebinary
Connectivity
ORAND
OR11
AND11
TPM
stateORAND
(0,0)0.00.0
(1,0)1.00.0
(0,1)1.00.0
(1,1)1.01.0

The transition probability matrix gives the dynamics — the probability that each unit turns on given the current state of the system:

substrate.tpm
FactoredTPM2 units · 4 states
Units2
States4
P(next unit on | current state)
stateORAND
(0,0)0.00.0
(1,0)1.00.0
(0,1)1.00.0
(1,1)1.01.0

Name the two units for readability:

OR, AND = 0, 1

The transition of interest#

We observe the whole substrate at time \(t-1\) and again at time \(t\), with OR on and AND off in both observations:

X = Y = (OR, AND)
X_state = Y_state = (1, 0)

The Transition object is the core of every actual causation computation. To build one, pass the substrate, its state at \(t-1\) and at \(t\), and the units of interest on the cause side (\(t-1\)) and the effect side (\(t\)). The state given must be the state of the entire substrate, not just the units in the transition. The transition must also satisfy the realization requirement: if the effect-side occurrence has zero probability given the state at \(t-1\), the transition cannot have occurred, and construction raises TransitionUnreachableError.

transition = actual.Transition(substrate, X_state, Y_state, X, Y)
transition
Transition([OR,AND] ━━▶ [OR,AND])

Cause and effect repertoires#

Cause and effect repertoires can be read off the transition. For example, the right side of Figure 2B shows how the occurrence \(\{OR = 1\}\) at \(t-1\) constrains the probability distribution over the purview \(\{OR, AND\}\) at \(t\):

transition.effect_repertoire((OR,), (OR, AND))
array([[0. , 0. ],
       [0.5, 0.5]])

Similarly, Figure 2C shows how the occurrence \(\{OR, AND = 10\}\) at \(t\) constrains the purview \(\{OR\}\) at \(t-1\):

transition.cause_repertoire((OR, AND), (OR,))
array([[0.5],
       [0.5]])

Note

In all Transition methods the constraining occurrence is passed as the mechanism argument and the constrained occurrence is the purview argument, mirroring the terminology used elsewhere in PyPhi.

Cause and effect ratios#

The transition also computes cause and effect ratios — the log-ratio by which an occurrence raises or lowers the probability of a purview state. The effect ratio of \(\{OR = 1\}\) at \(t-1\) constraining \(\{OR\}\) at \(t\) (Figure 3A) is positive:

round(transition.effect_ratio((OR,), (OR,)), 6)
0.415037

The effect ratio of \(\{OR = 1\}\) constraining \(\{AND\}\) is negative — the occurrence makes the observed AND outcome less likely:

round(transition.effect_ratio((OR,), (AND,)), 6)
-0.584963

And the cause ratio of \(\{OR = 1\}\) at \(t\) constraining \(\{OR, AND\}\) at \(t-1\) (Figure 3B) is:

round(transition.cause_ratio((OR,), (OR, AND)), 6)
0.415037

Finding the minimum information partition#

To find the irreducible cause or effect ratio \(\alpha\) of a particular pair of occurrences (Figure 3C), use find_mip(). Consider the candidate effect link \(\{OR, AND\} \rightarrow \{OR, AND\}\):

link = transition.find_mip(Direction.EFFECT, (OR, AND), (OR, AND))

This returns an object describing the minimum information partition. This particular link is reducible, as its \(\alpha\) is zero:

round(link.alpha, 6)
0.0

The partition attribute shows the partition that reduces it:

link.partition
JointPartition
OR/OR × AND/AND × ∅/∅
Severed connections
Connections cut2
ORAND
OR·✕
AND✕·

The candidate cause link \(\{OR, AND\} \rightarrow \{OR, AND\}\) (Figure 3D), by contrast, is irreducible, with positive \(\alpha\):

cause_link = transition.find_mip(Direction.CAUSE, (OR, AND), (OR, AND))
round(cause_link.alpha, 6)
0.169925

To find the actual cause or actual effect of an occurrence — the maximally irreducible link over it — use find_actual_cause() or find_actual_effect():

transition.find_actual_cause((OR, AND))
CausalLink
α0.169925
DirectionCAUSE
Mechanism[OR,AND]
Purview[OR,AND]

Accounts#

The complete causal account of the transition — every irreducible cause and effect link — is computed with account():

account = actual.account(transition)
account
Account
Causal links5
Σα1.83007
Causal links
DirectionMechanismPurviewα
CAUSE[OR][OR]0.415037
CAUSE[AND][AND]0.415037
CAUSE[OR,AND][OR,AND]0.169925
EFFECT[OR][OR]0.415037
EFFECT[AND][AND]0.415037

These are the causal links shown in Figure 4. The account behaves like a sequence of links:

len(account)
5

Irreducible accounts#

Whether the account of the transition is itself irreducible — whether the transition, taken as a whole, is more than the sum of its parts — is evaluated with sia() (system irreducibility analysis):

sia = actual.sia(transition)
round(sia.alpha, 6)
0.169925

As shown in Figure 4, the second-order occurrence \(\{OR, AND = 10\}\) is destroyed by the minimum information partition, so the partitioned account has only the four first-order links:

sia.partitioned_account
Account
Causal links4
Σα1.66015
Causal links
DirectionMechanismPurviewα
CAUSE[OR][OR]0.415037
CAUSE[AND][AND]0.415037
EFFECT[OR][OR]0.415037
EFFECT[AND][AND]0.415037

The partition that achieves this is available on the result:

sia.partition
DirectedJointPartition · cause
DirectedJointPartition CAUSE
Severed connections
Connections cut2
ORAND
OR·✕
AND✕·

The causal nexus#

The analysis so far fixed one transition of interest. To search over all possible transitions between the observed states and keep the irreducible ones, use nexus(). Each result is a system irreducibility analysis whose cause and effect units identify the transition it describes:

labels = substrate.node_labels
for candidate in actual.nexus(substrate, X_state, Y_state):
    cause = ",".join(labels[i] for i in candidate.cause_indices)
    effect = ",".join(labels[i] for i in candidate.effect_indices)
    print(f"[{cause}] -> [{effect}]   big_alpha = {round(candidate.alpha, 6)}")
[OR] -> [OR]   big_alpha = 2.0
[AND] -> [AND]   big_alpha = 2.0
[OR,AND] -> [OR,AND]   big_alpha = 0.169925

causal_nexus() returns the single maximally irreducible transition among these:

cn = actual.causal_nexus(substrate, X_state, Y_state)
round(cn.alpha, 6)
2.0
cause = ",".join(labels[i] for i in cn.cause_indices)
effect = ",".join(labels[i] for i in cn.effect_indices)
print(f"[{cause}] -> [{effect}]")
[OR] -> [OR]

The single-unit transition \(\{OR\} \rightarrow \{OR\}\) is the causal nexus of this observed state change.

Where to go next#