Partial Information Decomposition & continuous mutual-information estimators in safe Rust.
pid-rs implements the shared-exclusions partial information decomposition (I^sx_∩;
Makkeh–Gutknecht–Wibral 2021) and the continuous k-nearest-neighbour estimators it builds on —
KSG mutual information (Kraskov et al. 2004) and the continuous I^sx_∩ estimator (Ehrlich et al.
2024) — together with a Shannon-invariant screening layer, discrete I_min PID, geometry
diagnostics, and dependence-aware uncertainty quantification.
It was built to diagnose how information from different sources (e.g. vision and language)
is integrated in multimodal policies, but every estimator here is domain-agnostic: give it
samples of sources S1, S2, … and a target T and it estimates how much of the information about
T is redundant, unique, or synergistic.
I(S1,S2; T)
┌──────┴───────┐
Redundancy · Unique(S1) · Unique(S2) · Synergy
- KSG mutual information for continuous variables — L∞ joint metric, strict-radius marginal counting, digamma reference table.
- Continuous
I^sx_∩(shared-exclusions redundancy) via the Ehrlich et al. 2024 disjunction-neighbourhood kNN estimator — not a min-of-pointwise heuristic. - 2- and 3-source PID atoms whose Möbius identities (
Red + Unq₁ + Unq₂ + Syn = I(S1,S2;T)) hold by construction and are asserted in tests. - Discrete
I_minPID over the full 18-antichain 3-source lattice (Williams & Beer 2010). - Discrete shared-exclusions PID
i^sx_∩(Makkeh–Gutknecht–Wibral 2021) — pointwise and averaged signed atoms with informative/misinformative split, bit-faithful to the reference IDTxl wraps (Abzinger/SxPID) for 2- and 3-source, plus a generaldiscrete_sxpid_nfor 2–4 sources (the 166-antichain 4-source lattice). The discrete counterpart of the continuousI^sx_∩, so the library decomposes information with one measure across regimes. - Shannon invariants — co-information, O-information (Rosas et al. 2019), and the average
degrees of redundancy (
r̄) and vulnerability (v̄) (Gutknecht et al. 2025) — as cheap screening statistics. - Geometry diagnostics — intrinsic dimension (Levina–Bickel), distance concentration, Gromov hyperbolicity — to decide whether a continuous-kNN regime is even valid.
- Preprocessing — standardisation, PCA, hash projection, seeded jitter, and PLS.
- Honest uncertainty — block bootstrap and permutation tests that respect sample dependence.
- Reproducible by construction — content-addressed run-logs (
pid-runlog; SHA-256 payload digests on action/intervention/bridge records + a whole-trace replay hash and whole-file manifest covering every record), seeded RNG, and an optionalparallelfeature whose results are bit-identical to the serial path. - The estimator core (
pid-core) is#![forbid(unsafe_code)], returns errors rather thanpanic!-ing on valid-but-degenerate input, and keeps a dependency-light tree. (pid-runlogis also unsafe-free;pid-pythonnecessarily uses PyO3'sunsafeinternals.)
A high-level orientation for readers who already know the established toolboxes (not a feature-parity scorecard — each tool leads in its own niche):
| pid-rs | IDTxl | dit | JIDT | |
|---|---|---|---|---|
| Language | Rust (+ Python) | Python | Python | Java (+ wrappers) |
| KSG continuous MI | ✅ | ✅ | — (discrete-only) | ✅ |
Continuous I^sx_∩ (Ehrlich 2024) |
✅ | — | — | — |
Discrete SxPID i^sx_∩ |
✅ (bit-faithful to IDTxl) | ✅ (reference impl.) | ✅ (PID_SX) |
— |
Discrete I_min PID |
✅ | — (BROJA + SxPID only) | ✅ | — |
| Broad discrete PID/measure zoo | — | some | ✅ | — |
| Transfer entropy / network inference | — | ✅ | — | ✅ |
| Content-addressed, replayable run-logs | ✅ | — | — | — |
Memory-safe, unsafe-free core |
✅ | n/a | n/a | n/a |
| Bit-identical serial↔parallel results | ✅ | — | — | — |
Where the others lead: IDTxl for transfer entropy, full network inference, and its mature ecosystem (and it is the reference SxPID implementation this crate is validated against); dit for the sheer breadth of discrete information/PID measures; JIDT for its established JVM estimator suite. pid-rs's niche is a fast, memory-safe, reproducible implementation of the Wibral-group shared-exclusions PID unified across the continuous and discrete regimes.
pid-rs is at 0.4.0. The estimator core is validated against analytic ground truth (see
Validation); the surrounding statistics, performance, and tooling layers are usable
but have tracked follow-ups. This section is a quick honest map of where things stand — it does not
repeat the per-claim detail in Conventions,
Scientific cautions, or
Known limitations.
| Capability | Notes |
|---|---|
| KSG mutual information | Continuous variables, L∞ joint metric, strict-radius marginal counting; checked vs the closed-form Gaussian-channel MI. |
Continuous I^sx_∩ |
Ehrlich et al. 2024 disjunction-neighbourhood kNN redundancy (IsxMethod::EhrlichKsg); checked against a closed-form additive-Gaussian oracle (tests/sxpid_gaussian_oracle.rs) and pinned by frozen fixed-data regression values. |
| 2- & 3-source PID atoms | pid2_isx / pid3_isx; Möbius identities (Red + Unq₁ + Unq₂ + Syn = I(S1,S2;T)) hold by construction and are asserted in tests within 1e-10. |
Discrete I_min PID |
discrete_pid2 / discrete_pid3 over the full 18-antichain 3-source lattice (Williams & Beer 2010), with equal-width quantisation. |
Discrete SxPID i^sx_∩ |
discrete_sxpid2 / discrete_sxpid3 + general discrete_sxpid_n (2–4 sources; Makkeh–Gutknecht–Wibral 2021); pointwise + averaged signed atoms with informative/misinformative split, bit-faithful to the Abzinger/SxPID + IDTxl reference values to 1e-12; MGW Theorem IV.2/IV.3 axiom tests. |
| Shannon invariants | Co-information, O-information (Rosas et al. 2019), and r̄/v̄ (Gutknecht et al. 2025) as cheap screening statistics. |
| Geometry diagnostics | Intrinsic dimension (Levina–Bickel), distance concentration, Gromov hyperbolicity — to decide whether a continuous-kNN regime is even valid. |
| Preprocessing / PLS | Standardisation, PCA, hash (CountSketch) projection, seeded jitter, and supervised PLS with CV component selection. |
| Uncertainty quantification | Moving-block bootstrap and permutation tests that respect sample dependence. |
| Run-logs | pid-runlog: versioned, content-addressed JSONL schema (SHA-256 payload hashes on action/intervention/bridge records; every record covered by the whole-trace replay hash + whole-file manifest) and replay/validate/compare/sidecar CLIs. |
| Python bindings | pid_core_rs (PyO3 + maturin, abi3 ≥ CPython 3.11) — 18 functions over C-contiguous float64 NumPy arrays. Bindings shipped in 0.2.0; discrete SxPID exports added in 0.3.0. |
| Reproducibility | Seeded RNG; the optional parallel feature is bit-identical to the serial path; #![forbid(unsafe_code)]; errors (not panics) on degenerate input. |
- kNN is brute-force
O(n²).kth_neighbor_distance_*andcount_neighbors_withinscan all pairs per query — there is no kd-tree / approximate-NN backend, so largenis slow. - No multiple-comparison correction. Many atoms × sources × windows report raw per-atom p-values; apply your own FDR/FWER control.
runlog --validateis per-record, not whole-trace integrity. It checks per-event invariants (payload/config-hash matches, monotone timestamps/steps, singlerun_started/run_ended, bridge causality, finite values). Whole-trace integrity is a separate path: the order-sensitivereplay_trace_hash(--compare) and--verify-sidecars.exp0is a diagnostic gate, not a pass/fail build step. It emits a GO/PIVOT/NO-GO verdict from monotonicity / invariant / geometry counters and exits 0 by default (its default sweep deliberately enters regimes where kNN MI is known to break down).--strict-gatedoes not gate that default sweep; it enforcesGO(exit 3 otherwise) only on a curated, analytically grounded low-dimension band (see theexp0section below).- No crates.io release yet. Depend on the Git repository; the Python crate is
publish = falseby design (shipped as a wheel via maturin). - External cross-validation of the continuous
I^sx_∩pending. The discrete SxPID is validated bit-faithfully against IDTxl/Abzinger reference values, and the continuous estimator against a closed-form Gaussian oracle — but no reproducible externalcsxpidcross-check exists yet: the fixed-data constants intests/isx.rs/tests/pid3.rsare frozen regression pins of this implementation (their historical csxpid attribution left no dataset or invocation artifacts, so they must not be read as external validation).
- kNN failure modes are real. Estimators assume i.i.d. samples (trajectory autocorrelation
biases them — subsample or block-bootstrap); high ambient/intrinsic dimension causes distance
concentration that degrades kNN geometry; and strong (near-deterministic) dependence can
require prohibitive sample sizes (Gao et al. 2015). Run the geometry diagnostics and the
exp0gate before interpreting results. - Negative atoms are real, not bugs.
I^sx_∩trades all-atom non-negativity for the target chain rule, so atoms (including redundancy) can be negative; atoms and the MI terms feeding them are never clamped (Allowis forced insidepid2_isx/pid3_isx/co-information). Note the standaloneksg_midefaults toClampToZeroas a reporting convenience — setNegativeHandling::Allowwhen you need raw (possibly negative) MI estimates. - Cross-estimator PID2 mixing. In
pid2_isx,Unq/Syncombine KSG MI with EhrlichI^sxredundancy (different bias profiles), so small near-zero atoms can be an estimator artefact rather than structure. Likewise, do not pool continuousI^sx_∩atoms with discreteI_minatoms — they are different PID measures.
[dependencies]
pid-core = { git = "https://github.com/sepahead/pid-rs" }A crates.io release is planned; until then, depend on the Git repository.
Using Python? See the Python bindings below for
pip install maturinandmaturin develop.
use pid_core::{ksg_mi, pid2_isx, IsxConfig, KsgConfig, MatRef, NegativeHandling, Pid2Config};
// Columns are dimensions, rows are samples. Here: scalar S1, S2, T (n samples each).
// (s1_data/s2_data/t_data/n are your own `&[f64]` buffers; see examples/ksg_and_pid.rs for a runnable version.)
let s1 = MatRef::new(&s1_data, n, 1)?;
let s2 = MatRef::new(&s2_data, n, 1)?;
let t = MatRef::new(&t_data, n, 1)?;
// Mutual information (nats).
let ksg = KsgConfig { negative_handling: NegativeHandling::Allow, ..Default::default() };
let mi = ksg_mi(s1, t, &ksg)?;
// 2-source PID atoms via I^sx_∩.
let pid = pid2_isx(s1, s2, t, &Pid2Config { ksg, isx: IsxConfig::default() })?;
println!("Red={:.3} Unq1={:.3} Unq2={:.3} Syn={:.3}",
pid.redundancy, pid.unique_s1, pid.unique_s2, pid.synergy);
# Ok::<(), pid_core::PidError>(())Run the worked examples end-to-end:
cargo run --release --example ksg_and_pid # continuous KSG MI + I^sx_∩ PID
cargo run --release --example discrete_sxpid # discrete shared-exclusions PID on logic gatesA Criterion benchmark suite tracks the cost of the
brute-force O(n²) kNN backend and the discrete SxPID lattice across sample sizes (KSG MI, continuous
I^sx_∩, 2-source PID, discrete SxPID):
cargo bench -p pid-core(Common dev tasks are also codified as just recipes — run just
to list them.)
- Units: all information quantities are in nats (natural log).
- Co-information sign: for 2 sources
CI₂ = Red − Syn, so negative ⇒ synergy-dominant. This does not carry over to 3 sources —CI₃is parity-flipped (a pure 3-way synergy givesCI₃ > 0) and conflates atoms, so it is only a coarse screen. - Negative atoms are real:
I^sx_∩trades all-atom non-negativity for the target chain rule, so atoms (including redundancy) can be negative. Atoms and the MI terms feeding them are never clamped (NegativeHandling::Allowis forced inside the PID/co-information paths); only the standaloneksg_midefaults toClampToZero, and that is a reporting choice you can override.
kNN information estimators are powerful but have well-known failure modes. Validate before you interpret.
- i.i.d. assumption — trajectory/time-series autocorrelation biases kNN MI. Subsample or use the block bootstrap.
- Distance concentration — in high ambient/intrinsic dimension, kNN geometry degrades; check the geometry diagnostics first.
- Strong dependence — near-deterministic relationships (very large true MI) can need prohibitive sample sizes (Gao et al. 2015).
- Estimator ≠ truth — do not interpret a downstream result without passing a validation gate on synthetic systems whose information quantities are known analytically.
The exp0 binary is that diagnostic gate (synthetic systems with known MI, noise-dimension
invariance, strong-dependence sweeps). It sweeps dimensions up to 256 at n=500 — a range that
deliberately includes regimes where kNN MI is known to break down — so a PIVOT/NO-GO verdict on
the full default sweep is the expected, informative outcome, not a build failure. It reports
per-check counters (Monotonicity / Invariant / Geometry), and exits 0 by default.
--strict-gate is deliberately not allowed to gate that full sweep (a non-GO verdict there is
expected, not a failure). Instead it enforces GO — exiting with code 3 otherwise — on a curated
band where GO is legitimately expected and is checked against an analytic closed form: a
small grid of jointly-Gaussian systems at d=1, n=4000 (the KSG estimator's validated regime),
where the three mutual-information terms I(S1;T), I(S2;T), I(S1,S2;T) must match their
Cover–Thomas Gaussian values within the scale-aware tolerance. --strict-gate implies
--strict-band (run + report the band without enforcing). The four synthetic scenarios are still
exercised at d ∈ {2,4,8} as a non-gating diagnostic alongside the band.
cargo run -p pid-core --bin exp0 -- --seeds 4 --summary-json summary.json --runlog run.jsonl
cargo run -p pid-runlog --bin pid-runlog-replay -- --validate run.jsonlCorrectness is checked against analytically known ground truth, not just self-consistency:
- KSG MI vs the closed-form Gaussian-channel MI
I = −½ ln(1 − ρ²). - Continuous
I^sx_∩against a closed-form additive-Gaussian oracle (the analytic continuous limit of the discretei^sx;tests/sxpid_gaussian_oracle.rs), plus frozen fixed-data regression pins. - Discrete SxPID bit-faithfully against the IDTxl/Abzinger reference values (to
1e-12), including its axioms (MGW Theorems IV.2/IV.3 intests/sxpid_axioms.rs). - Discrete PID against the independently re-derived
I_minand the known structure of canonical gates (XOR = pure synergyln 2; a redundant copyS1 = S2= pure redundancy; AND's exact closed-form atoms; …). - 2-/3-source PID identities (atoms reconstruct total MI) within
1e-10. parallelfeature results are bit-identical to the serial path.
See crates/pid-core/tests for the suite.
This is a 0.4.0 release. The estimator core (KSG, continuous I^sx_∩, discrete I_min, and
the PID identities) is validated against analytic ground truth, but the surrounding
statistics/convenience layer has tracked follow-ups (see the issue tracker):
- No multiple-comparison correction. When testing many atoms × sources × windows, apply your own FDR/FWER control; the library reports raw per-atom p-values.
- Bootstrap/permutation caveats for kNN statistics. The block bootstraps are true
moving-block (Künsch) resamplers, but with-replacement resampling necessarily duplicates rows,
which distorts kNN local-density statistics even with tie-breaking jitter — prefer
RowResampleScheme::Subsamplefor KSG-based statistics.permutation_pid3's full-row shuffle is not a block-permutation null; for autocorrelated trajectory data construct a block-aware null instead. - Cross-estimator PID2 atoms.
Unq/Syncombine KSG MI with EhrlichI^sxredundancy (different bias profiles); small near-zero atoms can be an estimator artefact rather than structure. - External cross-validation provenance. The discrete SxPID is validated against the
IDTxl/Abzinger reference values; the continuous
I^sx_∩against a closed-form Gaussian oracle. A reproducible externalcsxpidcross-check of the continuous estimator (committed dataset + recorded invocation) is still pending; the fixed-data constants intests/isx.rs/tests/pid3.rsare frozen regression pins, not external validation.
None of these affects a single point estimate of MI or a PID atom — they concern uncertainty quantification and convenience-API ergonomics.
| Component | Reference |
|---|---|
| KSG mutual information | Kraskov, Stögbauer & Grassberger (2004), Phys. Rev. E 69, 066138 |
Shared-exclusions redundancy i^sx_∩ (discrete discrete_sxpid2/3) |
Makkeh, Gutknecht & Wibral (2021), Phys. Rev. E 103, 032149; reference impl. IDTxl pid_goettingen / Abzinger/SxPID |
| Parthood / formal-logic foundation of PID | Gutknecht, Wibral & Makkeh (2021), arXiv:2008.09535 |
Continuous I^sx_∩ kNN estimator |
Ehrlich, Schick-Poland, Makkeh, Lanfermann, Wollstadt & Wibral (2024), Phys. Rev. E 110, 014115 (arXiv:2311.06373) |
I_min redundancy & the PID lattice |
Williams & Beer (2010), arXiv:1004.2515 |
Shannon invariants (r̄, v̄) |
Gutknecht, Rosas, Ehrlich, Makkeh, Mediano & Wibral (2025), arXiv:2504.15779 |
| O-information | Rosas, Mediano, Gastpar & Jensen (2019), Phys. Rev. E 100, 032305 (arXiv:1902.11239) |
| PID non-negativity / chain-rule / invariance trilemma | Matthias, Makkeh, Wibral & Gutknecht (2025), arXiv:2512.16662 |
| kNN MI sample-complexity caveat | Gao, Ver Steeg & Galstyan (2015), arXiv:1411.2003 |
| Crate | Description |
|---|---|
pid-core |
The estimators, PID atoms, invariants, geometry, preprocessing, and the exp0 validation harness. |
pid-runlog |
Versioned, content-addressed run-log schema + replay/validation CLI for reproducible pipelines. |
pid-python |
Python bindings (PyO3 + maturin); the pid_core_rs module — 18 functions over NumPy arrays. |
The pid_core_rs bindings (added in 0.2.0) are built as a
stable-ABI (abi3, CPython ≥ 3.11) wheel with maturin. Arrays are passed as C-contiguous
float64 NumPy arrays (wrap transposed/order='F' arrays in np.ascontiguousarray first):
pip install maturin && maturin develop --release -m crates/pid-python/Cargo.toml
python -c "import numpy as np, pid_core_rs as p; print(p.compute_mi(np.random.randn(400,1), np.random.randn(400,1)))"1.80. The MSRV is treated as a semver-relevant property and is exercised in CI.
Contributions are welcome — see CONTRIBUTING.md and the Code of Conduct. For anything security-sensitive, see SECURITY.md.
If you use pid-rs in academic work, please cite it via CITATION.cff (GitHub
renders a “Cite this repository” button) and cite the underlying estimator papers above.
Licensed under either of
- MIT license (LICENSE-MIT), or
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
The I^sx_∩ measure and its continuous estimator are due to the Wibral group (Göttingen); this is
an independent, from-the-papers Rust implementation. Any errors are the maintainer's own.