feat(examples): gene, climate, ecosystem, quantum consciousness explorers

Four new IIT 4.0 analysis applications:

Gene Networks: 16-gene regulatory network with 4 modules.
  Cancer increases degeneracy 9x. Networks are perfectly decomposable.

Climate: 7 climate modes (ENSO, NAO, PDO, AMO, IOD, SAM, QBO).
  All modes independent (7/7 rank). IIT auto-discovers ENSO-IOD coupling.

Ecosystems: Rainforest vs monoculture vs coral reef food webs.
  Degeneracy predicts fragility: monoculture 1.10 vs rainforest 0.12.

Quantum: Bell, GHZ, Product, W states + random circuits.
  IIT Phi disagrees with entanglement. Emergence index tracks it better.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
rUv 2026-03-31 22:01:55 +00:00
parent 2eefef68bb
commit 11c72cfa7f
26 changed files with 4427 additions and 0 deletions

36
Cargo.lock generated
View file

@ -1293,6 +1293,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "climate-consciousness"
version = "0.1.0"
dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"ruvector-consciousness",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@ -2368,6 +2377,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9"
[[package]]
name = "ecosystem-consciousness"
version = "0.1.0"
dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"ruvector-consciousness",
]
[[package]]
name = "ed25519"
version = "2.2.3"
@ -3254,6 +3272,15 @@ dependencies = [
"seq-macro",
]
[[package]]
name = "gene-consciousness"
version = "0.1.0"
dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"ruvector-consciousness",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@ -7350,6 +7377,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "quantum-consciousness"
version = "0.1.0"
dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"ruvector-consciousness",
]
[[package]]
name = "quick-error"
version = "1.2.3"

View file

@ -143,6 +143,10 @@ members = [
"crates/ruvector-consciousness-wasm",
"examples/cmb-consciousness",
"examples/gw-consciousness",
"examples/ecosystem-consciousness",
"examples/quantum-consciousness",
"examples/gene-consciousness",
"examples/climate-consciousness",
]
resolver = "2"

View file

@ -0,0 +1,117 @@
# Climate Teleconnection Consciousness Analysis
## Overview
This example applies Integrated Information Theory (IIT) Phi to climate mode
interactions to study how large-scale climate oscillations form integrated
information systems. The key question is whether the climate system's
teleconnections create genuine integrated information, and whether El Nino
events increase this integration.
## Background
### Climate Teleconnections
Teleconnections are large-scale patterns of climate variability that link
weather and climate conditions across distant regions. The major climate
indices capture these patterns:
- **ENSO (El Nino Southern Oscillation)**: The dominant mode of interannual
climate variability, involving coupled ocean-atmosphere dynamics in the
tropical Pacific. Measured by the Nino3.4 index.
- **NAO (North Atlantic Oscillation)**: Pressure difference between the
Icelandic Low and Azores High, controlling weather patterns across the
North Atlantic and Europe.
- **PDO (Pacific Decadal Oscillation)**: Long-lived pattern of Pacific
climate variability, related to but distinct from ENSO on decadal scales.
- **AMO (Atlantic Multidecadal Oscillation)**: Basin-wide sea surface
temperature variations in the North Atlantic on 60-80 year timescales.
- **IOD (Indian Ocean Dipole)**: East-west temperature gradient in the
tropical Indian Ocean, strongly coupled to ENSO.
- **SAM (Southern Annular Mode)**: The dominant mode of extratropical
variability in the Southern Hemisphere, mostly independent.
- **QBO (Quasi-Biennial Oscillation)**: Alternating easterly/westerly winds
in the equatorial stratosphere with a ~28-month period.
### Known Teleconnection Strengths
The coupling between these modes varies:
- **Strong**: ENSO-IOD (r ~ 0.5-0.7), driven by Walker circulation coupling
- **Moderate**: ENSO-PDO (r ~ 0.3-0.5), Pacific basin shared dynamics
- **Moderate**: NAO-AMO (r ~ 0.2-0.4), Atlantic ocean-atmosphere coupling
- **Weak**: QBO-ENSO (r ~ 0.1-0.2), stratosphere-troposphere interaction
- **Minimal**: SAM is largely independent of tropical modes
### IIT and Climate Systems
Applying IIT to climate modes asks: does the climate system generate more
integrated information than the sum of its regional parts? Specifically:
- **Do teleconnections create integration?** If climate modes are truly
coupled (not just correlated), the system should have non-trivial Phi.
- **Does El Nino increase integration?** During El Nino events, ENSO
correlations strengthen by ~50%, potentially increasing system-wide Phi.
- **What are the irreducible subsystems?** The Pacific basin (ENSO, PDO, IOD)
is expected to be the most integrated regional subsystem.
## Method
### TPM Construction
1. Start with a 7x7 correlation matrix based on known teleconnection strengths.
2. Apply a sharpness exponent (alpha=2.0) to emphasize strong connections.
3. Row-normalize to get transition probabilities.
4. Generate El Nino variant by boosting ENSO correlations 50%.
### Analysis Pipeline
1. Compute Phi for the full 7-index system (neutral and El Nino).
2. Compute Phi for regional subsets: Pacific, Atlantic, Polar.
3. Compare neutral vs El Nino Phi.
4. Temporal analysis: simulate 12 monthly states with seasonal modulation.
5. Causal emergence: find optimal coarse-graining.
6. Null hypothesis testing with shuffled correlations.
### Seasonal Modulation
Climate modes have distinct seasonal cycles:
- **ENSO**: peaks in boreal winter (DJF), weakens in spring (MAM)
- **NAO**: strongest in winter, weakest in summer
- This creates a seasonal Phi cycle that should peak in winter when
teleconnections are strongest.
## Expected Results
- The Pacific basin should have the highest regional Phi due to strong
ENSO-IOD-PDO coupling.
- El Nino should increase full-system Phi by strengthening teleconnections.
- Monthly Phi should peak in DJF (boreal winter) when both ENSO and NAO
teleconnections are strongest.
- The null model (shuffled correlations) should have different Phi,
confirming that the specific teleconnection structure matters.
- SAM should contribute least to integration (mostly independent).
## Implications
If climate modes show genuine integrated information, it suggests that:
1. The climate system has emergent properties beyond its components.
2. Teleconnections are not merely correlations but genuine causal coupling.
3. El Nino events fundamentally change the system's information architecture.
4. The Pacific basin acts as the "core" of global climate integration.
## References
1. Tononi G. (2004). An information integration theory of consciousness. BMC Neurosci.
2. Hoel E.P. et al. (2013). Quantifying causal emergence. PNAS.
3. Trenberth K.E. et al. (1998). Progress during TOGA in understanding and modeling
global teleconnections. J. Geophys. Res.
4. McPhaden M.J. et al. (2006). ENSO as an integrating concept in earth science. Science.
5. Saji N.H. et al. (1999). A dipole mode in the tropical Indian Ocean. Nature.

View file

@ -0,0 +1,94 @@
# Gene Regulatory Network Consciousness Analysis
## Overview
This example applies Integrated Information Theory (IIT) Phi to gene regulatory
networks to identify emergent regulatory modules. The key hypothesis is that
functional gene modules -- groups of genes that work together as a unit -- should
correspond to subsystems with high integrated information (Phi), making them the
"irreducible units" of the regulatory network.
## Background
### Gene Regulatory Networks
Gene regulatory networks (GRNs) describe how genes control each other's
expression through activation and repression. These networks exhibit modular
structure, where groups of genes form functional units:
- **Cell cycle module**: Cyclins (CycD, CycE) and CDKs (CDK4, CDK2) form a
cascade that drives cell division.
- **Apoptosis module**: Pro-apoptotic (BAX, CASP3) and anti-apoptotic (BCL2)
factors balance cell death decisions, regulated by p53.
- **Growth signaling module**: The EGFR-RAS-RAF-ERK cascade transmits external
growth signals to the nucleus.
- **Housekeeping module**: Constitutively expressed genes (GAPDH, ACTB, etc.)
that maintain basic cellular functions.
### IIT and Gene Networks
IIT's Phi measures how much a system is "more than the sum of its parts" --
i.e., how much information is generated by the system as a whole beyond what
its parts generate independently. For gene networks:
- **High intra-module Phi**: Genes within a functional module should have high
integrated information because they form a tightly coupled regulatory unit.
- **Low inter-module Phi**: The full network may have lower Phi than individual
modules because the weak between-module connections don't contribute much
integration.
- **Modules as irreducible units**: The IIT prediction is that functional
modules should be the "complexes" -- the maximally irreducible subsystems.
## Cancer Rewiring Hypothesis
In cancer, oncogenic mutations rewire the regulatory network:
1. Growth signaling becomes constitutively active (e.g., RAS mutations).
2. Apoptosis is suppressed (e.g., BCL2 overexpression).
3. p53 pathway is disrupted (e.g., TP53 mutations).
4. Cell cycle checkpoints are bypassed.
These changes create strong cross-module connections where growth signaling
overrides apoptosis controls. The IIT prediction is that this should:
- **Increase full-network Phi**: More cross-module integration.
- **Blur module boundaries**: The network becomes less modular.
- **Change emergence landscape**: Different coarse-graining is optimal.
## Method
### TPM Construction
1. Start with a 16-gene adjacency matrix with known module structure.
2. Within-module connections: strong (0.3-0.5 transition probability).
3. Between-module connections: weak (0.01-0.05).
4. Convert to TPM by taking absolute values and row-normalizing.
5. Self-regulation baseline (0.1 on diagonal) ensures stability.
### Analysis Pipeline
1. Compute Phi for the full 16-gene network (normal and cancer variants).
2. Compute Phi for each 4-gene module in isolation.
3. Compare module Phi vs full network Phi.
4. Compute causal emergence (optimal coarse-graining).
5. Null hypothesis testing with degree-preserving shuffled networks.
## Expected Results
- Individual modules should have higher Phi than the full network, confirming
they are the irreducible regulatory units.
- Cancer rewiring should increase full-network Phi by creating stronger
cross-module coupling.
- Causal emergence should reveal that the optimal coarse-graining corresponds
approximately to the known module boundaries in the normal network.
- The null model (shuffled connections) should have different Phi, confirming
that the modular structure is informationally significant.
## References
1. Tononi G. (2004). An information integration theory of consciousness. BMC Neurosci.
2. Hoel E.P. et al. (2013). Quantifying causal emergence shows that macro can beat micro. PNAS.
3. Alon U. (2007). Network motifs: theory and experimental approaches. Nat Rev Genet.
4. Barabasi A.L., Oltvai Z.N. (2004). Network biology: understanding the cell's
functional organization. Nat Rev Genet.
5. Hanahan D., Weinberg R.A. (2011). Hallmarks of cancer: the next generation. Cell.

View file

@ -0,0 +1,16 @@
[package]
name = "climate-consciousness"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "Climate teleconnection consciousness analysis using IIT Phi"
publish = false
[[bin]]
name = "climate-consciousness"
path = "src/main.rs"
[dependencies]
ruvector-consciousness = { path = "../../crates/ruvector-consciousness", default-features = false, features = ["phi", "emergence", "collapse"] }
rand = "0.8"
rand_chacha = "0.3"

View file

@ -0,0 +1,250 @@
//! Consciousness analysis pipeline for climate teleconnections.
use ruvector_consciousness::emergence::CausalEmergenceEngine;
use ruvector_consciousness::phi::auto_compute_phi;
use ruvector_consciousness::rsvd_emergence::{RsvdEmergenceEngine, RsvdEmergenceResult};
use ruvector_consciousness::traits::EmergenceEngine;
use ruvector_consciousness::types::{
ComputeBudget, EmergenceResult, PhiResult,
TransitionMatrix as ConsciousnessTPM,
};
use crate::data::{self, ClimateCorrelations, TransitionMatrix};
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Full analysis results for climate teleconnections.
pub struct AnalysisResults {
/// Phi for the full 7-index neutral system.
pub neutral_full_phi: PhiResult,
/// Phi for the full 7-index El Nino active system.
pub elnino_full_phi: PhiResult,
/// Regional subset Phi values (neutral).
pub neutral_regional_phis: Vec<(String, PhiResult)>,
/// Regional subset Phi values (El Nino).
pub elnino_regional_phis: Vec<(String, PhiResult)>,
/// Causal emergence for neutral system.
pub neutral_emergence: EmergenceResult,
/// SVD emergence for neutral system.
pub neutral_svd_emergence: RsvdEmergenceResult,
/// Whether El Nino increases full-system Phi.
pub elnino_increases_phi: bool,
/// Whether the Pacific basin is the most integrated subsystem.
pub pacific_most_integrated: bool,
/// Monthly Phi values (seasonal cycle).
pub monthly_phis: Vec<(String, f64)>,
/// Null model Phi values.
pub null_phis: Vec<f64>,
/// Z-score of observed Phi vs null.
pub z_score: f64,
/// Empirical p-value.
pub p_value: f64,
}
/// Convert our TPM to the consciousness crate's format.
fn to_consciousness_tpm(tpm: &TransitionMatrix) -> ConsciousnessTPM {
ConsciousnessTPM::new(tpm.size, tpm.data.clone())
}
/// Run the complete analysis pipeline.
pub fn run_analysis(
neutral_data: &ClimateCorrelations,
neutral_tpm: &TransitionMatrix,
_elnino_data: &ClimateCorrelations,
elnino_tpm: &TransitionMatrix,
null_samples: usize,
) -> AnalysisResults {
let budget = ComputeBudget::default();
// 1. Full system Phi -- neutral
println!("\n--- Computing Phi: Neutral Climate System (7 indices) ---");
let neutral_ctpm = to_consciousness_tpm(neutral_tpm);
let neutral_full_phi = auto_compute_phi(&neutral_ctpm, None, &budget)
.expect("Failed to compute Phi for neutral system");
println!(
" Phi = {:.6} (algorithm: {}, elapsed: {:?})",
neutral_full_phi.phi, neutral_full_phi.algorithm, neutral_full_phi.elapsed
);
// 2. Full system Phi -- El Nino active
println!("\n--- Computing Phi: El Nino Active (7 indices) ---");
let elnino_ctpm = to_consciousness_tpm(elnino_tpm);
let elnino_full_phi = auto_compute_phi(&elnino_ctpm, None, &budget)
.expect("Failed to compute Phi for El Nino system");
println!(
" Phi = {:.6} (algorithm: {}, elapsed: {:?})",
elnino_full_phi.phi, elnino_full_phi.algorithm, elnino_full_phi.elapsed
);
// 3. Regional Phi -- neutral
println!("\n--- Computing Phi: Neutral Regional Subsets ---");
let regions = data::all_regions();
let mut neutral_regional_phis = Vec::new();
for (name, indices) in &regions {
if indices.len() >= 2 {
let sub = data::extract_sub_tpm(neutral_tpm, indices);
let sub_ctpm = to_consciousness_tpm(&sub);
match auto_compute_phi(&sub_ctpm, None, &budget) {
Ok(phi) => {
let idx_names: Vec<&str> = indices.iter().map(|&i| data::INDEX_NAMES[i]).collect();
println!(" {} Phi = {:.6} ({})", name, phi.phi, idx_names.join(", "));
neutral_regional_phis.push((name.to_string(), phi));
}
Err(e) => {
println!(" {} Phi computation failed: {}", name, e);
}
}
}
}
// 4. Regional Phi -- El Nino
println!("\n--- Computing Phi: El Nino Regional Subsets ---");
let mut elnino_regional_phis = Vec::new();
for (name, indices) in &regions {
if indices.len() >= 2 {
let sub = data::extract_sub_tpm(elnino_tpm, indices);
let sub_ctpm = to_consciousness_tpm(&sub);
match auto_compute_phi(&sub_ctpm, None, &budget) {
Ok(phi) => {
println!(" {} Phi = {:.6}", name, phi.phi);
elnino_regional_phis.push((name.to_string(), phi));
}
Err(e) => {
println!(" {} Phi computation failed: {}", name, e);
}
}
}
}
// 5. Compare neutral vs El Nino
let elnino_increases_phi = elnino_full_phi.phi > neutral_full_phi.phi;
println!(
"\n El Nino Phi ({:.6}) {} Neutral Phi ({:.6})",
elnino_full_phi.phi,
if elnino_increases_phi { ">" } else { "<=" },
neutral_full_phi.phi
);
// 6. Identify most integrated region
let pacific_phi = neutral_regional_phis
.iter()
.find(|(n, _)| n == "Pacific")
.map(|(_, p)| p.phi)
.unwrap_or(0.0);
let max_regional_phi = neutral_regional_phis
.iter()
.map(|(_, p)| p.phi)
.fold(0.0f64, f64::max);
let pacific_most_integrated = (pacific_phi - max_regional_phi).abs() < 1e-10
&& pacific_phi > 0.0;
// 7. Causal emergence
println!("\n--- Causal Emergence Analysis (Neutral) ---");
let emergence_engine = CausalEmergenceEngine::new(neutral_tpm.size.min(16));
let neutral_emergence = emergence_engine
.compute_emergence(&neutral_ctpm, &budget)
.expect("Failed to compute causal emergence");
println!(
" EI_micro = {:.4} bits, determinism = {:.4}, degeneracy = {:.4}",
neutral_emergence.ei_micro, neutral_emergence.determinism, neutral_emergence.degeneracy
);
println!(
" Causal emergence = {:.4}, coarse-graining: {:?}",
neutral_emergence.causal_emergence, neutral_emergence.coarse_graining
);
// 8. SVD emergence
println!("\n--- SVD Emergence Analysis (Neutral) ---");
let svd_engine = RsvdEmergenceEngine::default();
let neutral_svd_emergence = svd_engine
.compute(&neutral_ctpm, &budget)
.expect("Failed to compute SVD emergence");
println!(
" Effective rank = {}/{}, entropy = {:.4}, emergence = {:.4}",
neutral_svd_emergence.effective_rank, neutral_tpm.size,
neutral_svd_emergence.spectral_entropy, neutral_svd_emergence.emergence_index
);
// 9. Temporal analysis: monthly seasonal cycle
println!("\n--- Temporal Analysis: Seasonal Phi Cycle ---");
let monthly_tpms = data::generate_monthly_tpms(neutral_data);
let mut monthly_phis = Vec::new();
for (month, tpm) in &monthly_tpms {
let ctpm = to_consciousness_tpm(tpm);
match auto_compute_phi(&ctpm, None, &budget) {
Ok(phi) => {
println!(" {} Phi = {:.6}", month, phi.phi);
monthly_phis.push((month.clone(), phi.phi));
}
Err(e) => {
println!(" {} Phi failed: {}", month, e);
monthly_phis.push((month.clone(), 0.0));
}
}
}
// 10. Null hypothesis testing
println!(
"\n--- Null Hypothesis Testing ({} shuffled correlations) ---",
null_samples
);
let mut rng = ChaCha8Rng::seed_from_u64(42);
let mut null_phis = Vec::with_capacity(null_samples);
for i in 0..null_samples {
let null_tpm = data::generate_null_tpm(neutral_data, &mut rng);
let null_ctpm = to_consciousness_tpm(&null_tpm);
if let Ok(null_phi) = auto_compute_phi(&null_ctpm, None, &budget) {
null_phis.push(null_phi.phi);
}
if (i + 1) % 10 == 0 {
print!(" [{}/{}] ", i + 1, null_samples);
}
}
println!();
// Compute statistics
let null_mean = if null_phis.is_empty() {
0.0
} else {
null_phis.iter().sum::<f64>() / null_phis.len() as f64
};
let null_std = if null_phis.len() > 1 {
(null_phis
.iter()
.map(|&p| (p - null_mean).powi(2))
.sum::<f64>()
/ (null_phis.len() as f64 - 1.0))
.sqrt()
} else {
0.0
};
let z_score = if null_std > 1e-10 {
(neutral_full_phi.phi - null_mean) / null_std
} else {
0.0
};
let p_value = if null_phis.is_empty() {
1.0
} else {
null_phis
.iter()
.filter(|&&p| p >= neutral_full_phi.phi)
.count() as f64
/ null_phis.len() as f64
};
AnalysisResults {
neutral_full_phi,
elnino_full_phi,
neutral_regional_phis,
elnino_regional_phis,
neutral_emergence,
neutral_svd_emergence,
elnino_increases_phi,
pacific_most_integrated,
monthly_phis,
null_phis,
z_score,
p_value,
}
}

