Source code for dit.multivariate.synergistic_disclosure

"""
Scalar functions for synergistic disclosure.

Rosas, Mediano, Rassouli & Barrett (2020), "An operational information
decomposition via synergistic disclosure", J. Phys. A: Math. Theor. 53 485001.
https://doi.org/10.1088/1751-8121/abb723

Provides standalone functions for computing:
- S_alpha(X -> Y) for a single constraint set alpha
- The backbone decomposition {B^m_partial for m=1..n}
- Self-synergy S_alpha(X -> X)
"""

from ..utils import flatten, unitful

__all__ = (
    "backbone_disclosure",
    "modified_synergistic_disclosure",
    "self_synergy",
    "synergistic_disclosure",
)


[docs] @unitful def synergistic_disclosure(dist, sources, target, alpha, niter=None, bound=None): """ Compute S_alpha(X -> Y) for a single constraint set alpha. This is the maximum I(V; Y) over all alpha-synergistic channels, i.e. channels p_{V|X} satisfying I(V; X_{alpha_i}) = 0 for each subgroup alpha_i in alpha. Parameters ---------- dist : Distribution The joint distribution over sources and target. sources : list of lists Each inner list gives the indices of one source variable group. target : list The indices of the target variable. alpha : list of lists Each inner list gives source-list indices (0-based) specifying which sources form each constraint subgroup. niter : int, None Number of basin-hopping restarts. bound : int, None Cardinality bound on V. Returns ------- s_alpha : float The synergistic disclosure, in bits. """ from ..pid.syndisc import SyndiscOptimizer alpha_tuples = tuple(tuple(a) for a in alpha) if not alpha_tuples: from .coinformation import coinformation return coinformation(dist, [list(flatten(sources)), list(target)]) flat_sources = [list(s) for s in sources] try: opt = SyndiscOptimizer( dist, flat_sources, list(target), alpha_tuples, bound=bound, ) opt.optimize(niter=niter) val = opt.synergistic_disclosure(opt._optima) return max(val, 0.0) except Exception: return 0.0
[docs] def backbone_disclosure(dist, sources=None, target=None, niter=None, bound=None): """ Compute the full backbone decomposition of I(X; Y). The backbone decomposes I(X;Y) = sum_{m=1}^{n} B^m_partial, where each B^m_partial >= 0 measures the marginal gain from relaxing constraints on groups of m variables to groups of m-1. Parameters ---------- dist : Distribution The joint distribution over sources and target. sources : iter of iters, None The source variable groups. If None, all but last are used. target : iter, None The target variable. If None, the last variable is used. niter : int, None Number of basin-hopping restarts per lattice node. bound : int, None Cardinality bound on V. Returns ------- backbone : dict Keys are integers m=1..n, values are B^m_partial (non-negative backbone atoms). """ from ..pid.syndisc import SynDisc sd = SynDisc(dist, sources=sources, target=target, niter=niter, bound=bound) n = len(sd._sources) return {m: sd.get_backbone_atom(m) for m in range(1, n + 1)}
@unitful def modified_synergistic_disclosure(dist, sources, target, alpha, niter=None, bound=None): """ Compute the modified synergistic disclosure for a single constraint alpha. For singleton alpha (|alpha| = 1), returns I(T : a^C | a) instead of the optimization-based S_alpha. For |alpha| >= 2, falls back to the standard synergistic disclosure. Gutknecht, Makkeh & Wibral (2023), "From Babel to Boole: The Logical Organization of Information Decompositions", arXiv:2306.00734v2, eq. 52. Parameters ---------- dist : Distribution The joint distribution over sources and target. sources : list of lists Each inner list gives the indices of one source variable group. target : list The indices of the target variable. alpha : list of lists Each inner list gives source-list indices (0-based) specifying which sources form each constraint subgroup. niter : int, None Number of basin-hopping restarts (used only for |alpha| >= 2). bound : int, None Cardinality bound on V (used only for |alpha| >= 2). Returns ------- s_msd : float The modified synergistic disclosure, in bits. """ alpha_tuples = tuple(tuple(a) for a in alpha) if not alpha_tuples: from .coinformation import coinformation return coinformation(dist, [list(flatten(sources)), list(target)]) if len(alpha_tuples) == 1: from .coinformation import coinformation constrained_vars = list(flatten(sources[i] for i in alpha_tuples[0])) all_source_vars = list(flatten(sources)) total = coinformation(dist, [all_source_vars, list(target)]) source_mi = coinformation(dist, [constrained_vars, list(target)]) return total - source_mi return synergistic_disclosure( dist, sources, target, alpha, niter=niter, bound=bound, )
[docs] @unitful def self_synergy(dist, sources=None, alpha=None, niter=None, bound=None): """ Compute S_alpha(X -> X), the self-synergy of sources X. When alpha is None, uses the full individual-source constraints alpha = {{0}, {1}, ..., {n-1}}. Parameters ---------- dist : Distribution The joint distribution over sources. sources : list of lists, None The source variable groups. If None, each variable is its own group. alpha : list of lists, None Constraint subgroups (source-list indices). If None, each individual source is constrained. niter : int, None Number of basin-hopping restarts. bound : int, None Cardinality bound on V. Returns ------- s_self : float The self-synergy, in bits. """ if sources is None: sources = dist.rvs flat_sources = [list(s) for s in sources] target = list(flatten(flat_sources)) from ..pid.syndisc import SyndiscOptimizer alpha = tuple((i,) for i in range(len(flat_sources))) if alpha is None else tuple(tuple(a) for a in alpha) opt = SyndiscOptimizer( dist, flat_sources, target, alpha, bound=bound, ) opt.optimize(niter=niter) val = opt.synergistic_disclosure(opt._optima) return max(val, 0.0)