Source code for pyphi.timescale
# timescale.py
"""Functions for manipulating the timescale of TPMs."""
import numpy as np
from scipy.sparse import csc_matrix
from . import convert
from . import exceptions
[docs]
def sparse(matrix, threshold=0.1):
"""Return whether ``matrix`` is sparse enough to benefit from a
scipy sparse matrix power: at most ``threshold`` of its entries are
nonzero."""
return np.sum(matrix > 0) / matrix.size <= threshold
[docs]
def sparse_time(tpm, time_scale):
sparse_tpm = csc_matrix(tpm)
return (sparse_tpm**time_scale).toarray()
[docs]
def dense_time(tpm, time_scale):
return np.linalg.matrix_power(tpm, time_scale)
[docs]
def run_tpm(tpm, time_scale):
"""Iterate a TPM by the specified number of time steps.
Parameters
----------
tpm : np.ndarray
A state-by-node TPM.
time_scale : int
The number of steps to run the TPM.
Returns
-------
np.ndarray
The state-by-node TPM advanced by ``time_scale`` steps.
Raises
------
pyphi.exceptions.ConditionallyDependentError
If the iterated dynamics are not conditionally independent, so
no state-by-node TPM can represent them. Iterating typically
introduces conditional dependence between nodes that share inputs,
even though the single-step TPM is conditionally independent by
construction.
Notes
-----
The TPM is converted to state-by-state form and raised to the
``time_scale`` power there, then converted back. A scipy sparse matrix
power (:func:`sparse_time`) is used when the :func:`sparse` heuristic,
evaluated on the state-by-state matrix actually being raised to a power,
returns true — i.e. when at most 10% of its entries are nonzero — and a
dense power (:func:`dense_time`) otherwise.
"""
sbs_tpm = convert.state_by_node2state_by_state(tpm)
if sparse(sbs_tpm):
iterated = sparse_time(sbs_tpm, time_scale)
else:
iterated = dense_time(sbs_tpm, time_scale)
sbn_tpm = convert.state_by_state2state_by_node(iterated)
# Converting back to state-by-node form assumes conditional independence;
# verify the round trip so conditional dependence introduced by iteration
# is never silently discarded.
if not np.allclose(convert.state_by_node2state_by_state(sbn_tpm), iterated):
raise exceptions.ConditionallyDependentError(
f"the TPM iterated by {time_scale} steps is not conditionally "
"independent, so it cannot be expressed in state-by-node form. "
"Use the state-by-state form instead: "
"dense_time(convert.state_by_node2state_by_state(tpm), time_scale)."
)
return sbn_tpm
[docs]
def run_cm(cm, time_scale):
"""Iterate a connectivity matrix the specified number of steps.
Raising the connectivity matrix to the ``time_scale`` power counts directed
paths of that length between nodes; every nonzero entry is then clipped back
to 1, so the result marks which node pairs are connected by a path of
``time_scale`` steps.
Parameters
----------
cm : np.ndarray
A connectivity matrix.
time_scale : int
The number of steps to run.
Returns
-------
np.ndarray
The connectivity matrix at the new timescale.
Notes
-----
``np.linalg.matrix_power`` returns its input unchanged (not a copy) when
``time_scale == 1``, so a copy is made up front: without it, clipping
values back to 1 would mutate the caller's matrix in place and raise on
a read-only one.
"""
cm = np.array(cm, copy=True)
cm = np.linalg.matrix_power(cm, time_scale)
# Round non-unitary values back to 1
cm[cm > 1] = 1
return cm