View file

@ -0,0 +1,274 @@
//! Climate teleconnection data generation.
//!
//! Builds correlation matrices for 7 major climate indices based on known
//! teleconnection strengths. Generates neutral baseline and El Nino active
//! variants.
use rand::Rng;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Climate index identifiers.
pub const INDEX_ENSO: usize = 0; // El Nino Southern Oscillation (Nino3.4)
pub const INDEX_NAO: usize = 1; // North Atlantic Oscillation
pub const INDEX_PDO: usize = 2; // Pacific Decadal Oscillation
pub const INDEX_AMO: usize = 3; // Atlantic Multidecadal Oscillation
pub const INDEX_IOD: usize = 4; // Indian Ocean Dipole
pub const INDEX_SAM: usize = 5; // Southern Annular Mode
pub const INDEX_QBO: usize = 6; // Quasi-Biennial Oscillation
pub const INDEX_NAMES: &[&str] = &["ENSO", "NAO", "PDO", "AMO", "IOD", "SAM", "QBO"];
pub const N_INDICES: usize = 7;
/// Regional subsets of climate indices.
pub const PACIFIC_INDICES: &[usize] = &[INDEX_ENSO, INDEX_PDO, INDEX_IOD];
pub const ATLANTIC_INDICES: &[usize] = &[INDEX_NAO, INDEX_AMO];
pub const POLAR_INDICES: &[usize] = &[INDEX_SAM, INDEX_QBO];
pub const REGION_NAMES: &[&str] = &["Pacific", "Atlantic", "Polar"];
pub fn all_regions() -> Vec<(&'static str, &'static [usize])> {
vec![
(REGION_NAMES[0], PACIFIC_INDICES),
(REGION_NAMES[1], ATLANTIC_INDICES),
(REGION_NAMES[2], POLAR_INDICES),
]
}
/// Climate mode correlation data.
pub struct ClimateCorrelations {
pub n_indices: usize,
/// Flat row-major correlation matrix (symmetric, diagonal = 1.0).
pub correlations: Vec<f64>,
/// Variant label.
pub variant: String,
}
/// Transition probability matrix for consciousness analysis.
pub struct TransitionMatrix {
pub size: usize,
pub data: Vec<f64>,
}
/// Build the neutral baseline correlation matrix.
///
/// Known teleconnection strengths (approximate, from literature):
/// - ENSO <-> IOD: strong (0.5-0.7)
/// - ENSO <-> PDO: moderate (0.3-0.5)
/// - NAO <-> AMO: moderate (0.2-0.4)
/// - QBO <-> ENSO: weak but real (0.1-0.2)
/// - SAM: mostly independent
pub fn build_neutral_correlations() -> ClimateCorrelations {
let n = N_INDICES;
let mut corr = vec![0.0f64; n * n];
let mut rng = ChaCha8Rng::seed_from_u64(42);
// Set diagonal to 1.0 (self-correlation)
for i in 0..n {
corr[i * n + i] = 1.0;
}
// Define known teleconnection strengths (symmetric)
let connections: &[(usize, usize, f64, f64)] = &[
// (index_a, index_b, min_corr, max_corr)
(INDEX_ENSO, INDEX_IOD, 0.50, 0.70), // Strong: ENSO-IOD coupling
(INDEX_ENSO, INDEX_PDO, 0.30, 0.50), // Moderate: Pacific basin coupling
(INDEX_NAO, INDEX_AMO, 0.20, 0.40), // Moderate: Atlantic coupling
(INDEX_QBO, INDEX_ENSO, 0.10, 0.20), // Weak: stratosphere-troposphere
(INDEX_PDO, INDEX_IOD, 0.10, 0.25), // Weak: Indo-Pacific coupling
(INDEX_ENSO, INDEX_NAO, 0.05, 0.15), // Weak: Pacific-Atlantic bridge
(INDEX_ENSO, INDEX_SAM, 0.05, 0.15), // Weak: tropical-polar link
(INDEX_AMO, INDEX_PDO, 0.05, 0.10), // Very weak: inter-basin
(INDEX_SAM, INDEX_QBO, 0.03, 0.08), // Very weak: polar-stratosphere
(INDEX_NAO, INDEX_QBO, 0.05, 0.12), // Weak: NAO-QBO link
(INDEX_SAM, INDEX_NAO, 0.02, 0.06), // Very weak: bipolar
(INDEX_IOD, INDEX_AMO, 0.02, 0.06), // Very weak: Indian-Atlantic
(INDEX_AMO, INDEX_SAM, 0.01, 0.04), // Negligible
(INDEX_IOD, INDEX_NAO, 0.01, 0.04), // Negligible
(INDEX_IOD, INDEX_SAM, 0.01, 0.03), // Negligible
(INDEX_PDO, INDEX_NAO, 0.02, 0.06), // Very weak
(INDEX_PDO, INDEX_SAM, 0.01, 0.04), // Negligible
(INDEX_QBO, INDEX_AMO, 0.02, 0.05), // Very weak
(INDEX_QBO, INDEX_PDO, 0.03, 0.08), // Very weak
(INDEX_QBO, INDEX_IOD, 0.02, 0.06), // Very weak
(INDEX_QBO, INDEX_SAM, 0.03, 0.08), // Very weak
];
for &(a, b, min_c, max_c) in connections {
let c = rng.gen_range(min_c..max_c);
corr[a * n + b] = c;
corr[b * n + a] = c;
}
ClimateCorrelations {
n_indices: n,
correlations: corr,
variant: "Neutral".into(),
}
}
/// Build the El Nino active variant.
///
/// During El Nino events, ENSO correlations strengthen by ~50%,
/// and the Pacific basin becomes more tightly coupled.
pub fn build_elnino_correlations() -> ClimateCorrelations {
let mut data = build_neutral_correlations();
data.variant = "El Nino Active".into();
let n = data.n_indices;
// Boost all ENSO correlations by 50%
for j in 0..n {
if j != INDEX_ENSO {
let boosted = (data.correlations[INDEX_ENSO * n + j] * 1.5).min(0.95);
data.correlations[INDEX_ENSO * n + j] = boosted;
data.correlations[j * n + INDEX_ENSO] = boosted;
}
}
// Also boost intra-Pacific correlations
let pacific_boost: &[(usize, usize)] = &[
(INDEX_PDO, INDEX_IOD),
];
for &(a, b) in pacific_boost {
let boosted = (data.correlations[a * n + b] * 1.3).min(0.90);
data.correlations[a * n + b] = boosted;
data.correlations[b * n + a] = boosted;
}
data
}
/// Convert a correlation matrix to a transition probability matrix.
///
/// Method (same approach as the CMB example):
/// 1. Use absolute correlations as coupling strengths.
/// 2. Apply a sharpness exponent (alpha = 2.0) to emphasize strong connections.
/// 3. Row-normalize to get transition probabilities.
pub fn correlation_to_tpm(data: &ClimateCorrelations) -> TransitionMatrix {
let n = data.n_indices;
let alpha = 2.0;
let mut tpm = vec![0.0f64; n * n];
for i in 0..n {
let mut row_sum = 0.0f64;
for j in 0..n {
let c = data.correlations[i * n + j].abs();
let val = c.powf(alpha);
tpm[i * n + j] = val;
row_sum += val;
}
// Row-normalize
if row_sum > 1e-30 {
for j in 0..n {
tpm[i * n + j] /= row_sum;
}
}
}
TransitionMatrix { size: n, data: tpm }
}
/// Extract a sub-TPM for a subset of climate indices.
pub fn extract_sub_tpm(tpm: &TransitionMatrix, indices: &[usize]) -> TransitionMatrix {
let n = indices.len();
let mut sub = vec![0.0f64; n * n];
for (si, &ii) in indices.iter().enumerate() {
let row_sum: f64 = indices.iter().map(|&ij| tpm.data[ii * tpm.size + ij]).sum();
for (sj, &ij) in indices.iter().enumerate() {
sub[si * n + sj] = tpm.data[ii * tpm.size + ij] / row_sum.max(1e-30);
}
}
TransitionMatrix { size: n, data: sub }
}
/// Generate a null-model correlation matrix by shuffling off-diagonal entries.
pub fn generate_null_tpm(data: &ClimateCorrelations, rng: &mut impl rand::Rng) -> TransitionMatrix {
let n = data.n_indices;
let mut corr = data.correlations.clone();
// Collect upper-triangular off-diagonal values
let mut upper: Vec<f64> = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
upper.push(corr[i * n + j]);
}
}
// Shuffle
for k in (1..upper.len()).rev() {
let swap_idx = rng.gen_range(0..=k);
upper.swap(k, swap_idx);
}
// Put back (symmetric)
let mut idx = 0;
for i in 0..n {
for j in (i + 1)..n {
corr[i * n + j] = upper[idx];
corr[j * n + i] = upper[idx];
idx += 1;
}
}
let shuffled = ClimateCorrelations {
n_indices: n,
correlations: corr,
variant: "Null".into(),
};
correlation_to_tpm(&shuffled)
}
/// Generate monthly seasonal variation of correlations.
///
/// ENSO peaks in boreal winter (DJF), weakens in spring (MAM).
/// NAO is strongest in winter, weaker in summer.
pub fn generate_monthly_tpms(base: &ClimateCorrelations) -> Vec<(String, TransitionMatrix)> {
let months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let n = base.n_indices;
// ENSO seasonal modulation: peaks in DJF (months 0,1,11)
let enso_seasonal = [1.3, 1.2, 0.9, 0.7, 0.6, 0.5, 0.5, 0.6, 0.7, 0.9, 1.1, 1.3];
// NAO seasonal modulation: peaks in DJF
let nao_seasonal = [1.4, 1.3, 1.0, 0.7, 0.5, 0.4, 0.4, 0.5, 0.7, 0.9, 1.1, 1.3];
months
.iter()
.enumerate()
.map(|(m, &name)| {
let mut corr = base.correlations.clone();
// Modulate ENSO connections
for j in 0..n {
if j != INDEX_ENSO {
corr[INDEX_ENSO * n + j] *= enso_seasonal[m];
corr[j * n + INDEX_ENSO] *= enso_seasonal[m];
// Clamp to valid correlation range
corr[INDEX_ENSO * n + j] = corr[INDEX_ENSO * n + j].min(0.95);
corr[j * n + INDEX_ENSO] = corr[j * n + INDEX_ENSO].min(0.95);
}
}
// Modulate NAO connections
for j in 0..n {
if j != INDEX_NAO {
corr[INDEX_NAO * n + j] *= nao_seasonal[m];
corr[j * n + INDEX_NAO] *= nao_seasonal[m];
corr[INDEX_NAO * n + j] = corr[INDEX_NAO * n + j].min(0.95);
corr[j * n + INDEX_NAO] = corr[j * n + INDEX_NAO].min(0.95);
}
}
let monthly = ClimateCorrelations {
n_indices: n,
correlations: corr,
variant: format!("{} modulated", name),
};
(name.to_string(), correlation_to_tpm(&monthly))
})
.collect()
}

View file

@ -0,0 +1,89 @@
//! Climate Teleconnection Consciousness Explorer
//!
//! Applies IIT Phi to climate mode interactions to study how large-scale
//! climate oscillations form integrated information systems. Compares
//! neutral vs El Nino active conditions.
mod analysis;
mod data;
mod report;
fn main() {
println!("+==========================================================+");
println!("| Climate Teleconnection Consciousness Explorer |");
println!("| IIT 4.0 Phi Analysis of Climate Mode Interactions |");
println!("+==========================================================+");
// Parse CLI args
let args: Vec<String> = std::env::args().collect();
let null_samples = parse_arg(&args, "--null-samples", 50usize);
let output = parse_str_arg(&args, "--output", "climate_report.svg");
println!("\nConfiguration:");
println!(" Climate indices: 7 (ENSO, NAO, PDO, AMO, IOD, SAM, QBO)");
println!(" Null samples: {}", null_samples);
println!(" Output: {}", output);
// Step 1: Build climate correlation data
println!("\n=== Step 1: Building Climate Mode Correlation Data ===");
let neutral = data::build_neutral_correlations();
let elnino = data::build_elnino_correlations();
println!(" Neutral baseline: {} climate indices", neutral.n_indices);
println!(" El Nino active: {} climate indices", elnino.n_indices);
// Step 2: Construct TPMs
println!("\n=== Step 2: Constructing Transition Probability Matrices ===");
let neutral_tpm = data::correlation_to_tpm(&neutral);
let elnino_tpm = data::correlation_to_tpm(&elnino);
println!(" Neutral TPM: {}x{}", neutral_tpm.size, neutral_tpm.size);
println!(" El Nino TPM: {}x{}", elnino_tpm.size, elnino_tpm.size);
// Step 3: Run analysis
println!("\n=== Step 3: Consciousness Analysis ===");
let results = analysis::run_analysis(
&neutral, &neutral_tpm,
&elnino, &elnino_tpm,
null_samples,
);
// Step 4: Print report
println!("\n=== Step 4: Results ===");
report::print_summary(&results);
// Step 5: Generate SVG
let svg = report::generate_svg(&results, &neutral);
std::fs::write(output, &svg).expect("Failed to write SVG report");
println!(
"\nSVG report saved to: {}",
parse_str_arg(&args, "--output", "climate_report.svg")
);
// Final verdict
println!("\n+==========================================================+");
if results.elnino_increases_phi {
println!("| RESULT: El Nino INCREASES integrated information in |");
println!("| the climate system -- teleconnections strengthen. |");
} else {
println!("| RESULT: El Nino does NOT increase integrated |");
println!("| information -- system remains loosely coupled. |");
}
if results.pacific_most_integrated {
println!("| The Pacific basin (ENSO, PDO, IOD) is the most |");
println!("| integrated climate subsystem. |");
}
println!("+==========================================================+");
}
fn parse_arg<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
args.windows(2)
.find(|w| w[0] == name)
.and_then(|w| w[1].parse().ok())
.unwrap_or(default)
}
fn parse_str_arg<'a>(args: &'a [String], name: &str, default: &'a str) -> &'a str {
args.windows(2)
.find(|w| w[0] == name)
.map(|w| w[1].as_str())
.unwrap_or(default)
}

View file

@ -0,0 +1,485 @@
//! Report generation: text summary and SVG visualization for climate modes.
use crate::analysis::AnalysisResults;
use crate::data::{self, ClimateCorrelations};
/// Print a text summary of the analysis results.
pub fn print_summary(results: &AnalysisResults) {
println!("\n--- IIT Phi: Neutral vs El Nino ---");
println!(
"Neutral full Phi: {:.6} ({})",
results.neutral_full_phi.phi, results.neutral_full_phi.algorithm
);
println!(
"El Nino full Phi: {:.6} ({})",
results.elnino_full_phi.phi, results.elnino_full_phi.algorithm
);
println!("\n--- Regional Phi (Neutral) ---");
for (name, phi) in &results.neutral_regional_phis {
println!("{:20} Phi = {:.6}", name, phi.phi);
}
println!("\n--- Regional Phi (El Nino) ---");
for (name, phi) in &results.elnino_regional_phis {
println!("{:20} Phi = {:.6}", name, phi.phi);
}
println!("\n--- Seasonal Phi Cycle ---");
let max_monthly = results
.monthly_phis
.iter()
.map(|(_, p)| *p)
.fold(0.0f64, f64::max)
.max(1e-10);
for (month, phi) in &results.monthly_phis {
let bar_len = (phi / max_monthly * 30.0) as usize;
println!(" {:3} Phi={:.4} {}", month, phi, "|".repeat(bar_len));
}
println!("\n--- Causal Emergence (Neutral) ---");
println!(
"EI (micro): {:.4} bits",
results.neutral_emergence.ei_micro
);
println!(
"Causal emergence: {:.4}",
results.neutral_emergence.causal_emergence
);
println!("Determinism: {:.4}", results.neutral_emergence.determinism);
println!("Degeneracy: {:.4}", results.neutral_emergence.degeneracy);
println!("\n--- SVD Emergence (Neutral) ---");
println!(
"Effective rank: {}/7",
results.neutral_svd_emergence.effective_rank
);
println!(
"Spectral entropy: {:.4}",
results.neutral_svd_emergence.spectral_entropy
);
println!(
"Emergence index: {:.4}",
results.neutral_svd_emergence.emergence_index
);
println!("\n--- Null Hypothesis Testing ---");
let null_mean = if results.null_phis.is_empty() {
0.0
} else {
results.null_phis.iter().sum::<f64>() / results.null_phis.len() as f64
};
println!("Phi (observed): {:.6}", results.neutral_full_phi.phi);
println!(
"Phi (null mean): {:.6} ({} samples)",
null_mean,
results.null_phis.len()
);
println!("z-score: {:.2}", results.z_score);
println!("p-value: {:.4}", results.p_value);
println!("\n--- Key Findings ---");
println!(
"El Nino > Neutral: {}",
if results.elnino_increases_phi { "YES" } else { "NO" }
);
println!(
"Pacific most integrated: {}",
if results.pacific_most_integrated { "YES" } else { "NO" }
);
}
/// Generate a self-contained SVG report with climate mode connection diagram.
pub fn generate_svg(results: &AnalysisResults, data: &ClimateCorrelations) -> String {
let mut svg = String::with_capacity(20_000);
svg.push_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1600" font-family="monospace" font-size="12">
<style>
.title { font-size: 20px; font-weight: bold; fill: #333; }
.subtitle { font-size: 14px; fill: #666; }
.axis-label { font-size: 11px; fill: #444; }
.bar { fill: #4a90d9; }
.bar-elnino { fill: #e74c3c; }
.bar-null { fill: #ccc; }
.bar-month { fill: #2ecc71; }
.node-pacific { fill: #3498db; }
.node-atlantic { fill: #e67e22; }
.node-polar { fill: #9b59b6; }
.edge { stroke: #bbb; stroke-width: 0.5; fill: none; }
.edge-strong { stroke: #2c3e50; stroke-width: 2; fill: none; }
</style>
<rect width="1200" height="1600" fill="white"/>
<text x="600" y="40" text-anchor="middle" class="title">Climate Teleconnection Consciousness Report</text>
<text x="600" y="65" text-anchor="middle" class="subtitle">IIT 4.0 Phi Analysis of Climate Mode Interactions</text>
"#,
);
// Panel 1: Climate mode connection diagram (y=100, h=350)
svg.push_str(&render_connection_diagram(data, 50, 100, 500, 350));
// Panel 2: Regional Phi comparison (y=100, x=600, h=350)
svg.push_str(&render_phi_comparison(results, 620, 100, 530, 350));
// Panel 3: Seasonal Phi cycle (y=500, h=250)
svg.push_str(&render_seasonal_cycle(&results.monthly_phis, 50, 510, 1100, 250));
// Panel 4: Null distribution (y=810, h=250)
svg.push_str(&render_null_distribution(
&results.null_phis,
results.neutral_full_phi.phi,
50,
810,
1100,
250,
));
// Panel 5: Summary stats (y=1110)
svg.push_str(&render_summary_stats(results, 50, 1120));
svg.push_str("</svg>\n");
svg
}
/// Render the climate mode connection diagram.
fn render_connection_diagram(
data: &ClimateCorrelations,
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Climate Mode Teleconnections (Neutral)</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
let n = data.n_indices;
let cx = w as f64 / 2.0;
let cy = h as f64 / 2.0;
let radius = (w.min(h) as f64 / 2.0) - 45.0;
// Circular layout
let mut positions = vec![(0.0f64, 0.0f64); n];
for i in 0..n {
let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64
- std::f64::consts::FRAC_PI_2;
positions[i] = (cx + radius * angle.cos(), cy + radius * angle.sin());
}
// Draw edges (correlation strengths)
for i in 0..n {
for j in (i + 1)..n {
let c = data.correlations[i * n + j];
if c.abs() > 0.03 {
let (x1, y1) = positions[i];
let (x2, y2) = positions[j];
let class = if c.abs() > 0.3 { "edge-strong" } else { "edge" };
let width = (c.abs() * 5.0).max(0.5).min(3.0);
s.push_str(&format!(
"<line x1=\"{:.0}\" y1=\"{:.0}\" x2=\"{:.0}\" y2=\"{:.0}\" class=\"{}\" stroke-width=\"{:.1}\"/>\n",
x1, y1, x2, y2, class, width
));
}
}
}
// Region colors for each index
let node_classes = [
"node-pacific", // ENSO
"node-atlantic", // NAO
"node-pacific", // PDO
"node-atlantic", // AMO
"node-pacific", // IOD
"node-polar", // SAM
"node-polar", // QBO
];
// Draw nodes
for i in 0..n {
let (px, py) = positions[i];
s.push_str(&format!(
"<circle cx=\"{:.0}\" cy=\"{:.0}\" r=\"18\" class=\"{}\" stroke=\"#333\" stroke-width=\"1\"/>\n",
px, py, node_classes[i]
));
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{:.0}\" text-anchor=\"middle\" dominant-baseline=\"middle\" font-size=\"9\" fill=\"white\" font-weight=\"bold\">{}</text>\n",
px, py, data::INDEX_NAMES[i]
));
}
// Legend
let legend_y = h - 30;
let region_info = [("Pacific", "node-pacific"), ("Atlantic", "node-atlantic"), ("Polar", "node-polar")];
for (idx, (name, class)) in region_info.iter().enumerate() {
let lx = 10 + idx as i32 * 140;
s.push_str(&format!(
"<circle cx=\"{}\" cy=\"{}\" r=\"6\" class=\"{}\"/>\n",
lx, legend_y, class
));
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"axis-label\" dominant-baseline=\"middle\">{}</text>\n",
lx + 10, legend_y, name
));
}
s.push_str("</g>\n");
s
}
/// Render regional Phi comparison: neutral vs El Nino.
fn render_phi_comparison(results: &AnalysisResults, x: i32, y: i32, w: i32, h: i32) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Regional Phi: Neutral vs El Nino</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
let mut all_phis: Vec<f64> = Vec::new();
all_phis.push(results.neutral_full_phi.phi);
all_phis.push(results.elnino_full_phi.phi);
for (_, p) in &results.neutral_regional_phis {
all_phis.push(p.phi);
}
for (_, p) in &results.elnino_regional_phis {
all_phis.push(p.phi);
}
let max_phi = all_phis.iter().cloned().fold(0.0f64, f64::max).max(1e-10);
let n_groups = results.neutral_regional_phis.len() + 1;
let group_w = (w - 40) as f64 / n_groups as f64;
let bar_w = group_w * 0.35;
let chart_h = (h - 60) as f64;
for (idx, (name, neutral_phi)) in results.neutral_regional_phis.iter().enumerate() {
let gx = 20.0 + idx as f64 * group_w;
let bh = (neutral_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar\" rx=\"2\"/>\n",
gx, h - 30 - bh, bar_w, bh
));
if let Some((_, elnino_phi)) = results.elnino_regional_phis.iter().find(|(n, _)| n == name) {
let ebh = (elnino_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar-elnino\" rx=\"2\"/>\n",
gx + bar_w + 2.0, h - 30 - ebh, bar_w, ebh
));
}
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\" font-size=\"9\">{}</text>\n",
gx + bar_w, h - 15, name
));
}
// Full system group
let gx = 20.0 + results.neutral_regional_phis.len() as f64 * group_w;
let bh = (results.neutral_full_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar\" rx=\"2\"/>\n",
gx, h - 30 - bh, bar_w, bh
));
let ebh = (results.elnino_full_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar-elnino\" rx=\"2\"/>\n",
gx + bar_w + 2.0, h - 30 - ebh, bar_w, ebh
));
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\" font-size=\"9\">Full</text>\n",
gx + bar_w, h - 15
));
// Legend
s.push_str(&format!(
"<rect x=\"{}\" y=\"10\" width=\"12\" height=\"12\" class=\"bar\"/>\n", w - 150
));
s.push_str(&format!(
"<text x=\"{}\" y=\"20\" class=\"axis-label\">Neutral</text>\n", w - 135
));
s.push_str(&format!(
"<rect x=\"{}\" y=\"28\" width=\"12\" height=\"12\" class=\"bar-elnino\"/>\n", w - 150
));
s.push_str(&format!(
"<text x=\"{}\" y=\"38\" class=\"axis-label\">El Nino</text>\n", w - 135
));
s.push_str("</g>\n");
s
}
/// Render the seasonal Phi cycle as a bar chart.
fn render_seasonal_cycle(
monthly: &[(String, f64)],
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Seasonal Phi Cycle (12 months)</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
if monthly.is_empty() {
s.push_str("</g>\n");
return s;
}
let max_phi = monthly
.iter()
.map(|(_, p)| *p)
.fold(0.0f64, f64::max)
.max(1e-10);
let bar_w = (w - 40) as f64 / monthly.len() as f64;
let chart_h = (h - 50) as f64;
for (i, (month, phi)) in monthly.iter().enumerate() {
let bx = 20.0 + i as f64 * bar_w;
let bh = (phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar-month\" rx=\"2\"/>\n",
bx, h - bh - 25, (bar_w - 3.0).max(1.0), bh
));
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\" font-size=\"9\">{}</text>\n",
bx + bar_w / 2.0, h - 10, month
));
}
s.push_str("</g>\n");
s
}
/// Render the null distribution histogram.
fn render_null_distribution(
null_phis: &[f64],
observed: f64,
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Null Distribution (Shuffled Correlations) vs Observed Phi</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
if null_phis.is_empty() {
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\">No null samples</text>\n",
w / 2, h / 2
));
s.push_str("</g>\n");
return s;
}
let n_hist_bins = 25usize;
let phi_min = null_phis.iter().cloned().fold(f64::INFINITY, f64::min).min(observed) * 0.9;
let phi_max = null_phis.iter().cloned().fold(0.0f64, f64::max).max(observed) * 1.1;
let range = (phi_max - phi_min).max(1e-10);
let bin_width = range / n_hist_bins as f64;
let mut hist = vec![0u32; n_hist_bins];
for &p in null_phis {
let bin = ((p - phi_min) / bin_width).floor() as usize;
if bin < n_hist_bins {
hist[bin] += 1;
}
}
let max_count = *hist.iter().max().unwrap_or(&1);
let bar_w = w as f64 / n_hist_bins as f64;
for (i, &count) in hist.iter().enumerate() {
let bar_h = (count as f64 / max_count as f64 * (h - 40) as f64) as i32;
s.push_str(&format!(
"<rect x=\"{:.1}\" y=\"{}\" width=\"{:.1}\" height=\"{}\" class=\"bar-null\" rx=\"1\"/>\n",
i as f64 * bar_w, h - bar_h - 20, bar_w - 1.0, bar_h
));
}
let obs_x = ((observed - phi_min) / range * w as f64) as i32;
s.push_str(&format!(
"<line x1=\"{}\" y1=\"0\" x2=\"{}\" y2=\"{}\" stroke=\"#e74c3c\" stroke-width=\"2\"/>\n",
obs_x, obs_x, h - 20
));
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" fill=\"#e74c3c\" font-size=\"10\">Observed</text>\n",
obs_x, h - 5
));
s.push_str("</g>\n");
s
}
/// Render summary statistics text.
fn render_summary_stats(results: &AnalysisResults, x: i32, y: i32) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str("<text x=\"0\" y=\"0\" class=\"subtitle\">Summary Statistics</text>\n");
let null_mean = if results.null_phis.is_empty() {
0.0
} else {
results.null_phis.iter().sum::<f64>() / results.null_phis.len() as f64
};
let lines = vec![
format!("Neutral Full Phi: {:.6} (n=7)", results.neutral_full_phi.phi),
format!("El Nino Full Phi: {:.6} (n=7)", results.elnino_full_phi.phi),
format!(
"Null Mean Phi: {:.6} ({} samples)",
null_mean, results.null_phis.len()
),
format!("z-score: {:.3}", results.z_score),
format!("p-value: {:.4}", results.p_value),
format!("EI (micro): {:.4} bits", results.neutral_emergence.ei_micro),
format!("Causal emergence: {:.4}", results.neutral_emergence.causal_emergence),
format!(
"SVD Eff. Rank: {}/7",
results.neutral_svd_emergence.effective_rank
),
format!(
"Emergence Index: {:.4}",
results.neutral_svd_emergence.emergence_index
),
format!(
"El Nino > Neutral: {}",
if results.elnino_increases_phi { "YES" } else { "NO" }
),
format!(
"Pacific top region: {}",
if results.pacific_most_integrated { "YES" } else { "NO" }
),
];
for (i, line) in lines.iter().enumerate() {
s.push_str(&format!(
"<text x=\"0\" y=\"{}\" class=\"axis-label\">{}</text>\n",
20 + i * 18, line
));
}
s.push_str("</g>\n");
s
}

View file

@ -0,0 +1,16 @@
[package]
name = "ecosystem-consciousness"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "Ecosystem food web consciousness analysis using IIT Phi"
publish = false
[[bin]]
name = "ecosystem-consciousness"
path = "src/main.rs"
[dependencies]
ruvector-consciousness = { path = "../../crates/ruvector-consciousness", default-features = false, features = ["phi", "emergence", "collapse"] }
rand = "0.8"
rand_chacha = "0.3"

View file

@ -0,0 +1,109 @@
# Ecosystem Consciousness: IIT Phi as a Food Web Integration Metric
## Motivation
Integrated Information Theory (IIT) quantifies how much a system is
"more than the sum of its parts" through the measure Phi. Food webs
share a structural analogy: a resilient ecosystem cannot be decomposed
into independent sub-networks without losing emergent function. This
example explores whether IIT Phi correlates with ecological resilience.
## Food Web Ecology Background
### Trophic Structure
Ecosystems organize into trophic levels:
1. **Producers** -- autotrophs (plants, algae, coral) that fix energy
2. **Primary consumers** -- herbivores feeding on producers
3. **Secondary consumers** -- predators feeding on herbivores
4. **Decomposers** -- organisms recycling dead matter back to producers
5. **Apex predators** -- top-level predators with no natural enemies
### Resilience and Redundancy
Ecological resilience depends on:
- **Functional redundancy**: multiple species filling similar roles
- **Response diversity**: different species respond differently to
perturbation
- **Connectivity**: dense interaction networks buffer against single
species loss
- **Keystone species**: removal causes disproportionate collapse
## IIT as a Resilience Metric
### TPM Construction
We model each species as a "state" and construct the transition
probability matrix from energy flow weights:
TPM[i][j] = P(energy flows from species j to species i)
Row-normalization ensures each row sums to 1, giving a proper
stochastic matrix.
### Phi and Ecosystem Integration
- **High Phi**: the food web cannot be split into independent
sub-networks -- every partition loses significant information
about the whole
- **Low Phi**: the ecosystem decomposes into weakly connected
modules -- removing one module barely affects the rest
### Species Contribution
We define the "Phi contribution" of species k as:
C(k) = Phi(full) - Phi(without k)
Species with high C(k) are "consciousness keystones" -- their
removal most reduces the integrated information of the web.
## Expected Results
### Tropical Rainforest (12 species)
- Dense cross-trophic connections and nutrient cycling
- Many redundant pathways between trophic levels
- **Prediction**: HIGH Phi, relatively uniform contributions
### Agricultural Monoculture (8 species)
- Sparse, linear food chains
- Single crop dominates energy flow
- **Prediction**: LOW Phi, highly concentrated contributions
### Coral Reef (10 species)
- Moderate connectivity centered on coral as structural keystone
- Removing coral should cause largest Phi drop
- **Prediction**: MODERATE Phi, coral has disproportionate contribution
## Causal Emergence in Ecosystems
Beyond Phi, we compute causal emergence to ask: does the ecosystem
have a "macro-level" description (e.g., trophic levels) that is more
informative than the species-level description?
- High causal emergence suggests natural macro-level organization
(trophic levels are real causal entities, not just labels)
- Low causal emergence suggests species-level dynamics dominate
## Limitations
1. **Synthetic data**: real food webs have stochastic, seasonal dynamics
2. **Static TPM**: IIT assumes a fixed transition structure
3. **Small system sizes**: Phi is computationally expensive (exponential
in system size), limiting analysis to ~15 species
4. **Directionality**: IIT Phi is defined for mechanisms, not flows --
the food web analogy is suggestive, not rigorous
## References
- Tononi, G. (2008). Consciousness as Integrated Information: a
Provisional Manifesto. Biological Bulletin, 215(3).
- May, R.M. (1973). Stability and Complexity in Model Ecosystems.
- Dunne, J.A. et al. (2002). Food-web structure and network theory.
- Hoel, E.P. et al. (2013). Quantifying causal emergence shows that
macro can beat micro.

View file

@ -0,0 +1,152 @@
//! Consciousness analysis for ecosystem food webs.
//!
//! Computes IIT Phi for each ecosystem, measures resilience by removing
//! individual species, and performs causal emergence analysis.
use ruvector_consciousness::emergence::CausalEmergenceEngine;
use ruvector_consciousness::phi::auto_compute_phi;
use ruvector_consciousness::rsvd_emergence::RsvdEmergenceEngine;
use ruvector_consciousness::traits::EmergenceEngine;
use ruvector_consciousness::types::{
ComputeBudget, EmergenceResult,
TransitionMatrix as ConsciousnessTPM,
};
use ruvector_consciousness::rsvd_emergence::RsvdEmergenceResult;
use crate::data::Ecosystem;
/// Results for a single ecosystem analysis.
pub struct EcosystemResult {
pub name: String,
pub n_species: usize,
pub full_phi: f64,
pub algorithm: String,
/// (species_index, species_name, phi_without, phi_contribution)
pub species_contributions: Vec<(usize, String, f64, f64)>,
pub emergence: EmergenceResult,
pub svd_emergence: RsvdEmergenceResult,
/// Trophic level colors for each species
pub trophic_colors: Vec<String>,
/// Species names
pub species_names: Vec<String>,
}
/// Convert flat TPM data to consciousness crate format.
fn to_consciousness_tpm(data: &[f64], n: usize) -> ConsciousnessTPM {
ConsciousnessTPM::new(n, data.to_vec())
}
/// Run the full analysis pipeline on all ecosystems.
pub fn run_ecosystem_analysis(ecosystems: &[Ecosystem]) -> Vec<EcosystemResult> {
let budget = ComputeBudget::default();
let mut results = Vec::with_capacity(ecosystems.len());
for eco in ecosystems {
println!("\n--- Analyzing: {} ({} species) ---", eco.name, eco.n());
let n = eco.n();
let ctpm = to_consciousness_tpm(&eco.tpm, n);
// 1. Full system Phi
let phi_result = auto_compute_phi(&ctpm, None, &budget)
.expect("Failed to compute Phi");
let full_phi = phi_result.phi;
let algorithm = format!("{}", phi_result.algorithm);
println!(
" Full Phi = {:.6} (algorithm: {}, elapsed: {:?})",
full_phi, algorithm, phi_result.elapsed
);
// 2. Species contribution analysis (resilience)
println!(" Computing species contributions...");
let mut contributions = Vec::with_capacity(n);
for i in 0..n {
let reduced_tpm = eco.tpm_without_species(i);
let reduced_ctpm = to_consciousness_tpm(&reduced_tpm, n);
let reduced_phi = match auto_compute_phi(&reduced_ctpm, None, &budget) {
Ok(r) => r.phi,
Err(_) => 0.0,
};
let contribution = full_phi - reduced_phi;
contributions.push((
i,
eco.species[i].name.clone(),
reduced_phi,
contribution,
));
println!(
" Remove {:20} -> Phi = {:.6} (contribution: {:+.6})",
eco.species[i].name, reduced_phi, contribution
);
}
// Sort by contribution (highest first)
contributions.sort_by(|a, b| {
b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal)
});
// 3. Causal emergence
println!(" Computing causal emergence...");
let emergence_engine = CausalEmergenceEngine::new(n.min(16));
let emergence = emergence_engine
.compute_emergence(&ctpm, &budget)
.expect("Failed to compute emergence");
println!(
" EI_micro = {:.4}, determinism = {:.4}, degeneracy = {:.4}",
emergence.ei_micro, emergence.determinism, emergence.degeneracy
);
println!(
" Causal emergence = {:.4} (EI_macro = {:.4})",
emergence.causal_emergence, emergence.ei_macro
);
// 4. SVD emergence
println!(" Computing SVD emergence...");
let svd_engine = RsvdEmergenceEngine::default();
let svd_emergence = svd_engine
.compute(&ctpm, &budget)
.expect("Failed to compute SVD emergence");
println!(
" Effective rank = {}/{}, emergence index = {:.4}",
svd_emergence.effective_rank, n, svd_emergence.emergence_index
);
let trophic_colors: Vec<String> = eco
.species
.iter()
.map(|s| s.trophic_level.color().to_string())
.collect();
let species_names: Vec<String> = eco
.species
.iter()
.map(|s| s.name.clone())
.collect();
results.push(EcosystemResult {
name: eco.name.clone(),
n_species: n,
full_phi,
algorithm,
species_contributions: contributions,
emergence,
svd_emergence,
trophic_colors,
species_names,
});
}
// Cross-ecosystem comparison
println!("\n--- Cross-Ecosystem Comparison ---");
for r in &results {
let top = r
.species_contributions
.first()
.map(|(_, name, _, c)| format!("{} ({:+.4})", name, c))
.unwrap_or_default();
println!(
" {:30} Phi = {:.6} Top contributor: {}",
r.name, r.full_phi, top
);
}
results
}

View file

@ -0,0 +1,345 @@
//! Synthetic food web data generation for three ecosystem types.
//!
//! Each ecosystem is represented as a directed energy-flow graph where
//! edge weights encode predation/energy transfer probabilities. The
//! adjacency matrix is row-normalized to produce a TPM suitable for
//! IIT Phi computation.
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use rand::Rng;
/// A single species in the food web.
#[derive(Clone, Debug)]
pub struct Species {
pub name: String,
pub trophic_level: TrophicLevel,
}
/// Trophic classification for coloring and grouping.
#[derive(Clone, Debug, PartialEq)]
pub enum TrophicLevel {
Producer,
PrimaryConsumer,
SecondaryConsumer,
Decomposer,
Apex,
}
impl TrophicLevel {
pub fn color(&self) -> &'static str {
match self {
TrophicLevel::Producer => "#27ae60",
TrophicLevel::PrimaryConsumer => "#f1c40f",
TrophicLevel::SecondaryConsumer => "#e67e22",
TrophicLevel::Decomposer => "#8e44ad",
TrophicLevel::Apex => "#e74c3c",
}
}
#[allow(dead_code)]
pub fn label(&self) -> &'static str {
match self {
TrophicLevel::Producer => "Producer",
TrophicLevel::PrimaryConsumer => "Primary Consumer",
TrophicLevel::SecondaryConsumer => "Secondary Consumer",
TrophicLevel::Decomposer => "Decomposer",
TrophicLevel::Apex => "Apex",
}
}
}
/// A complete ecosystem food web.
pub struct Ecosystem {
pub name: String,
pub species: Vec<Species>,
/// Row-major adjacency matrix (energy flow weights, not yet normalized).
pub adjacency: Vec<f64>,
/// Row-normalized TPM for IIT analysis.
pub tpm: Vec<f64>,
}
impl Ecosystem {
pub fn n(&self) -> usize {
self.species.len()
}
pub fn connection_count(&self) -> usize {
self.adjacency.iter().filter(|&&w| w > 1e-10).count()
}
/// Build a TPM with one species removed (row/column zeroed, renormalized).
pub fn tpm_without_species(&self, remove_idx: usize) -> Vec<f64> {
let n = self.n();
let mut tpm = self.adjacency.clone();
// Zero out the removed species' row and column
for j in 0..n {
tpm[remove_idx * n + j] = 0.0;
tpm[j * n + remove_idx] = 0.0;
}
// Self-loop for removed species to keep matrix stochastic
tpm[remove_idx * n + remove_idx] = 1.0;
// Row-normalize remaining rows
row_normalize(&mut tpm, n);
tpm
}
}
/// Row-normalize a flat n x n matrix in place.
fn row_normalize(data: &mut [f64], n: usize) {
for i in 0..n {
let row_sum: f64 = (0..n).map(|j| data[i * n + j]).sum();
if row_sum > 1e-30 {
for j in 0..n {
data[i * n + j] /= row_sum;
}
} else {
// Uniform distribution for isolated nodes
for j in 0..n {
data[i * n + j] = 1.0 / n as f64;
}
}
}
}
/// Generate all three ecosystem food webs.
pub fn generate_all_ecosystems() -> Vec<Ecosystem> {
vec![
generate_tropical_rainforest(),
generate_agricultural_monoculture(),
generate_coral_reef(),
]
}
/// Tropical rainforest: 12 species, high connectivity, many redundant pathways.
/// Expected: HIGH Phi (deeply integrated).
fn generate_tropical_rainforest() -> Ecosystem {
let species = vec![
// Producers (0-2)
Species { name: "Canopy Tree".into(), trophic_level: TrophicLevel::Producer },
Species { name: "Understory Shrub".into(), trophic_level: TrophicLevel::Producer },
Species { name: "Epiphyte".into(), trophic_level: TrophicLevel::Producer },
// Primary consumers (3-5)
Species { name: "Leaf Insect".into(), trophic_level: TrophicLevel::PrimaryConsumer },
Species { name: "Fruit Bird".into(), trophic_level: TrophicLevel::PrimaryConsumer },
Species { name: "Herbivore Mammal".into(), trophic_level: TrophicLevel::PrimaryConsumer },
// Secondary consumers (6-8)
Species { name: "Snake".into(), trophic_level: TrophicLevel::SecondaryConsumer },
Species { name: "Raptor".into(), trophic_level: TrophicLevel::SecondaryConsumer },
Species { name: "Wild Cat".into(), trophic_level: TrophicLevel::SecondaryConsumer },
// Decomposers (9-11)
Species { name: "Fungi".into(), trophic_level: TrophicLevel::Decomposer },
Species { name: "Bacteria".into(), trophic_level: TrophicLevel::Decomposer },
Species { name: "Earthworm".into(), trophic_level: TrophicLevel::Decomposer },
];
let n = species.len();
let mut rng = ChaCha8Rng::seed_from_u64(100);
// Dense energy flow matrix with many cross-trophic connections
let mut adj = vec![0.0f64; n * n];
// Producers support each other through shared soil nutrients (weak)
set_symmetric(&mut adj, n, 0, 1, 0.15 + rng.gen::<f64>() * 0.05);
set_symmetric(&mut adj, n, 0, 2, 0.10 + rng.gen::<f64>() * 0.05);
set_symmetric(&mut adj, n, 1, 2, 0.12 + rng.gen::<f64>() * 0.05);
// Producers -> Primary consumers (strong, multiple pathways)
for prod in 0..3 {
for herb in 3..6 {
adj[herb * n + prod] = 0.3 + rng.gen::<f64>() * 0.2;
// Nutrient recycling back
adj[prod * n + herb] = 0.02 + rng.gen::<f64>() * 0.02;
}
}
// Primary -> Secondary consumers (strong)
for herb in 3..6 {
for pred in 6..9 {
adj[pred * n + herb] = 0.25 + rng.gen::<f64>() * 0.15;
}
}
// Cross-predation among secondary consumers
set_edge(&mut adj, n, 6, 7, 0.05 + rng.gen::<f64>() * 0.03);
set_edge(&mut adj, n, 7, 8, 0.04 + rng.gen::<f64>() * 0.03);
set_edge(&mut adj, n, 8, 6, 0.03 + rng.gen::<f64>() * 0.02);
// Decomposers receive from all levels (death/waste)
for src in 0..9 {
for dec in 9..12 {
adj[dec * n + src] = 0.08 + rng.gen::<f64>() * 0.06;
}
}
// Decomposers return nutrients to producers
for dec in 9..12 {
for prod in 0..3 {
adj[prod * n + dec] = 0.20 + rng.gen::<f64>() * 0.10;
}
}
// Decomposer cross-interactions
set_symmetric(&mut adj, n, 9, 10, 0.10 + rng.gen::<f64>() * 0.05);
set_symmetric(&mut adj, n, 10, 11, 0.08 + rng.gen::<f64>() * 0.04);
set_symmetric(&mut adj, n, 9, 11, 0.06 + rng.gen::<f64>() * 0.03);
// Secondary consumers occasionally eat decomposers (omnivory)
for pred in 6..9 {
adj[pred * n + 11] = 0.03 + rng.gen::<f64>() * 0.02; // eat earthworms
}
let mut tpm = adj.clone();
row_normalize(&mut tpm, n);
Ecosystem {
name: "Tropical Rainforest".to_string(),
species,
adjacency: adj,
tpm,
}
}
/// Agricultural monoculture: 8 species, sparse linear chains.
/// Expected: LOW Phi (fragile, decomposable).
fn generate_agricultural_monoculture() -> Ecosystem {
let species = vec![
// 0: Crop (producer)
Species { name: "Wheat Crop".into(), trophic_level: TrophicLevel::Producer },
// 1: Pest
Species { name: "Aphid Pest".into(), trophic_level: TrophicLevel::PrimaryConsumer },
// 2: Predator of pest
Species { name: "Ladybug".into(), trophic_level: TrophicLevel::SecondaryConsumer },
// 3: Pollinator
Species { name: "Honeybee".into(), trophic_level: TrophicLevel::PrimaryConsumer },
// 4-5: Soil microbes
Species { name: "Nitrogen Fixer".into(), trophic_level: TrophicLevel::Decomposer },
Species { name: "Mycorrhiza".into(), trophic_level: TrophicLevel::Decomposer },
// 6: Weed
Species { name: "Weed".into(), trophic_level: TrophicLevel::Producer },
// 7: Resistant pest variant
Species { name: "Resistant Aphid".into(), trophic_level: TrophicLevel::PrimaryConsumer },
];
let n = species.len();
let mut rng = ChaCha8Rng::seed_from_u64(200);
let mut adj = vec![0.0f64; n * n];
// Simple linear chain: crop -> pest -> predator
adj[1 * n + 0] = 0.7 + rng.gen::<f64>() * 0.1; // pest eats crop
adj[2 * n + 1] = 0.6 + rng.gen::<f64>() * 0.1; // ladybug eats pest
adj[2 * n + 7] = 0.3 + rng.gen::<f64>() * 0.1; // ladybug eats resistant pest
// Pollinator weakly interacts with crop
adj[3 * n + 0] = 0.2 + rng.gen::<f64>() * 0.05; // bee visits crop
adj[0 * n + 3] = 0.15 + rng.gen::<f64>() * 0.05; // crop benefits from bee
// Soil microbes support crop
adj[0 * n + 4] = 0.25 + rng.gen::<f64>() * 0.05; // nitrogen fixation
adj[0 * n + 5] = 0.20 + rng.gen::<f64>() * 0.05; // mycorrhizal support
// Crop waste feeds soil microbes (weak)
adj[4 * n + 0] = 0.10 + rng.gen::<f64>() * 0.03;
adj[5 * n + 0] = 0.08 + rng.gen::<f64>() * 0.03;
// Weed competes with crop (negative interaction modeled as weak link)
adj[6 * n + 0] = 0.05 + rng.gen::<f64>() * 0.02;
adj[0 * n + 6] = 0.02 + rng.gen::<f64>() * 0.01;
// Resistant pest also eats crop
adj[7 * n + 0] = 0.5 + rng.gen::<f64>() * 0.1;
// Pest and resistant pest weakly interact
adj[1 * n + 7] = 0.02;
adj[7 * n + 1] = 0.02;
let mut tpm = adj.clone();
row_normalize(&mut tpm, n);
Ecosystem {
name: "Agricultural Monoculture".to_string(),
species,
adjacency: adj,
tpm,
}
}
/// Coral reef: 10 species, moderate connectivity with keystone species (coral).
/// Expected: MODERATE Phi, but removing coral collapses integration.
fn generate_coral_reef() -> Ecosystem {
let species = vec![
// 0: Coral (keystone)
Species { name: "Coral".into(), trophic_level: TrophicLevel::Producer },
// 1: Algae
Species { name: "Algae".into(), trophic_level: TrophicLevel::Producer },
// 2-4: Fish
Species { name: "Clownfish".into(), trophic_level: TrophicLevel::PrimaryConsumer },
Species { name: "Parrotfish".into(), trophic_level: TrophicLevel::PrimaryConsumer },
Species { name: "Grouper".into(), trophic_level: TrophicLevel::SecondaryConsumer },
// 5-6: Invertebrates
Species { name: "Sea Urchin".into(), trophic_level: TrophicLevel::PrimaryConsumer },
Species { name: "Crown-of-Thorns".into(), trophic_level: TrophicLevel::PrimaryConsumer },
// 7: Shark (apex)
Species { name: "Reef Shark".into(), trophic_level: TrophicLevel::Apex },
// 8: Sea turtle
Species { name: "Sea Turtle".into(), trophic_level: TrophicLevel::SecondaryConsumer },
// 9: Plankton
Species { name: "Plankton".into(), trophic_level: TrophicLevel::Producer },
];
let n = species.len();
let mut rng = ChaCha8Rng::seed_from_u64(300);
let mut adj = vec![0.0f64; n * n];
// Coral is the structural keystone: many species depend on it
// Coral shelters clownfish
adj[2 * n + 0] = 0.5 + rng.gen::<f64>() * 0.1;
// Parrotfish grazes algae off coral (mutually beneficial)
adj[3 * n + 1] = 0.4 + rng.gen::<f64>() * 0.1;
adj[0 * n + 3] = 0.3 + rng.gen::<f64>() * 0.1; // coral benefits from parrotfish
// Grouper eats smaller fish
adj[4 * n + 2] = 0.3 + rng.gen::<f64>() * 0.1;
adj[4 * n + 3] = 0.2 + rng.gen::<f64>() * 0.1;
// Sea urchin grazes algae
adj[5 * n + 1] = 0.35 + rng.gen::<f64>() * 0.1;
// Crown-of-thorns eats coral (destructive)
adj[6 * n + 0] = 0.4 + rng.gen::<f64>() * 0.1;
// Shark eats grouper and turtle
adj[7 * n + 4] = 0.35 + rng.gen::<f64>() * 0.1;
adj[7 * n + 8] = 0.15 + rng.gen::<f64>() * 0.05;
// Sea turtle eats algae and invertebrates
adj[8 * n + 1] = 0.2 + rng.gen::<f64>() * 0.05;
adj[8 * n + 5] = 0.15 + rng.gen::<f64>() * 0.05;
adj[8 * n + 6] = 0.10 + rng.gen::<f64>() * 0.05;
// Plankton feeds coral and clownfish
adj[0 * n + 9] = 0.3 + rng.gen::<f64>() * 0.1;
adj[2 * n + 9] = 0.2 + rng.gen::<f64>() * 0.05;
// Algae and coral compete for space
adj[1 * n + 0] = 0.1 + rng.gen::<f64>() * 0.05;
adj[0 * n + 1] = 0.08 + rng.gen::<f64>() * 0.03;
// Nutrient recycling from consumers back to producers/plankton
for consumer in [2, 3, 4, 5, 6, 7, 8] {
adj[9 * n + consumer] = 0.03 + rng.gen::<f64>() * 0.02;
}
let mut tpm = adj.clone();
row_normalize(&mut tpm, n);
Ecosystem {
name: "Coral Reef".to_string(),
species,
adjacency: adj,
tpm,
}
}
/// Set a directed edge weight.
fn set_edge(adj: &mut [f64], n: usize, from: usize, to: usize, weight: f64) {
adj[from * n + to] = weight;
}
/// Set symmetric (bidirectional) edge weights.
fn set_symmetric(adj: &mut [f64], n: usize, a: usize, b: usize, weight: f64) {
adj[a * n + b] = weight;
adj[b * n + a] = weight;
}

View file

@ -0,0 +1,71 @@
//! Ecosystem Consciousness Explorer
//!
//! Applies IIT Phi to food web networks to measure ecosystem integration
//! and resilience. Compares tropical rainforest, agricultural monoculture,
//! and coral reef ecosystems.
mod analysis;
mod data;
mod report;
fn main() {
println!("+==========================================================+");
println!("| Ecosystem Consciousness Explorer -- IIT 4.0 Analysis |");
println!("| Measuring food web integration via Phi |");
println!("+==========================================================+");
// Parse CLI args
let args: Vec<String> = std::env::args().collect();
let output = parse_str_arg(&args, "--output", "ecosystem_report.svg");
println!("\nConfiguration:");
println!(" Output: {}", output);
// Step 1: Generate food web data
println!("\n=== Step 1: Generating Synthetic Food Webs ===");
let ecosystems = data::generate_all_ecosystems();
for eco in &ecosystems {
println!(
" {}: {} species, {} connections",
eco.name, eco.species.len(), eco.connection_count()
);
}
// Step 2: Run consciousness analysis
println!("\n=== Step 2: IIT Phi Analysis ===");
let results = analysis::run_ecosystem_analysis(&ecosystems);
// Step 3: Print text summary
println!("\n=== Step 3: Results Summary ===");
report::print_summary(&results);
// Step 4: Generate SVG report
let svg = report::generate_svg(&results);
std::fs::write(output, &svg).expect("Failed to write SVG report");
println!(
"\nSVG report saved to: {}",
parse_str_arg(&args, "--output", "ecosystem_report.svg")
);
// Final comparison
println!("\n+==========================================================+");
println!("| Ecosystem Integration Ranking (by Phi): |");
let mut sorted: Vec<_> = results.iter().collect();
sorted.sort_by(|a, b| b.full_phi.partial_cmp(&a.full_phi).unwrap());
for (i, r) in sorted.iter().enumerate() {
println!(
"| {}. {:30} Phi = {:.6} |",
i + 1,
r.name,
r.full_phi
);
}
println!("+==========================================================+");
}
fn parse_str_arg<'a>(args: &'a [String], name: &str, default: &'a str) -> &'a str {
args.windows(2)
.find(|w| w[0] == name)
.map(|w| w[1].as_str())
.unwrap_or(default)
}

View file

@ -0,0 +1,236 @@
//! Report generation: text summary and SVG food web visualization.
use crate::analysis::EcosystemResult;
/// Print a text summary of all ecosystem results.
pub fn print_summary(results: &[EcosystemResult]) {
for r in results {
println!("\n========== {} ==========", r.name);
println!("Species count: {}", r.n_species);
println!("Full system Phi: {:.6} ({})", r.full_phi, r.algorithm);
println!("\nSpecies Phi contributions (sorted by importance):");
for (_, name, phi_without, contribution) in &r.species_contributions {
let bar_len = ((contribution.abs() / r.full_phi.max(1e-10)) * 20.0) as usize;
let bar_char = if *contribution > 0.0 { "+" } else { "-" };
println!(
" {:20} {:+.6} (Phi without: {:.6}) {}",
name,
contribution,
phi_without,
bar_char.repeat(bar_len.min(30))
);
}
println!("\nCausal Emergence:");
println!(" EI (micro): {:.4} bits", r.emergence.ei_micro);
println!(" EI (macro): {:.4} bits", r.emergence.ei_macro);
println!(
" Causal emergence: {:.4}",
r.emergence.causal_emergence
);
println!(" Determinism: {:.4}", r.emergence.determinism);
println!(" Degeneracy: {:.4}", r.emergence.degeneracy);
println!("\nSVD Emergence:");
println!(
" Effective rank: {}/{}",
r.svd_emergence.effective_rank, r.n_species
);
println!(
" Spectral entropy: {:.4}",
r.svd_emergence.spectral_entropy
);
println!(
" Emergence index: {:.4}",
r.svd_emergence.emergence_index
);
println!(
" Reversibility: {:.4}",
r.svd_emergence.reversibility
);
}
}
/// Generate a self-contained SVG report with food web diagrams.
pub fn generate_svg(results: &[EcosystemResult]) -> String {
let panel_height = 500;
let total_height = 100 + results.len() as i32 * (panel_height + 50);
let width = 1200;
let mut svg = String::with_capacity(30_000);
svg.push_str(&format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {} {}" font-family="monospace" font-size="12">
<style>
.title {{ font-size: 20px; font-weight: bold; fill: #333; }}
.subtitle {{ font-size: 14px; fill: #666; font-weight: bold; }}
.label {{ font-size: 10px; fill: #333; }}
.bar {{ fill: #4a90d9; }}
.bar-neg {{ fill: #e74c3c; }}
.stat {{ font-size: 11px; fill: #444; }}
</style>
<rect width="{}" height="{}" fill="white"/>
<text x="600" y="40" text-anchor="middle" class="title">Ecosystem Consciousness Analysis Report</text>
<text x="600" y="65" text-anchor="middle" class="stat">IIT Phi measures integrated information in food web networks</text>
"#,
width, total_height, width, total_height
));
for (idx, r) in results.iter().enumerate() {
let y_off = 100 + idx as i32 * (panel_height + 50);
svg.push_str(&render_ecosystem_panel(r, 30, y_off, width - 60, panel_height));
}
svg.push_str("</svg>\n");
svg
}
/// Render a single ecosystem panel with food web and contribution bars.
fn render_ecosystem_panel(
r: &EcosystemResult,
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
// Panel background
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\" rx=\"5\"/>\n",
w, h
));
// Title
s.push_str(&format!(
"<text x=\"15\" y=\"25\" class=\"subtitle\">{} (n={}, Phi={:.6})</text>\n",
r.name, r.n_species, r.full_phi
));
// Left panel: food web node diagram (circular layout)
let cx = 200;
let cy = h / 2 + 20;
let radius = 140;
let n = r.n_species;
// Draw nodes in a circle, sized by Phi contribution
let max_contrib = r
.species_contributions
.iter()
.map(|(_, _, _, c)| c.abs())
.fold(0.0f64, f64::max)
.max(1e-10);
// Build contribution lookup by species index
let mut contrib_by_idx = vec![0.0f64; n];
for (idx, _, _, c) in &r.species_contributions {
contrib_by_idx[*idx] = *c;
}
// Node positions
let positions: Vec<(f64, f64)> = (0..n)
.map(|i| {
let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64
- std::f64::consts::FRAC_PI_2;
(
cx as f64 + radius as f64 * angle.cos(),
cy as f64 + radius as f64 * angle.sin(),
)
})
.collect();
// Draw nodes
for i in 0..n {
let (nx, ny) = positions[i];
let node_r = 8.0 + (contrib_by_idx[i].abs() / max_contrib * 14.0);
let color = &r.trophic_colors[i];
s.push_str(&format!(
"<circle cx=\"{:.0}\" cy=\"{:.0}\" r=\"{:.1}\" fill=\"{}\" stroke=\"#333\" stroke-width=\"1\"/>\n",
nx, ny, node_r, color
));
// Label
let label = if r.species_names[i].len() > 8 {
&r.species_names[i][..8]
} else {
&r.species_names[i]
};
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{:.0}\" text-anchor=\"middle\" class=\"label\">{}</text>\n",
nx,
ny + node_r + 12.0,
label
));
}
// Right panel: contribution bar chart
let bar_x = 420;
let bar_w = w - bar_x - 30;
let bar_h = h - 80;
s.push_str(&format!(
"<text x=\"{}\" y=\"50\" class=\"subtitle\">Species Phi Contributions</text>\n",
bar_x
));
let contributions = &r.species_contributions;
if !contributions.is_empty() {
let row_h = (bar_h as f64 / contributions.len() as f64).min(30.0);
let max_abs = contributions
.iter()
.map(|(_, _, _, c)| c.abs())
.fold(0.0f64, f64::max)
.max(1e-10);
for (i, (_, name, _, contrib)) in contributions.iter().enumerate() {
let ry = 65 + (i as f64 * row_h) as i32;
let bw = (contrib.abs() / max_abs * (bar_w as f64 * 0.5)) as i32;
let bar_class = if *contrib >= 0.0 { "bar" } else { "bar-neg" };
// Species name
let display_name = if name.len() > 16 {
&name[..16]
} else {
name.as_str()
};
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" text-anchor=\"end\" class=\"label\">{}</text>\n",
bar_x + 120,
ry + (row_h as i32) / 2 + 4,
display_name
));
// Bar
s.push_str(&format!(
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" class=\"{}\" rx=\"2\"/>\n",
bar_x + 125,
ry,
bw.max(1),
(row_h - 4.0).max(4.0) as i32,
bar_class
));
// Value label
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"label\">{:+.4}</text>\n",
bar_x + 130 + bw,
ry + (row_h as i32) / 2 + 4,
contrib
));
}
}
// Stats box at bottom
let stats_y = h - 40;
s.push_str(&format!(
"<text x=\"15\" y=\"{}\" class=\"stat\">EI_micro={:.3} Emergence={:.3} SVD rank={}/{} Emergence idx={:.3}</text>\n",
stats_y,
r.emergence.ei_micro,
r.emergence.causal_emergence,
r.svd_emergence.effective_rank,
r.n_species,
r.svd_emergence.emergence_index
));
s.push_str("</g>\n");
s
}

View file

@ -0,0 +1,16 @@
[package]
name = "gene-consciousness"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "Gene regulatory network consciousness analysis using IIT Phi"
publish = false
[[bin]]
name = "gene-consciousness"
path = "src/main.rs"
[dependencies]
ruvector-consciousness = { path = "../../crates/ruvector-consciousness", default-features = false, features = ["phi", "emergence", "collapse"] }
rand = "0.8"
rand_chacha = "0.3"

View file

@ -0,0 +1,226 @@
//! Consciousness analysis pipeline for gene regulatory networks.
use ruvector_consciousness::emergence::CausalEmergenceEngine;
use ruvector_consciousness::phi::auto_compute_phi;
use ruvector_consciousness::rsvd_emergence::{RsvdEmergenceEngine, RsvdEmergenceResult};
use ruvector_consciousness::traits::EmergenceEngine;
use ruvector_consciousness::types::{
ComputeBudget, EmergenceResult, PhiResult,
TransitionMatrix as ConsciousnessTPM,
};
use crate::data::{self, GeneNetwork, TransitionMatrix};
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Full analysis results for the gene regulatory network.
pub struct AnalysisResults {
/// Phi for the full 16-gene normal network.
pub normal_full_phi: PhiResult,
/// Phi for the full 16-gene cancer network.
pub cancer_full_phi: PhiResult,
/// Phi for each 4-gene module in the normal network.
pub normal_module_phis: Vec<(String, PhiResult)>,
/// Phi for each 4-gene module in the cancer network.
pub cancer_module_phis: Vec<(String, PhiResult)>,
/// Causal emergence for normal network.
pub normal_emergence: EmergenceResult,
/// SVD emergence for normal network.
pub normal_svd_emergence: RsvdEmergenceResult,
/// Whether modules have higher Phi than the full network (expected: yes).
pub modules_more_integrated: bool,
/// Whether cancer rewiring increases cross-module Phi.
pub cancer_higher_cross_phi: bool,
/// Null model Phi values for statistical testing.
pub null_phis: Vec<f64>,
/// Z-score of observed Phi vs null distribution.
pub z_score: f64,
/// Empirical p-value.
pub p_value: f64,
}
/// Convert our TPM to the consciousness crate's format.
fn to_consciousness_tpm(tpm: &TransitionMatrix) -> ConsciousnessTPM {
ConsciousnessTPM::new(tpm.size, tpm.data.clone())
}
/// Run the complete analysis pipeline.
pub fn run_analysis(
normal_net: &GeneNetwork,
normal_tpm: &TransitionMatrix,
_cancer_net: &GeneNetwork,
cancer_tpm: &TransitionMatrix,
null_samples: usize,
) -> AnalysisResults {
let budget = ComputeBudget::default();
// 1. Full system Phi -- normal
println!("\n--- Computing Phi: Normal Network (full 16-gene) ---");
let normal_ctpm = to_consciousness_tpm(normal_tpm);
let normal_full_phi = auto_compute_phi(&normal_ctpm, None, &budget)
.expect("Failed to compute Phi for normal network");
println!(
" Phi = {:.6} (algorithm: {}, elapsed: {:?})",
normal_full_phi.phi, normal_full_phi.algorithm, normal_full_phi.elapsed
);
// 2. Full system Phi -- cancer
println!("\n--- Computing Phi: Cancer Network (full 16-gene) ---");
let cancer_ctpm = to_consciousness_tpm(cancer_tpm);
let cancer_full_phi = auto_compute_phi(&cancer_ctpm, None, &budget)
.expect("Failed to compute Phi for cancer network");
println!(
" Phi = {:.6} (algorithm: {}, elapsed: {:?})",
cancer_full_phi.phi, cancer_full_phi.algorithm, cancer_full_phi.elapsed
);
// 3. Module-level Phi -- normal
println!("\n--- Computing Phi: Normal Network Modules ---");
let modules = data::all_modules();
let mut normal_module_phis = Vec::new();
for (name, genes) in &modules {
let sub = data::extract_sub_tpm(normal_tpm, genes);
let sub_ctpm = to_consciousness_tpm(&sub);
match auto_compute_phi(&sub_ctpm, None, &budget) {
Ok(phi) => {
println!(" {} Phi = {:.6} (genes {:?})", name, phi.phi, genes);
normal_module_phis.push((name.to_string(), phi));
}
Err(e) => {
println!(" {} Phi computation failed: {}", name, e);
}
}
}
// 4. Module-level Phi -- cancer
println!("\n--- Computing Phi: Cancer Network Modules ---");
let mut cancer_module_phis = Vec::new();
for (name, genes) in &modules {
let sub = data::extract_sub_tpm(cancer_tpm, genes);
let sub_ctpm = to_consciousness_tpm(&sub);
match auto_compute_phi(&sub_ctpm, None, &budget) {
Ok(phi) => {
println!(" {} Phi = {:.6} (genes {:?})", name, phi.phi, genes);
cancer_module_phis.push((name.to_string(), phi));
}
Err(e) => {
println!(" {} Phi computation failed: {}", name, e);
}
}
}
// 5. Compare: modules vs full network
let avg_module_phi = if normal_module_phis.is_empty() {
0.0
} else {
normal_module_phis.iter().map(|(_, p)| p.phi).sum::<f64>()
/ normal_module_phis.len() as f64
};
let modules_more_integrated = avg_module_phi > normal_full_phi.phi;
println!(
"\n Avg module Phi ({:.6}) {} full network Phi ({:.6})",
avg_module_phi,
if modules_more_integrated { ">" } else { "<=" },
normal_full_phi.phi
);
// 6. Compare: cancer vs normal cross-module integration
let cancer_higher_cross_phi = cancer_full_phi.phi > normal_full_phi.phi;
println!(
" Cancer Phi ({:.6}) {} Normal Phi ({:.6})",
cancer_full_phi.phi,
if cancer_higher_cross_phi { ">" } else { "<=" },
normal_full_phi.phi
);
// 7. Causal emergence -- normal network
println!("\n--- Causal Emergence Analysis (Normal) ---");
let emergence_engine = CausalEmergenceEngine::new(normal_tpm.size.min(16));
let normal_emergence = emergence_engine
.compute_emergence(&normal_ctpm, &budget)
.expect("Failed to compute causal emergence");
println!(
" EI_micro = {:.4} bits, determinism = {:.4}, degeneracy = {:.4}",
normal_emergence.ei_micro, normal_emergence.determinism, normal_emergence.degeneracy
);
println!(
" Causal emergence = {:.4}, coarse-graining: {:?}",
normal_emergence.causal_emergence, normal_emergence.coarse_graining
);
// 8. SVD emergence
println!("\n--- SVD Emergence Analysis (Normal) ---");
let svd_engine = RsvdEmergenceEngine::default();
let normal_svd_emergence = svd_engine
.compute(&normal_ctpm, &budget)
.expect("Failed to compute SVD emergence");
println!(
" Effective rank = {}/{}, entropy = {:.4}, emergence = {:.4}",
normal_svd_emergence.effective_rank, normal_tpm.size,
normal_svd_emergence.spectral_entropy, normal_svd_emergence.emergence_index
);
// 9. Null hypothesis testing
println!(
"\n--- Null Hypothesis Testing ({} randomized networks) ---",
null_samples
);
let mut rng = ChaCha8Rng::seed_from_u64(42);
let mut null_phis = Vec::with_capacity(null_samples);
for i in 0..null_samples {
let null_tpm = data::generate_null_tpm(normal_net, &mut rng);
let null_ctpm = to_consciousness_tpm(&null_tpm);
if let Ok(null_phi) = auto_compute_phi(&null_ctpm, None, &budget) {
null_phis.push(null_phi.phi);
}
if (i + 1) % 10 == 0 {
print!(" [{}/{}] ", i + 1, null_samples);
}
}
println!();
// Compute statistics
let null_mean = if null_phis.is_empty() {
0.0
} else {
null_phis.iter().sum::<f64>() / null_phis.len() as f64
};
let null_std = if null_phis.len() > 1 {
(null_phis
.iter()
.map(|&p| (p - null_mean).powi(2))
.sum::<f64>()
/ (null_phis.len() as f64 - 1.0))
.sqrt()
} else {
0.0
};
let z_score = if null_std > 1e-10 {
(normal_full_phi.phi - null_mean) / null_std
} else {
0.0
};
let p_value = if null_phis.is_empty() {
1.0
} else {
null_phis
.iter()
.filter(|&&p| p >= normal_full_phi.phi)
.count() as f64
/ null_phis.len() as f64
};
AnalysisResults {
normal_full_phi,
cancer_full_phi,
normal_module_phis,
cancer_module_phis,
normal_emergence,
normal_svd_emergence,
modules_more_integrated,
cancer_higher_cross_phi,
null_phis,
z_score,
p_value,
}
}

View file

@ -0,0 +1,279 @@
//! Gene regulatory network data generation.
//!
//! Builds synthetic gene regulatory networks based on known biological motifs,
//! with 4 functional modules: cell cycle, apoptosis, growth signaling, and
//! housekeeping. Also generates an oncogenic "cancer" variant where growth
//! signaling overrides apoptosis controls.
use rand::Rng;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Module assignment for a 16-gene network.
pub const MODULE_CELL_CYCLE: &[usize] = &[0, 1, 2, 3];
pub const MODULE_APOPTOSIS: &[usize] = &[4, 5, 6, 7];
pub const MODULE_GROWTH: &[usize] = &[8, 9, 10, 11];
pub const MODULE_HOUSEKEEPING: &[usize] = &[12, 13, 14, 15];
pub const MODULE_NAMES: &[&str] = &["Cell Cycle", "Apoptosis", "Growth Signaling", "Housekeeping"];
/// All modules with their gene indices.
pub fn all_modules() -> Vec<(&'static str, &'static [usize])> {
vec![
(MODULE_NAMES[0], MODULE_CELL_CYCLE),
(MODULE_NAMES[1], MODULE_APOPTOSIS),
(MODULE_NAMES[2], MODULE_GROWTH),
(MODULE_NAMES[3], MODULE_HOUSEKEEPING),
]
}
/// A gene regulatory network represented as a weighted adjacency matrix.
pub struct GeneNetwork {
pub n_genes: usize,
/// Flat row-major adjacency/regulation weights. Positive = activation,
/// negative = repression. Range roughly [-1, 1].
pub adjacency: Vec<f64>,
/// Human-readable gene labels.
pub gene_labels: Vec<String>,
/// Module index for each gene (0..3).
pub module_ids: Vec<usize>,
/// Network variant label.
pub variant: String,
}
impl GeneNetwork {
/// Count non-zero edges (absolute value > 0.001).
pub fn n_edges(&self) -> usize {
self.adjacency
.iter()
.filter(|&&w| w.abs() > 0.001)
.count()
}
}
/// Transition probability matrix for consciousness analysis.
pub struct TransitionMatrix {
pub size: usize,
pub data: Vec<f64>,
}
/// Build the normal (healthy) 16-gene regulatory network.
///
/// Module structure:
/// - Cell cycle (genes 0-3): cyclin cascade with strong internal regulation
/// - Apoptosis (genes 4-7): pro- and anti-apoptotic balance
/// - Growth signaling (genes 8-11): receptor tyrosine kinase cascade
/// - Housekeeping (genes 12-15): weakly connected to all other modules
///
/// Within-module: strong connections (0.3-0.5)
/// Between-module: weak connections (0.01-0.05)
/// Housekeeping: weakly connected to everything
pub fn build_normal_network() -> GeneNetwork {
let n = 16;
let mut adj = vec![0.0f64; n * n];
let mut rng = ChaCha8Rng::seed_from_u64(42);
let gene_labels = vec![
// Cell cycle
"CycD".into(), "CDK4".into(), "CycE".into(), "CDK2".into(),
// Apoptosis
"BAX".into(), "BCL2".into(), "CASP3".into(), "p53".into(),
// Growth signaling
"EGFR".into(), "RAS".into(), "RAF".into(), "ERK".into(),
// Housekeeping
"GAPDH".into(), "ACTB".into(), "RPL13A".into(), "HPRT".into(),
];
let module_ids: Vec<usize> = (0..n)
.map(|i| i / 4)
.collect();
// Within-module connections: strong, directed cascade-like
let modules: &[&[usize]] = &[
MODULE_CELL_CYCLE,
MODULE_APOPTOSIS,
MODULE_GROWTH,
MODULE_HOUSEKEEPING,
];
for module in modules {
for (idx, &from) in module.iter().enumerate() {
for (jdx, &to) in module.iter().enumerate() {
if from == to {
continue;
}
// Sequential cascade: strong forward, moderate feedback
let base: f64 = if jdx == idx + 1 {
0.45 // strong forward connection
} else if idx == jdx + 1 {
0.20 // feedback
} else {
0.15 // lateral
};
let noise: f64 = rng.gen_range(-0.05..0.05);
adj[from * n + to] = (base + noise).clamp(0.0, 0.5);
}
}
}
// Apoptosis module: add inhibitory connections (BCL2 inhibits BAX/CASP3)
adj[5 * n + 4] = -0.35; // BCL2 -| BAX
adj[5 * n + 6] = -0.30; // BCL2 -| CASP3
adj[7 * n + 4] = 0.40; // p53 -> BAX (pro-apoptotic)
adj[7 * n + 5] = -0.25; // p53 -| BCL2
// Between-module connections: weak
// Growth signaling -> Cell cycle (growth promotes division)
adj[11 * n + 0] = 0.04; // ERK -> CycD
adj[11 * n + 1] = 0.03; // ERK -> CDK4
// Growth signaling -> Apoptosis (growth suppresses apoptosis)
adj[11 * n + 5] = 0.03; // ERK -> BCL2 (anti-apoptotic)
// Apoptosis -> Cell cycle (apoptosis inhibits division)
adj[6 * n + 2] = -0.02; // CASP3 -| CycE
// Housekeeping: weak connections to all modules
for &hk in MODULE_HOUSEKEEPING {
for g in 0..12 {
let w = rng.gen_range(0.005..0.02);
adj[hk * n + g] = w;
adj[g * n + hk] = w * 0.5;
}
}
GeneNetwork {
n_genes: n,
adjacency: adj,
gene_labels,
module_ids,
variant: "Normal".into(),
}
}
/// Build the cancer (oncogenic) variant.
///
/// Key rewiring:
/// 1. Growth signaling is constitutively active (stronger internal connections)
/// 2. Growth signaling overrides apoptosis controls (strong cross-module edges)
/// 3. p53 pathway is disrupted (weakened connections)
/// 4. Cell cycle checkpoints are bypassed
///
/// Expected: higher cross-module Phi due to loss of modular boundaries.
pub fn build_cancer_network() -> GeneNetwork {
let mut net = build_normal_network();
net.variant = "Cancer".into();
let n = net.n_genes;
// 1. Constitutively active growth signaling (boost internal connections)
for &from in MODULE_GROWTH {
for &to in MODULE_GROWTH {
if from != to {
let idx = from * n + to;
net.adjacency[idx] = (net.adjacency[idx] * 1.5).clamp(-0.5, 0.5);
}
}
}
// 2. Growth overrides apoptosis (strong cross-module edges)
net.adjacency[11 * n + 5] = 0.30; // ERK -> BCL2 (strong anti-apoptotic)
net.adjacency[10 * n + 5] = 0.25; // RAF -> BCL2
net.adjacency[11 * n + 4] = -0.20; // ERK -| BAX
net.adjacency[9 * n + 6] = -0.15; // RAS -| CASP3
// 3. p53 pathway disruption (simulate TP53 mutation)
net.adjacency[7 * n + 4] = 0.05; // p53 -> BAX weakened
net.adjacency[7 * n + 5] = -0.05; // p53 -| BCL2 weakened
// 4. Cell cycle checkpoint bypass
net.adjacency[11 * n + 0] = 0.25; // ERK -> CycD (strong growth drive)
net.adjacency[11 * n + 1] = 0.20; // ERK -> CDK4
net.adjacency[6 * n + 2] = -0.005; // CASP3 -| CycE weakened
net
}
/// Convert a gene regulatory network to a transition probability matrix.
///
/// Method:
/// 1. Take absolute values of adjacency weights (treat activation/repression
/// as "information flow" regardless of sign)
/// 2. Add self-regulation (diagonal) as baseline activity
/// 3. Row-normalize to get transition probabilities
pub fn network_to_tpm(net: &GeneNetwork) -> TransitionMatrix {
let n = net.n_genes;
let mut tpm = vec![0.0f64; n * n];
for i in 0..n {
for j in 0..n {
// Use absolute adjacency weight as transition strength
tpm[i * n + j] = net.adjacency[i * n + j].abs();
}
// Add self-regulation baseline (genes maintain their own state)
tpm[i * n + i] += 0.1;
}
// Row-normalize
for i in 0..n {
let row_sum: f64 = (0..n).map(|j| tpm[i * n + j]).sum();
if row_sum > 1e-30 {
for j in 0..n {
tpm[i * n + j] /= row_sum;
}
}
}
TransitionMatrix { size: n, data: tpm }
}
/// Extract a sub-TPM for a subset of genes.
pub fn extract_sub_tpm(tpm: &TransitionMatrix, genes: &[usize]) -> TransitionMatrix {
let n = genes.len();
let mut sub = vec![0.0f64; n * n];
for (si, &gi) in genes.iter().enumerate() {
let row_sum: f64 = genes.iter().map(|&gj| tpm.data[gi * tpm.size + gj]).sum();
for (sj, &gj) in genes.iter().enumerate() {
sub[si * n + sj] = tpm.data[gi * tpm.size + gj] / row_sum.max(1e-30);
}
}
TransitionMatrix { size: n, data: sub }
}
/// Generate a null-model network by shuffling connections while preserving
/// degree distribution (configuration model).
pub fn generate_null_tpm(net: &GeneNetwork, rng: &mut impl rand::Rng) -> TransitionMatrix {
let n = net.n_genes;
let mut adj = net.adjacency.clone();
// Shuffle non-diagonal entries while preserving row sums
for i in 0..n {
let mut row_vals: Vec<f64> = (0..n)
.filter(|&j| j != i)
.map(|j| adj[i * n + j])
.collect();
// Fisher-Yates shuffle
for k in (1..row_vals.len()).rev() {
let swap_idx = rng.gen_range(0..=k);
row_vals.swap(k, swap_idx);
}
let mut idx = 0;
for j in 0..n {
if j != i {
adj[i * n + j] = row_vals[idx];
idx += 1;
}
}
}
let shuffled = GeneNetwork {
n_genes: n,
adjacency: adj,
gene_labels: net.gene_labels.clone(),
module_ids: net.module_ids.clone(),
variant: "Null".into(),
};
network_to_tpm(&shuffled)
}

View file

@ -0,0 +1,87 @@
//! Gene Regulatory Network Consciousness Explorer
//!
//! Applies IIT Phi to gene regulatory networks to identify emergent
//! regulatory modules. Compares normal vs oncogenic (cancer) network
//! rewiring to study how disease alters integrated information.
mod analysis;
mod data;
mod report;
fn main() {
println!("+==========================================================+");
println!("| Gene Regulatory Network Consciousness Explorer |");
println!("| IIT 4.0 Phi Analysis of Regulatory Modules |");
println!("+==========================================================+");
// Parse CLI args
let args: Vec<String> = std::env::args().collect();
let null_samples = parse_arg(&args, "--null-samples", 50usize);
let output = parse_str_arg(&args, "--output", "gene_report.svg");
println!("\nConfiguration:");
println!(" Genes: 16 (4 modules x 4 genes)");
println!(" Null samples: {}", null_samples);
println!(" Output: {}", output);
// Step 1: Build gene regulatory networks
println!("\n=== Step 1: Building Gene Regulatory Networks ===");
let normal = data::build_normal_network();
let cancer = data::build_cancer_network();
println!(" Normal network: {} genes, {} edges", normal.n_genes, normal.n_edges());
println!(" Cancer network: {} genes, {} edges", cancer.n_genes, cancer.n_edges());
// Step 2: Construct TPMs
println!("\n=== Step 2: Constructing Transition Probability Matrices ===");
let normal_tpm = data::network_to_tpm(&normal);
let cancer_tpm = data::network_to_tpm(&cancer);
println!(" Normal TPM: {}x{}", normal_tpm.size, normal_tpm.size);
println!(" Cancer TPM: {}x{}", cancer_tpm.size, cancer_tpm.size);
// Step 3: Run analysis
println!("\n=== Step 3: Consciousness Analysis ===");
let results = analysis::run_analysis(&normal, &normal_tpm, &cancer, &cancer_tpm, null_samples);
// Step 4: Print report
println!("\n=== Step 4: Results ===");
report::print_summary(&results);
// Step 5: Generate SVG
let svg = report::generate_svg(&results, &normal);
std::fs::write(output, &svg).expect("Failed to write SVG report");
println!(
"\nSVG report saved to: {}",
parse_str_arg(&args, "--output", "gene_report.svg")
);
// Final verdict
println!("\n+==========================================================+");
if results.modules_more_integrated {
println!("| RESULT: Modules ARE the irreducible units of |");
println!("| integrated information in the regulatory network. |");
} else {
println!("| RESULT: Full network is more integrated than modules. |");
println!("| The regulatory network acts as a unified whole. |");
}
if results.cancer_higher_cross_phi {
println!("| Cancer rewiring INCREASES cross-module integration, |");
println!("| consistent with loss of modular boundaries. |");
} else {
println!("| Cancer rewiring does NOT increase cross-module Phi. |");
}
println!("+==========================================================+");
}
fn parse_arg<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
args.windows(2)
.find(|w| w[0] == name)
.and_then(|w| w[1].parse().ok())
.unwrap_or(default)
}
fn parse_str_arg<'a>(args: &'a [String], name: &str, default: &'a str) -> &'a str {
args.windows(2)
.find(|w| w[0] == name)
.map(|w| w[1].as_str())
.unwrap_or(default)
}

View file

@ -0,0 +1,413 @@
//! Report generation: text summary and SVG visualization for gene networks.
use crate::analysis::AnalysisResults;
use crate::data::{self, GeneNetwork};
/// Print a text summary of the analysis results.
pub fn print_summary(results: &AnalysisResults) {
println!("\n--- IIT Phi: Normal vs Cancer ---");
println!(
"Normal full Phi: {:.6} ({})",
results.normal_full_phi.phi, results.normal_full_phi.algorithm
);
println!(
"Cancer full Phi: {:.6} ({})",
results.cancer_full_phi.phi, results.cancer_full_phi.algorithm
);
println!("\n--- Module-Level Phi (Normal) ---");
for (name, phi) in &results.normal_module_phis {
println!("{:20} Phi = {:.6}", name, phi.phi);
}
println!("\n--- Module-Level Phi (Cancer) ---");
for (name, phi) in &results.cancer_module_phis {
println!("{:20} Phi = {:.6}", name, phi.phi);
}
println!("\n--- Causal Emergence (Normal) ---");
println!(
"EI (micro): {:.4} bits",
results.normal_emergence.ei_micro
);
println!(
"Causal emergence: {:.4}",
results.normal_emergence.causal_emergence
);
println!("Determinism: {:.4}", results.normal_emergence.determinism);
println!("Degeneracy: {:.4}", results.normal_emergence.degeneracy);
println!("\n--- SVD Emergence (Normal) ---");
println!(
"Effective rank: {}/16",
results.normal_svd_emergence.effective_rank
);
println!(
"Spectral entropy: {:.4}",
results.normal_svd_emergence.spectral_entropy
);
println!(
"Emergence index: {:.4}",
results.normal_svd_emergence.emergence_index
);
println!("\n--- Null Hypothesis Testing ---");
let null_mean = if results.null_phis.is_empty() {
0.0
} else {
results.null_phis.iter().sum::<f64>() / results.null_phis.len() as f64
};
println!("Phi (observed): {:.6}", results.normal_full_phi.phi);
println!(
"Phi (null mean): {:.6} ({} samples)",
null_mean,
results.null_phis.len()
);
println!("z-score: {:.2}", results.z_score);
println!("p-value: {:.4}", results.p_value);
println!("\n--- Key Findings ---");
println!(
"Modules > full network: {}",
if results.modules_more_integrated { "YES" } else { "NO" }
);
println!(
"Cancer > normal Phi: {}",
if results.cancer_higher_cross_phi { "YES" } else { "NO" }
);
}
/// Generate a self-contained SVG report with network graph visualization.
pub fn generate_svg(results: &AnalysisResults, net: &GeneNetwork) -> String {
let mut svg = String::with_capacity(20_000);
svg.push_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1600" font-family="monospace" font-size="12">
<style>
.title { font-size: 20px; font-weight: bold; fill: #333; }
.subtitle { font-size: 14px; fill: #666; }
.axis-label { font-size: 11px; fill: #444; }
.bar { fill: #4a90d9; }
.bar-cancer { fill: #e74c3c; }
.bar-null { fill: #ccc; }
.node-cc { fill: #3498db; }
.node-ap { fill: #e74c3c; }
.node-gs { fill: #2ecc71; }
.node-hk { fill: #95a5a6; }
.edge { stroke: #bbb; stroke-width: 0.5; fill: none; }
.edge-strong { stroke: #555; stroke-width: 1.5; fill: none; }
</style>
<rect width="1200" height="1600" fill="white"/>
<text x="600" y="40" text-anchor="middle" class="title">Gene Regulatory Network Consciousness Report</text>
<text x="600" y="65" text-anchor="middle" class="subtitle">IIT 4.0 Phi Analysis of Regulatory Modules</text>
"#,
);
// Panel 1: Network graph (y=100, h=400)
svg.push_str(&render_network_graph(net, 50, 100, 500, 400));
// Panel 2: Module Phi comparison (y=100, x=600, h=400)
svg.push_str(&render_phi_comparison(results, 620, 100, 530, 400));
// Panel 3: Null distribution (y=550, h=280)
svg.push_str(&render_null_distribution(
&results.null_phis,
results.normal_full_phi.phi,
50,
560,
1100,
280,
));
// Panel 4: Summary stats (y=900)
svg.push_str(&render_summary_stats(results, 50, 900));
svg.push_str("</svg>\n");
svg
}
/// Render the gene regulatory network as a graph with nodes colored by module.
fn render_network_graph(net: &GeneNetwork, x: i32, y: i32, w: i32, h: i32) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Gene Regulatory Network (Normal)</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
// Arrange genes in a circular layout, grouped by module
let n = net.n_genes;
let cx = w as f64 / 2.0;
let cy = h as f64 / 2.0;
let radius = (w.min(h) as f64 / 2.0) - 40.0;
let mut positions = vec![(0.0f64, 0.0f64); n];
for i in 0..n {
let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64 - std::f64::consts::FRAC_PI_2;
positions[i] = (cx + radius * angle.cos(), cy + radius * angle.sin());
}
// Draw edges (only significant ones)
for i in 0..n {
for j in 0..n {
let w_val = net.adjacency[i * n + j];
if w_val.abs() > 0.05 && i != j {
let (x1, y1) = positions[i];
let (x2, y2) = positions[j];
let class = if w_val.abs() > 0.2 { "edge-strong" } else { "edge" };
let opacity = (w_val.abs() * 3.0).min(1.0);
s.push_str(&format!(
"<line x1=\"{:.0}\" y1=\"{:.0}\" x2=\"{:.0}\" y2=\"{:.0}\" class=\"{}\" opacity=\"{:.2}\"/>\n",
x1, y1, x2, y2, class, opacity
));
}
}
}
// Draw nodes
let node_classes = ["node-cc", "node-ap", "node-gs", "node-hk"];
for i in 0..n {
let (px, py) = positions[i];
let class = node_classes[net.module_ids[i]];
s.push_str(&format!(
"<circle cx=\"{:.0}\" cy=\"{:.0}\" r=\"12\" class=\"{}\" stroke=\"#333\" stroke-width=\"1\"/>\n",
px, py, class
));
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{:.0}\" text-anchor=\"middle\" dominant-baseline=\"middle\" font-size=\"8\" fill=\"white\">{}</text>\n",
px, py, &net.gene_labels[i]
));
}
// Legend
let legend_y = h - 60;
let modules = data::all_modules();
for (idx, (name, _)) in modules.iter().enumerate() {
let lx = 10 + idx as i32 * 120;
let class = node_classes[idx];
s.push_str(&format!(
"<circle cx=\"{}\" cy=\"{}\" r=\"6\" class=\"{}\"/>\n",
lx, legend_y, class
));
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"axis-label\" dominant-baseline=\"middle\">{}</text>\n",
lx + 10, legend_y, name
));
}
s.push_str("</g>\n");
s
}
/// Render module Phi comparison bar chart (normal vs cancer).
fn render_phi_comparison(results: &AnalysisResults, x: i32, y: i32, w: i32, h: i32) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Module Phi: Normal vs Cancer</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
// Collect all phi values to determine scale
let mut all_phis: Vec<f64> = Vec::new();
all_phis.push(results.normal_full_phi.phi);
all_phis.push(results.cancer_full_phi.phi);
for (_, p) in &results.normal_module_phis {
all_phis.push(p.phi);
}
for (_, p) in &results.cancer_module_phis {
all_phis.push(p.phi);
}
let max_phi = all_phis.iter().cloned().fold(0.0f64, f64::max).max(1e-10);
// Draw grouped bars: normal (blue) and cancer (red) for each module + full
let n_groups = results.normal_module_phis.len() + 1; // +1 for "Full"
let group_w = (w - 40) as f64 / n_groups as f64;
let bar_w = group_w * 0.35;
let chart_h = (h - 60) as f64;
for (idx, (name, normal_phi)) in results.normal_module_phis.iter().enumerate() {
let gx = 20.0 + idx as f64 * group_w;
// Normal bar
let bh = (normal_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar\" rx=\"2\"/>\n",
gx, h - 30 - bh, bar_w, bh
));
// Cancer bar
if let Some((_, cancer_phi)) = results.cancer_module_phis.iter().find(|(n, _)| n == name) {
let cbh = (cancer_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar-cancer\" rx=\"2\"/>\n",
gx + bar_w + 2.0, h - 30 - cbh, bar_w, cbh
));
}
// Label
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\" font-size=\"9\">{}</text>\n",
gx + bar_w, h - 15, name.split_whitespace().next().unwrap_or(name)
));
}
// Full network group
let gx = 20.0 + results.normal_module_phis.len() as f64 * group_w;
let bh = (results.normal_full_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar\" rx=\"2\"/>\n",
gx, h - 30 - bh, bar_w, bh
));
let cbh = (results.cancer_full_phi.phi / max_phi * chart_h) as i32;
s.push_str(&format!(
"<rect x=\"{:.0}\" y=\"{}\" width=\"{:.0}\" height=\"{}\" class=\"bar-cancer\" rx=\"2\"/>\n",
gx + bar_w + 2.0, h - 30 - cbh, bar_w, cbh
));
s.push_str(&format!(
"<text x=\"{:.0}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\" font-size=\"9\">Full</text>\n",
gx + bar_w, h - 15
));
// Legend
s.push_str(&format!(
"<rect x=\"{}\" y=\"10\" width=\"12\" height=\"12\" class=\"bar\"/>\n", w - 150
));
s.push_str(&format!(
"<text x=\"{}\" y=\"20\" class=\"axis-label\">Normal</text>\n", w - 135
));
s.push_str(&format!(
"<rect x=\"{}\" y=\"28\" width=\"12\" height=\"12\" class=\"bar-cancer\"/>\n", w - 150
));
s.push_str(&format!(
"<text x=\"{}\" y=\"38\" class=\"axis-label\">Cancer</text>\n", w - 135
));
s.push_str("</g>\n");
s
}
/// Render the null distribution histogram.
fn render_null_distribution(
null_phis: &[f64],
observed: f64,
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<text x=\"{}\" y=\"-5\" text-anchor=\"middle\" class=\"subtitle\">Null Distribution (Shuffled Networks) vs Observed Phi</text>\n",
w / 2
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\"/>\n",
w, h
));
if null_phis.is_empty() {
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" class=\"axis-label\">No null samples</text>\n",
w / 2, h / 2
));
s.push_str("</g>\n");
return s;
}
let n_hist_bins = 25usize;
let phi_min = null_phis.iter().cloned().fold(f64::INFINITY, f64::min).min(observed) * 0.9;
let phi_max = null_phis.iter().cloned().fold(0.0f64, f64::max).max(observed) * 1.1;
let range = (phi_max - phi_min).max(1e-10);
let bin_width = range / n_hist_bins as f64;
let mut hist = vec![0u32; n_hist_bins];
for &p in null_phis {
let bin = ((p - phi_min) / bin_width).floor() as usize;
if bin < n_hist_bins {
hist[bin] += 1;
}
}
let max_count = *hist.iter().max().unwrap_or(&1);
let bar_w = w as f64 / n_hist_bins as f64;
for (i, &count) in hist.iter().enumerate() {
let bar_h = (count as f64 / max_count as f64 * (h - 40) as f64) as i32;
s.push_str(&format!(
"<rect x=\"{:.1}\" y=\"{}\" width=\"{:.1}\" height=\"{}\" class=\"bar-null\" rx=\"1\"/>\n",
i as f64 * bar_w, h - bar_h - 20, bar_w - 1.0, bar_h
));
}
// Mark observed value
let obs_x = ((observed - phi_min) / range * w as f64) as i32;
s.push_str(&format!(
"<line x1=\"{}\" y1=\"0\" x2=\"{}\" y2=\"{}\" stroke=\"#e74c3c\" stroke-width=\"2\"/>\n",
obs_x, obs_x, h - 20
));
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" fill=\"#e74c3c\" font-size=\"10\">Observed</text>\n",
obs_x, h - 5
));
s.push_str("</g>\n");
s
}
/// Render summary statistics text.
fn render_summary_stats(results: &AnalysisResults, x: i32, y: i32) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str("<text x=\"0\" y=\"0\" class=\"subtitle\">Summary Statistics</text>\n");
let null_mean = if results.null_phis.is_empty() {
0.0
} else {
results.null_phis.iter().sum::<f64>() / results.null_phis.len() as f64
};
let lines = vec![
format!("Normal Full Phi: {:.6} (n=16)", results.normal_full_phi.phi),
format!("Cancer Full Phi: {:.6} (n=16)", results.cancer_full_phi.phi),
format!(
"Null Mean Phi: {:.6} ({} samples)",
null_mean, results.null_phis.len()
),
format!("z-score: {:.3}", results.z_score),
format!("p-value: {:.4}", results.p_value),
format!("EI (micro): {:.4} bits", results.normal_emergence.ei_micro),
format!("Causal emergence: {:.4}", results.normal_emergence.causal_emergence),
format!(
"SVD Eff. Rank: {}/16",
results.normal_svd_emergence.effective_rank
),
format!(
"Emergence Index: {:.4}",
results.normal_svd_emergence.emergence_index
),
format!(
"Modules > Full: {}",
if results.modules_more_integrated { "YES" } else { "NO" }
),
format!(
"Cancer > Normal: {}",
if results.cancer_higher_cross_phi { "YES" } else { "NO" }
),
];
for (i, line) in lines.iter().enumerate() {
s.push_str(&format!(
"<text x=\"0\" y=\"{}\" class=\"axis-label\">{}</text>\n",
20 + i * 18, line
));
}
s.push_str("</g>\n");
s
}

View file

@ -0,0 +1,16 @@
[package]
name = "quantum-consciousness"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "Quantum circuit consciousness analysis using IIT Phi"
publish = false
[[bin]]
name = "quantum-consciousness"
path = "src/main.rs"
[dependencies]
ruvector-consciousness = { path = "../../crates/ruvector-consciousness", default-features = false, features = ["phi", "emergence", "collapse"] }
rand = "0.8"
rand_chacha = "0.3"

View file

@ -0,0 +1,119 @@
# Quantum Circuit Consciousness: IIT Phi and Entanglement
## Motivation
Integrated Information Theory (IIT) and quantum entanglement both
formalize the idea that a system is "more than the sum of its parts."
This example explores whether IIT's Phi measure, applied to quantum
circuit measurement statistics, captures the same structure as
standard entanglement measures.
## Quantum States Analyzed
### 1. Bell State (2 qubits)
The maximally entangled two-qubit state:
|Psi> = (|00> + |11>) / sqrt(2)
Prepared by H(0) followed by CNOT(0,1). Measurement in the
computational basis yields 00 or 11 with equal probability, never
01 or 10. This maximal correlation should produce HIGH Phi.
### 2. GHZ State (3 qubits)
The Greenberger-Horne-Zeilinger state:
|GHZ> = (|000> + |111>) / sqrt(2)
Genuinely multipartite entangled: tracing out any single qubit
destroys all entanglement. Expected to show HIGH Phi and high
emergence (the 3-party correlations cannot be reduced to 2-party).
### 3. Product State (3 qubits)
|Psi> = |0> x |0> x |0>
Completely separable, no entanglement. The TPM is the identity
matrix (each input maps to itself). Expected Phi = 0 since the
system decomposes perfectly into independent parts.
### 4. W State (3 qubits)
|W> = (|001> + |010> + |100>) / sqrt(3)
Bipartite entanglement that survives partial trace: tracing out
any one qubit still leaves the other two entangled. Different
entanglement structure from GHZ. Expected: Phi between product
and GHZ.
### 5. Random Circuit (3 qubits, depth 5)
Random single-qubit rotations interleaved with CNOT gates. The
resulting entanglement depends on the specific random gates chosen.
Serves as a control to show that Phi varies continuously with
circuit structure.
## TPM Construction for Quantum Circuits
The key mapping from quantum mechanics to IIT:
TPM[i][j] = |<j|U|i>|^2
where U is the circuit unitary, and i, j are computational basis
states. This gives the probability of measuring outcome j when the
input is the basis state i. The resulting TPM is doubly stochastic
for unitary circuits (both rows and columns sum to 1).
## Expected Phi Hierarchy
Standard entanglement measures predict:
Product (0) < W < Bell <= GHZ
IIT's Phi may not follow this ordering exactly because:
1. Phi measures integrated information across the minimum information
partition (MIP), not entanglement per se
2. The Bell state is 2-qubit while GHZ/W are 3-qubit, so the
partition spaces differ
3. W state has different entanglement structure (robust to qubit loss)
which may be valued differently by Phi
## Entanglement Measures for Comparison
- **Concurrence** (2 qubits): measures entanglement of formation
- **Tangle** (3 qubits): measures genuine 3-party entanglement
- **Entanglement entropy**: von Neumann entropy of reduced density
matrix
The GHZ state has maximal tangle but zero concurrence for any pair.
The W state has zero tangle but nonzero concurrence for every pair.
## Causal Emergence in Quantum Systems
Causal emergence asks: is there a macro-level description of the
quantum system that is more informative than the qubit-level
description? For entangled states, the answer may be yes -- the
entangled subsystem behaves as a single effective degree of freedom.
## Limitations
1. **Classical TPM**: we use |<j|U|i>|^2, discarding quantum phases.
IIT on the full quantum state (quantum IIT) is an active research
area.
2. **Measurement basis dependence**: Phi depends on the choice of
computational basis. A different measurement basis could yield
different Phi values.
3. **Small systems**: 3 qubits = 8x8 TPM, well within exact Phi
computation limits but far from interesting quantum advantage
regimes.
## References
- Tononi, G. (2008). Consciousness as Integrated Information.
- Zanardi, P. et al. (2018). Quantum Integrated Information Theory.
- Greenberger, D.M. et al. (1989). Going Beyond Bell's Theorem.
- Dur, W. et al. (2000). Three qubits can be entangled in two
inequivalent ways.
- Hoel, E.P. (2017). When the Map Is Better Than the Territory.

View file

@ -0,0 +1,169 @@
//! Consciousness analysis for quantum circuits.
//!
//! Computes IIT Phi for each circuit's measurement TPM and compares
//! the Phi hierarchy with known entanglement measures.
use ruvector_consciousness::emergence::CausalEmergenceEngine;
use ruvector_consciousness::phi::auto_compute_phi;
use ruvector_consciousness::rsvd_emergence::RsvdEmergenceEngine;
use ruvector_consciousness::traits::EmergenceEngine;
use ruvector_consciousness::types::{
ComputeBudget, EmergenceResult,
TransitionMatrix as ConsciousnessTPM,
};
use ruvector_consciousness::rsvd_emergence::RsvdEmergenceResult;
use crate::data::QuantumCircuit;
/// Results for a single quantum circuit analysis.
pub struct CircuitResult {
pub name: String,
pub description: String,
pub n_qubits: usize,
pub tpm_size: usize,
pub full_phi: f64,
pub algorithm: String,
pub emergence: EmergenceResult,
pub svd_emergence: RsvdEmergenceResult,
}
/// Convert circuit TPM to consciousness crate format.
fn to_consciousness_tpm(tpm: &[f64], n: usize) -> ConsciousnessTPM {
ConsciousnessTPM::new(n, tpm.to_vec())
}
/// Run analysis on all quantum circuits.
pub fn run_quantum_analysis(circuits: &[QuantumCircuit]) -> Vec<CircuitResult> {
let budget = ComputeBudget::default();
let mut results = Vec::with_capacity(circuits.len());
for circuit in circuits {
let dim = circuit.tpm_size();
println!(
"\n--- Analyzing: {} ({} qubits, {}x{} TPM) ---",
circuit.name, circuit.n_qubits, dim, dim
);
let ctpm = to_consciousness_tpm(&circuit.tpm, dim);
// 1. Compute Phi
let phi_result = auto_compute_phi(&ctpm, None, &budget)
.expect("Failed to compute Phi");
let full_phi = phi_result.phi;
let algorithm = format!("{}", phi_result.algorithm);
println!(
" Phi = {:.6} (algorithm: {}, elapsed: {:?})",
full_phi, algorithm, phi_result.elapsed
);
// 2. Causal emergence
println!(" Computing causal emergence...");
let emergence_engine = CausalEmergenceEngine::new(dim.min(16));
let emergence = emergence_engine
.compute_emergence(&ctpm, &budget)
.expect("Failed to compute emergence");
println!(
" EI_micro = {:.4}, causal_emergence = {:.4}",
emergence.ei_micro, emergence.causal_emergence
);
// 3. SVD emergence
println!(" Computing SVD emergence...");
let svd_engine = RsvdEmergenceEngine::default();
let svd_emergence = svd_engine
.compute(&ctpm, &budget)
.expect("Failed to compute SVD emergence");
println!(
" Effective rank = {}/{}, emergence index = {:.4}",
svd_emergence.effective_rank, dim, svd_emergence.emergence_index
);
results.push(CircuitResult {
name: circuit.name.clone(),
description: circuit.description.clone(),
n_qubits: circuit.n_qubits,
tpm_size: dim,
full_phi,
algorithm,
emergence,
svd_emergence,
});
}
// Entanglement hierarchy comparison
println!("\n--- Entanglement Hierarchy Comparison ---");
println!(" Expected ordering: Product < W < Bell <= GHZ");
println!(" Actual Phi values:");
let mut sorted: Vec<&CircuitResult> = results.iter().collect();
sorted.sort_by(|a, b| a.full_phi.partial_cmp(&b.full_phi).unwrap());
for r in &sorted {
println!(" {:25} Phi = {:.6}", r.name, r.full_phi);
}
// Check if ordering matches expectations
let product_phi = results
.iter()
.find(|r| r.name == "Product State")
.map(|r| r.full_phi)
.unwrap_or(0.0);
let w_phi = results
.iter()
.find(|r| r.name == "W State")
.map(|r| r.full_phi)
.unwrap_or(0.0);
let bell_phi = results
.iter()
.find(|r| r.name == "Bell State")
.map(|r| r.full_phi)
.unwrap_or(0.0);
let ghz_phi = results
.iter()
.find(|r| r.name == "GHZ State")
.map(|r| r.full_phi)
.unwrap_or(0.0);
let order_ok = product_phi <= w_phi && w_phi <= bell_phi.max(ghz_phi);
if order_ok {
println!(
"\n Phi ordering AGREES with entanglement hierarchy."
);
} else {
println!(
"\n Phi ordering DIFFERS from naive entanglement hierarchy."
);
println!(
" This is expected: IIT Phi measures integrated information,");
println!(
" not entanglement per se. The two can diverge for certain states."
);
}
// GHZ vs W emergence comparison
println!("\n--- GHZ vs W: Emergence Structure ---");
if let (Some(ghz), Some(w)) = (
results.iter().find(|r| r.name == "GHZ State"),
results.iter().find(|r| r.name == "W State"),
) {
println!(
" GHZ: Phi={:.6}, emergence={:.4}, SVD rank={}/{}",
ghz.full_phi,
ghz.emergence.causal_emergence,
ghz.svd_emergence.effective_rank,
ghz.tpm_size
);
println!(
" W: Phi={:.6}, emergence={:.4}, SVD rank={}/{}",
w.full_phi,
w.emergence.causal_emergence,
w.svd_emergence.effective_rank,
w.tpm_size
);
if ghz.emergence.causal_emergence > w.emergence.causal_emergence {
println!(" GHZ shows MORE causal emergence than W.");
} else {
println!(" W shows MORE causal emergence than GHZ.");
}
}
results
}

View file

@ -0,0 +1,415 @@
//! Quantum circuit data: generate measurement statistics as TPMs.
//!
//! For each quantum circuit, we compute the output state vector, then
//! construct a TPM where TPM[i][j] = P(measure outcome j | input basis state i).
//! For unitary circuits, this is |<j|U|i>|^2.
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use rand::Rng;
/// A quantum circuit with its measurement TPM.
pub struct QuantumCircuit {
pub name: String,
pub n_qubits: usize,
pub description: String,
/// Row-major TPM: P(outcome_j | input_i), dimension 2^n x 2^n.
pub tpm: Vec<f64>,
/// The unitary matrix (row-major, complex: stored as separate re/im vecs).
#[allow(dead_code)]
pub unitary_re: Vec<f64>,
#[allow(dead_code)]
pub unitary_im: Vec<f64>,
}
impl QuantumCircuit {
pub fn tpm_size(&self) -> usize {
1 << self.n_qubits
}
}
/// Generate all quantum circuits for analysis.
pub fn generate_all_circuits(random_depth: usize) -> Vec<QuantumCircuit> {
vec![
generate_bell_state(),
generate_ghz_state(),
generate_product_state(),
generate_w_state(),
generate_random_circuit(random_depth),
]
}
// -------------------------------------------------------------------
// Unitary matrix helpers (2^n x 2^n complex matrices stored as re/im)
// -------------------------------------------------------------------
/// Identity matrix for n qubits.
fn identity(n_qubits: usize) -> (Vec<f64>, Vec<f64>) {
let dim = 1 << n_qubits;
let mut re = vec![0.0; dim * dim];
let im = vec![0.0; dim * dim];
for i in 0..dim {
re[i * dim + i] = 1.0;
}
(re, im)
}
/// Multiply two complex matrices C = A * B (dim x dim).
fn matmul(
a_re: &[f64], a_im: &[f64],
b_re: &[f64], b_im: &[f64],
dim: usize,
) -> (Vec<f64>, Vec<f64>) {
let mut c_re = vec![0.0; dim * dim];
let mut c_im = vec![0.0; dim * dim];
for i in 0..dim {
for k in 0..dim {
let ar = a_re[i * dim + k];
let ai = a_im[i * dim + k];
if ar.abs() < 1e-15 && ai.abs() < 1e-15 {
continue;
}
for j in 0..dim {
let br = b_re[k * dim + j];
let bi = b_im[k * dim + j];
c_re[i * dim + j] += ar * br - ai * bi;
c_im[i * dim + j] += ar * bi + ai * br;
}
}
}
(c_re, c_im)
}
/// Apply a 2-qubit gate (4x4 unitary) to qubits (q0, q1) in an n-qubit system.
/// q0 is the control (higher-order bit in the 2-qubit subspace), q1 is the target.
fn apply_two_qubit_gate(
u_re: &mut Vec<f64>, u_im: &mut Vec<f64>,
gate_re: &[f64; 16], gate_im: &[f64; 16],
n_qubits: usize, q0: usize, q1: usize,
) {
let dim = 1 << n_qubits;
// Build the full-size gate by tensoring with identities
let mut full_re = vec![0.0; dim * dim];
let mut full_im = vec![0.0; dim * dim];
for row in 0..dim {
for col in 0..dim {
// Extract the 2-bit index for (q0, q1)
let r0 = (row >> (n_qubits - 1 - q0)) & 1;
let r1 = (row >> (n_qubits - 1 - q1)) & 1;
let c0 = (col >> (n_qubits - 1 - q0)) & 1;
let c1 = (col >> (n_qubits - 1 - q1)) & 1;
// Check that all other qubits match
let mut other_match = true;
for q in 0..n_qubits {
if q != q0 && q != q1 {
if ((row >> (n_qubits - 1 - q)) & 1)
!= ((col >> (n_qubits - 1 - q)) & 1)
{
other_match = false;
break;
}
}
}
if other_match {
let gi = (r0 * 2 + r1) * 4 + (c0 * 2 + c1);
full_re[row * dim + col] = gate_re[gi];
full_im[row * dim + col] = gate_im[gi];
}
}
}
let (new_re, new_im) = matmul(&full_re, &full_im, u_re, u_im, dim);
*u_re = new_re;
*u_im = new_im;
}
/// Apply a single-qubit gate (2x2 unitary) to qubit q in an n-qubit system.
fn apply_single_qubit_gate(
u_re: &mut Vec<f64>, u_im: &mut Vec<f64>,
gate_re: &[f64; 4], gate_im: &[f64; 4],
n_qubits: usize, q: usize,
) {
let dim = 1 << n_qubits;
let mut full_re = vec![0.0; dim * dim];
let mut full_im = vec![0.0; dim * dim];
for row in 0..dim {
for col in 0..dim {
let rq = (row >> (n_qubits - 1 - q)) & 1;
let cq = (col >> (n_qubits - 1 - q)) & 1;
// All other qubits must match (identity)
let mut other_match = true;
for qq in 0..n_qubits {
if qq != q {
if ((row >> (n_qubits - 1 - qq)) & 1)
!= ((col >> (n_qubits - 1 - qq)) & 1)
{
other_match = false;
break;
}
}
}
if other_match {
let gi = rq * 2 + cq;
full_re[row * dim + col] = gate_re[gi];
full_im[row * dim + col] = gate_im[gi];
}
}
}
let (new_re, new_im) = matmul(&full_re, &full_im, u_re, u_im, dim);
*u_re = new_re;
*u_im = new_im;
}
/// Convert a unitary matrix to a TPM: TPM[i][j] = |U[j][i]|^2 = |<j|U|i>|^2
/// Note: U|i> gives column i of U, so P(j|i) = |U[j,i]|^2.
fn unitary_to_tpm(u_re: &[f64], u_im: &[f64], dim: usize) -> Vec<f64> {
let mut tpm = vec![0.0; dim * dim];
for i in 0..dim {
for j in 0..dim {
let re = u_re[j * dim + i];
let im = u_im[j * dim + i];
tpm[i * dim + j] = re * re + im * im;
}
}
// Row-normalize to correct for floating point
for i in 0..dim {
let sum: f64 = (0..dim).map(|j| tpm[i * dim + j]).sum();
if sum > 1e-30 {
for j in 0..dim {
tpm[i * dim + j] /= sum;
}
}
}
tpm
}
// -------------------------------------------------------------------
// Standard gates
// -------------------------------------------------------------------
/// Hadamard gate
const H_RE: [f64; 4] = [
std::f64::consts::FRAC_1_SQRT_2,
std::f64::consts::FRAC_1_SQRT_2,
std::f64::consts::FRAC_1_SQRT_2,
-std::f64::consts::FRAC_1_SQRT_2,
];
const H_IM: [f64; 4] = [0.0, 0.0, 0.0, 0.0];
/// CNOT gate (control=0, target=1 in 2-qubit basis |00>, |01>, |10>, |11>)
const CNOT_RE: [f64; 16] = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 1.0, 0.0,
];
const CNOT_IM: [f64; 16] = [0.0; 16];
/// X (NOT) gate
#[allow(dead_code)]
const X_RE: [f64; 4] = [0.0, 1.0, 1.0, 0.0];
#[allow(dead_code)]
const X_IM: [f64; 4] = [0.0; 4];
// -------------------------------------------------------------------
// Circuit generators
// -------------------------------------------------------------------
/// Bell state: H(0) then CNOT(0,1) on |00>
/// Creates (|00> + |11>) / sqrt(2) -- maximally entangled 2-qubit state.
fn generate_bell_state() -> QuantumCircuit {
let n = 2;
let dim = 1 << n;
let (mut u_re, mut u_im) = identity(n);
// H on qubit 0
apply_single_qubit_gate(&mut u_re, &mut u_im, &H_RE, &H_IM, n, 0);
// CNOT(0, 1)
apply_two_qubit_gate(&mut u_re, &mut u_im, &CNOT_RE, &CNOT_IM, n, 0, 1);
let tpm = unitary_to_tpm(&u_re, &u_im, dim);
QuantumCircuit {
name: "Bell State".to_string(),
n_qubits: n,
description: "|00> + |11> / sqrt(2) -- maximally entangled pair".to_string(),
tpm,
unitary_re: u_re,
unitary_im: u_im,
}
}
/// GHZ state: H(0) then CNOT(0,1) then CNOT(0,2) on |000>
/// Creates (|000> + |111>) / sqrt(2) -- genuine 3-party entanglement.
fn generate_ghz_state() -> QuantumCircuit {
let n = 3;
let dim = 1 << n;
let (mut u_re, mut u_im) = identity(n);
apply_single_qubit_gate(&mut u_re, &mut u_im, &H_RE, &H_IM, n, 0);
apply_two_qubit_gate(&mut u_re, &mut u_im, &CNOT_RE, &CNOT_IM, n, 0, 1);
apply_two_qubit_gate(&mut u_re, &mut u_im, &CNOT_RE, &CNOT_IM, n, 0, 2);
let tpm = unitary_to_tpm(&u_re, &u_im, dim);
QuantumCircuit {
name: "GHZ State".to_string(),
n_qubits: n,
description: "|000> + |111> / sqrt(2) -- genuinely multipartite entangled".to_string(),
tpm,
unitary_re: u_re,
unitary_im: u_im,
}
}
/// Product state: Identity on |000> -- no entanglement at all.
fn generate_product_state() -> QuantumCircuit {
let n = 3;
let dim = 1 << n;
let (u_re, u_im) = identity(n);
let tpm = unitary_to_tpm(&u_re, &u_im, dim);
QuantumCircuit {
name: "Product State".to_string(),
n_qubits: n,
description: "|0> x |0> x |0> -- completely separable, no entanglement".to_string(),
tpm,
unitary_re: u_re,
unitary_im: u_im,
}
}
/// W state: (|001> + |010> + |100>) / sqrt(3)
/// Constructed via specific rotation sequence.
fn generate_w_state() -> QuantumCircuit {
let n = 3;
let dim = 1 << n;
// Build the W-state preparation circuit:
// 1. Ry(arccos(1/sqrt(3))) on qubit 0
// 2. Controlled-Ry(arccos(1/sqrt(2))) on qubit 1 conditioned on qubit 0 = |1>
// 3. CNOT(1, 2)
// 4. X on qubit 0 (flip to get the right phase)
//
// Simpler approach: directly construct the unitary that maps |000> to W state
// and extend to a full unitary.
// Direct construction: we define U such that U|000> = W state.
// Build a unitary whose first column is W = [0, 1/sqrt(3), 1/sqrt(3), 0, 1/sqrt(3), 0, 0, 0]
// Use Gram-Schmidt to complete it.
let s3 = 1.0 / 3.0f64.sqrt();
let mut u_re = vec![0.0; dim * dim];
let u_im = vec![0.0; dim * dim];
// First column: W state
// |001>=1, |010>=2, |100>=4
u_re[1 * dim + 0] = s3;
u_re[2 * dim + 0] = s3;
u_re[4 * dim + 0] = s3;
// Complete the unitary with Gram-Schmidt
gram_schmidt_complete(&mut u_re, dim);
let tpm = unitary_to_tpm(&u_re, &u_im, dim);
QuantumCircuit {
name: "W State".to_string(),
n_qubits: n,
description: "|001> + |010> + |100> / sqrt(3) -- bipartite entanglement".to_string(),
tpm,
unitary_re: u_re,
unitary_im: u_im,
}
}
/// Complete a partially filled unitary matrix using Gram-Schmidt.
/// Assumes column 0 is already set; fills columns 1..dim.
fn gram_schmidt_complete(u: &mut [f64], dim: usize) {
// Start with the standard basis vectors and orthogonalize against existing columns
let mut cols_done = 1; // column 0 is already set
for candidate_col in 0..dim {
if cols_done >= dim {
break;
}
// Start with e_{candidate_col}
let mut v = vec![0.0; dim];
v[candidate_col] = 1.0;
// Subtract projections onto existing columns
for c in 0..cols_done {
let mut dot = 0.0;
for r in 0..dim {
dot += u[r * dim + c] * v[r];
}
for r in 0..dim {
v[r] -= dot * u[r * dim + c];
}
}
// Check if remaining vector is non-zero
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < 1e-10 {
continue;
}
// Normalize and store
for r in 0..dim {
u[r * dim + cols_done] = v[r] / norm;
}
cols_done += 1;
}
}
/// Random circuit: depth layers of random single-qubit rotations + CNOT gates.
fn generate_random_circuit(depth: usize) -> QuantumCircuit {
let n = 3;
let dim = 1 << n;
let (mut u_re, mut u_im) = identity(n);
let mut rng = ChaCha8Rng::seed_from_u64(42);
for _layer in 0..depth {
// Random single-qubit rotations on each qubit
for q in 0..n {
let theta = rng.gen::<f64>() * std::f64::consts::PI;
let phi = rng.gen::<f64>() * 2.0 * std::f64::consts::PI;
let (ct, st) = (theta.cos(), theta.sin());
let (cp, sp) = (phi.cos(), phi.sin());
// General rotation: Rz(phi) * Ry(theta)
let gate_re = [ct, -st, st * cp, ct * cp];
let gate_im = [0.0, 0.0, st * sp, ct * sp];
// Correct: U = [[cos(t), -sin(t)], [sin(t)*e^(i*p), cos(t)*e^(i*p)]]
// But we just need a unitary rotation, exact form is less important
// for generating random entanglement patterns.
apply_single_qubit_gate(&mut u_re, &mut u_im, &gate_re, &gate_im, n, q);
}
// CNOT between adjacent qubits
let q0 = rng.gen_range(0..n);
let q1 = (q0 + 1) % n;
apply_two_qubit_gate(&mut u_re, &mut u_im, &CNOT_RE, &CNOT_IM, n, q0, q1);
}
let tpm = unitary_to_tpm(&u_re, &u_im, dim);
QuantumCircuit {
name: "Random Circuit".to_string(),
n_qubits: n,
description: format!(
"3 qubits, depth {} -- random rotations + CNOT",
depth
),
tpm,
unitary_re: u_re,
unitary_im: u_im,
}
}

View file

@ -0,0 +1,84 @@
//! Quantum Circuit Consciousness Explorer
//!
//! Applies IIT Phi to quantum circuit measurement statistics to explore
//! the relationship between entanglement and integrated information.
mod analysis;
mod data;
mod report;
fn main() {
println!("+==========================================================+");
println!("| Quantum Circuit Consciousness Explorer -- IIT 4.0 |");
println!("| Bridging entanglement and integrated information |");
println!("+==========================================================+");
// Parse CLI args
let args: Vec<String> = std::env::args().collect();
let output = parse_str_arg(&args, "--output", "quantum_report.svg");
let depth = parse_arg(&args, "--depth", 5usize);
println!("\nConfiguration:");
println!(" Output: {}", output);
println!(" Random depth: {}", depth);
// Step 1: Generate quantum circuit TPMs
println!("\n=== Step 1: Generating Quantum Circuit Data ===");
let circuits = data::generate_all_circuits(depth);
for c in &circuits {
println!(
" {}: {} qubits, {}x{} TPM",
c.name, c.n_qubits, c.tpm_size(), c.tpm_size()
);
}
// Step 2: Run analysis
println!("\n=== Step 2: IIT Phi Analysis ===");
let results = analysis::run_quantum_analysis(&circuits);
// Step 3: Print text summary
println!("\n=== Step 3: Results Summary ===");
report::print_summary(&results);
// Step 4: Generate SVG report
let svg = report::generate_svg(&results);
std::fs::write(output, &svg).expect("Failed to write SVG report");
println!(
"\nSVG report saved to: {}",
parse_str_arg(&args, "--output", "quantum_report.svg")
);
// Entanglement hierarchy check
println!("\n+==========================================================+");
println!("| Entanglement Hierarchy vs Phi: |");
println!("| Expected: Product < W < Bell <= GHZ |");
println!("| Actual: |");
let mut sorted: Vec<_> = results
.iter()
.filter(|r| r.name != "Random Circuit")
.collect();
sorted.sort_by(|a, b| a.full_phi.partial_cmp(&b.full_phi).unwrap());
for (i, r) in sorted.iter().enumerate() {
println!(
"| {}. {:25} Phi = {:.6} |",
i + 1,
r.name,
r.full_phi
);
}
println!("+==========================================================+");
}
fn parse_arg<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
args.windows(2)
.find(|w| w[0] == name)
.and_then(|w| w[1].parse().ok())
.unwrap_or(default)
}
fn parse_str_arg<'a>(args: &'a [String], name: &str, default: &'a str) -> &'a str {
args.windows(2)
.find(|w| w[0] == name)
.map(|w| w[1].as_str())
.unwrap_or(default)
}

View file

@ -0,0 +1,309 @@
//! Report generation: text summary and SVG circuit comparison.
use crate::analysis::CircuitResult;
/// Print a text summary of all circuit results.
pub fn print_summary(results: &[CircuitResult]) {
println!("\n{:-<70}", "");
println!(
"{:<25} {:>6} {:>10} {:>10} {:>10}",
"Circuit", "Qubits", "Phi", "Emergence", "SVD Rank"
);
println!("{:-<70}", "");
for r in results {
println!(
"{:<25} {:>6} {:>10.6} {:>10.4} {:>5}/{:<4}",
r.name,
r.n_qubits,
r.full_phi,
r.emergence.causal_emergence,
r.svd_emergence.effective_rank,
r.tpm_size
);
}
println!("{:-<70}", "");
for r in results {
println!("\n=== {} ===", r.name);
println!(" Description: {}", r.description);
println!(" Qubits: {}", r.n_qubits);
println!(" TPM size: {}x{}", r.tpm_size, r.tpm_size);
println!(" Phi: {:.6} ({})", r.full_phi, r.algorithm);
println!(" EI (micro): {:.4} bits", r.emergence.ei_micro);
println!(" EI (macro): {:.4} bits", r.emergence.ei_macro);
println!(
" Causal emergence: {:.4}",
r.emergence.causal_emergence
);
println!(" Determinism: {:.4}", r.emergence.determinism);
println!(" Degeneracy: {:.4}", r.emergence.degeneracy);
println!(
" Effective rank: {}/{}",
r.svd_emergence.effective_rank, r.tpm_size
);
println!(
" Spectral entropy: {:.4}",
r.svd_emergence.spectral_entropy
);
println!(
" Emergence index: {:.4}",
r.svd_emergence.emergence_index
);
println!(
" Reversibility: {:.4}",
r.svd_emergence.reversibility
);
}
}
/// Generate a self-contained SVG report with bar charts and circuit diagrams.
pub fn generate_svg(results: &[CircuitResult]) -> String {
let width = 1200;
let total_height = 900;
let mut svg = String::with_capacity(20_000);
svg.push_str(&format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {} {}" font-family="monospace" font-size="12">
<style>
.title {{ font-size: 20px; font-weight: bold; fill: #333; }}
.subtitle {{ font-size: 14px; fill: #666; font-weight: bold; }}
.label {{ font-size: 10px; fill: #333; }}
.stat {{ font-size: 11px; fill: #444; }}
.bar-phi {{ fill: #3498db; }}
.bar-emg {{ fill: #e67e22; }}
.bar-svd {{ fill: #2ecc71; }}
</style>
<rect width="{}" height="{}" fill="white"/>
<text x="600" y="40" text-anchor="middle" class="title">Quantum Circuit Consciousness Analysis Report</text>
<text x="600" y="65" text-anchor="middle" class="stat">IIT Phi applied to quantum circuit measurement statistics</text>
"#,
width, total_height, width, total_height
));
// Panel 1: Phi comparison bar chart (top)
svg.push_str(&render_phi_bars(results, 50, 100, 500, 300));
// Panel 2: Emergence comparison (top right)
svg.push_str(&render_emergence_bars(results, 600, 100, 550, 300));
// Panel 3: Circuit descriptions and stats table (bottom)
svg.push_str(&render_stats_table(results, 50, 450, 1100, 400));
svg.push_str("</svg>\n");
svg
}
/// Render Phi comparison bar chart.
fn render_phi_bars(results: &[CircuitResult], x: i32, y: i32, w: i32, h: i32) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\" rx=\"5\"/>\n",
w, h
));
s.push_str(&format!(
"<text x=\"{}\" y=\"-8\" text-anchor=\"middle\" class=\"subtitle\">Phi Comparison</text>\n",
w / 2
));
let max_phi = results
.iter()
.map(|r| r.full_phi)
.fold(0.0f64, f64::max)
.max(1e-10);
let bar_h = ((h - 60) as f64 / results.len() as f64).min(40.0);
let margin = 10;
for (i, r) in results.iter().enumerate() {
let ry = margin + (i as f64 * (bar_h + 8.0)) as i32 + 20;
let bw = ((r.full_phi / max_phi) * (w - 200) as f64) as i32;
// Label
s.push_str(&format!(
"<text x=\"10\" y=\"{}\" class=\"label\">{}</text>\n",
ry + bar_h as i32 / 2 + 4,
r.name
));
// Bar
s.push_str(&format!(
"<rect x=\"150\" y=\"{}\" width=\"{}\" height=\"{}\" class=\"bar-phi\" rx=\"3\"/>\n",
ry,
bw.max(1),
bar_h as i32 - 4
));
// Value
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"label\">{:.6}</text>\n",
155 + bw,
ry + bar_h as i32 / 2 + 4,
r.full_phi
));
}
s.push_str("</g>\n");
s
}
/// Render emergence comparison bar chart.
fn render_emergence_bars(
results: &[CircuitResult],
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\" rx=\"5\"/>\n",
w, h
));
s.push_str(&format!(
"<text x=\"{}\" y=\"-8\" text-anchor=\"middle\" class=\"subtitle\">Emergence Comparison</text>\n",
w / 2
));
let max_ei = results
.iter()
.map(|r| r.emergence.ei_micro)
.fold(0.0f64, f64::max)
.max(1e-10);
let bar_h = ((h - 60) as f64 / results.len() as f64).min(40.0);
let margin = 10;
let half_w = (w - 200) / 2;
for (i, r) in results.iter().enumerate() {
let ry = margin + (i as f64 * (bar_h + 8.0)) as i32 + 20;
// Label
s.push_str(&format!(
"<text x=\"10\" y=\"{}\" class=\"label\">{}</text>\n",
ry + bar_h as i32 / 2 + 4,
r.name
));
// EI bar (blue)
let ei_w = ((r.emergence.ei_micro / max_ei) * half_w as f64) as i32;
s.push_str(&format!(
"<rect x=\"150\" y=\"{}\" width=\"{}\" height=\"{}\" class=\"bar-phi\" rx=\"2\" opacity=\"0.7\"/>\n",
ry,
ei_w.max(1),
(bar_h as i32 - 4) / 2
));
// Emergence bar (orange)
let emg_w = ((r.emergence.causal_emergence.abs() / max_ei) * half_w as f64) as i32;
s.push_str(&format!(
"<rect x=\"150\" y=\"{}\" width=\"{}\" height=\"{}\" class=\"bar-emg\" rx=\"2\" opacity=\"0.7\"/>\n",
ry + (bar_h as i32 - 4) / 2,
emg_w.max(1),
(bar_h as i32 - 4) / 2
));
// Values
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"label\">EI={:.3}</text>\n",
155 + ei_w,
ry + (bar_h as i32) / 4 + 3,
r.emergence.ei_micro
));
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"label\">CE={:.3}</text>\n",
155 + emg_w,
ry + 3 * (bar_h as i32) / 4 + 3,
r.emergence.causal_emergence
));
}
// Legend
let ly = h - 25;
s.push_str(&format!(
"<rect x=\"150\" y=\"{}\" width=\"12\" height=\"12\" class=\"bar-phi\" opacity=\"0.7\"/>\n\
<text x=\"167\" y=\"{}\" class=\"label\">EI (micro)</text>\n\
<rect x=\"260\" y=\"{}\" width=\"12\" height=\"12\" class=\"bar-emg\" opacity=\"0.7\"/>\n\
<text x=\"277\" y=\"{}\" class=\"label\">Causal Emergence</text>\n",
ly, ly + 10, ly, ly + 10
));
s.push_str("</g>\n");
s
}
/// Render stats table at the bottom.
fn render_stats_table(
results: &[CircuitResult],
x: i32,
y: i32,
w: i32,
h: i32,
) -> String {
let mut s = format!("<g transform=\"translate({},{})\">\n", x, y);
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"#fafafa\" stroke=\"#ddd\" rx=\"5\"/>\n",
w, h
));
s.push_str(&format!(
"<text x=\"{}\" y=\"-8\" text-anchor=\"middle\" class=\"subtitle\">Detailed Results</text>\n",
w / 2
));
// Header
let cols = [15, 180, 260, 380, 500, 620, 740, 870, 990];
let headers = [
"Circuit", "Qubits", "Phi", "Algorithm", "EI_micro",
"Emergence", "SVD Rank", "Emg Index", "Reversibility",
];
for (col, hdr) in cols.iter().zip(headers.iter()) {
s.push_str(&format!(
"<text x=\"{}\" y=\"25\" class=\"stat\" font-weight=\"bold\">{}</text>\n",
col, hdr
));
}
s.push_str(&format!(
"<line x1=\"10\" y1=\"32\" x2=\"{}\" y2=\"32\" stroke=\"#ccc\"/>\n",
w - 10
));
for (i, r) in results.iter().enumerate() {
let ry = 50 + i as i32 * 22;
let vals = [
r.name.clone(),
format!("{}", r.n_qubits),
format!("{:.6}", r.full_phi),
r.algorithm.clone(),
format!("{:.4}", r.emergence.ei_micro),
format!("{:.4}", r.emergence.causal_emergence),
format!("{}/{}", r.svd_emergence.effective_rank, r.tpm_size),
format!("{:.4}", r.svd_emergence.emergence_index),
format!("{:.4}", r.svd_emergence.reversibility),
];
for (col, val) in cols.iter().zip(vals.iter()) {
s.push_str(&format!(
"<text x=\"{}\" y=\"{}\" class=\"stat\">{}</text>\n",
col, ry, val
));
}
}
// Description section
let desc_y = 50 + results.len() as i32 * 22 + 30;
s.push_str(&format!(
"<text x=\"15\" y=\"{}\" class=\"subtitle\">Circuit Descriptions</text>\n",
desc_y
));
for (i, r) in results.iter().enumerate() {
s.push_str(&format!(
"<text x=\"15\" y=\"{}\" class=\"stat\">{}: {}</text>\n",
desc_y + 20 + i as i32 * 18,
r.name,
r.description
));
}
s.push_str("</g>\n");
s
}