use crate::arena::PhiArena;
use crate::error::ConsciousnessError;
use crate::phi::{partition_information_loss_pub, validate_tpm};
use crate::simd::emd_l1;
use crate::traits::PhiEngine;
use crate::types::{Bipartition, ComputeBudget, PhiAlgorithm, PhiResult, TransitionMatrix};
use std::time::Instant;
pub struct GrayCodePartitionIter {
current: u64,
counter: u64,
max: u64,
n: usize,
}
impl GrayCodePartitionIter {
pub fn new(n: usize) -> Self {
assert!((2..=63).contains(&n));
Self {
current: 0,
counter: 1, max: 1u64 << (n - 1),
n,
}
}
#[inline]
pub fn changed_bit(prev_gray: u64, curr_gray: u64) -> u32 {
(prev_gray ^ curr_gray).trailing_zeros()
}
}
impl Iterator for GrayCodePartitionIter {
type Item = (Bipartition, u32);
fn next(&mut self) -> Option<Self::Item> {
if self.counter >= self.max {
return None;
}
let prev_gray = self.current;
let gray = self.counter ^ (self.counter >> 1);
self.current = gray;
self.counter += 1;
let mask = 1u64 | (gray << 1);
let full = (1u64 << self.n) - 1;
if mask == 0 || mask == full {
return self.next(); }
let changed = if prev_gray == 0 {
0 } else {
Self::changed_bit(prev_gray, gray) + 1 };
Some((Bipartition { mask, n: self.n }, changed))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.max - self.counter) as usize;
(remaining, Some(remaining))
}
}
fn canonical_partition(mask: u64, n: usize) -> u64 {
let popcount = mask.count_ones();
let complement_popcount = n as u32 - popcount;
if popcount > complement_popcount {
let full = (1u64 << n) - 1;
full & !mask
} else if popcount == complement_popcount {
let full = (1u64 << n) - 1;
mask.min(full & !mask)
} else {
mask
}
}
#[inline]
fn balance_score(mask: u64, n: usize) -> f64 {
let k = mask.count_ones() as f64;
let half = n as f64 / 2.0;
1.0 - ((k - half).abs() / half)
}
pub struct GeoMipPhiEngine {
pub prune_automorphisms: bool,
pub max_evaluations: u64,
}
impl GeoMipPhiEngine {
pub fn new(prune_automorphisms: bool, max_evaluations: u64) -> Self {
Self {
prune_automorphisms,
max_evaluations,
}
}
}
impl Default for GeoMipPhiEngine {
fn default() -> Self {
Self {
prune_automorphisms: true,
max_evaluations: 0,
}
}
}
impl PhiEngine for GeoMipPhiEngine {
fn compute_phi(
&self,
tpm: &TransitionMatrix,
state: Option<usize>,
budget: &ComputeBudget,
) -> Result<PhiResult, ConsciousnessError> {
validate_tpm(tpm)?;
let n = tpm.n;
if n > 25 {
return Err(ConsciousnessError::SystemTooLarge { n, max: 25 });
}
let state_idx = state.unwrap_or(0);
let start = Instant::now();
let arena = PhiArena::with_capacity(n * n * 16);
let total_partitions = (1u64 << n) - 2;
let mut min_phi = f64::MAX;
let mut best_partition = Bipartition { mask: 1, n };
let mut evaluated = 0u64;
let mut convergence = Vec::new();
let mut balanced_partitions: Vec<Bipartition> = Vec::new();
let half = n / 2;
for mask in 1..((1u64 << n) - 1) {
let popcount = mask.count_ones() as usize;
if (popcount == half || popcount == half + 1)
&& (!self.prune_automorphisms
|| canonical_partition(mask, n) == mask)
{
balanced_partitions.push(Bipartition { mask, n });
}
}
balanced_partitions
.sort_by(|a, b| balance_score(b.mask, n).partial_cmp(&balance_score(a.mask, n)).unwrap());
for partition in &balanced_partitions {
if self.max_evaluations > 0 && evaluated >= self.max_evaluations {
break;
}
if budget.max_partitions > 0 && evaluated >= budget.max_partitions {
break;
}
if start.elapsed() > budget.max_time {
break;
}
let loss = partition_information_loss_pub(tpm, state_idx, partition, &arena);
arena.reset();
if loss < min_phi {
min_phi = loss;
best_partition = partition.clone();
}
if min_phi < 1e-12 {
evaluated += 1;
break;
}
evaluated += 1;
if evaluated % 500 == 0 {
convergence.push(min_phi);
}
}
if min_phi > 1e-12 {
let mut seen = std::collections::HashSet::new();
for bp in &balanced_partitions {
seen.insert(bp.mask);
}
for (partition, _changed_bit) in GrayCodePartitionIter::new(n) {
if self.max_evaluations > 0 && evaluated >= self.max_evaluations {
break;
}
if budget.max_partitions > 0 && evaluated >= budget.max_partitions {
break;
}
if start.elapsed() > budget.max_time {
break;
}
if seen.contains(&partition.mask) {
continue;
}
if self.prune_automorphisms {
let canon = canonical_partition(partition.mask, n);
if canon != partition.mask && seen.contains(&canon) {
continue;
}
seen.insert(partition.mask);
}
let loss = partition_information_loss_pub(tpm, state_idx, &partition, &arena);
arena.reset();
if loss < min_phi {
min_phi = loss;
best_partition = partition;
}
if min_phi < 1e-12 {
evaluated += 1;
break;
}
evaluated += 1;
if evaluated % 500 == 0 {
convergence.push(min_phi);
}
}
}
convergence.push(min_phi);
Ok(PhiResult {
phi: if min_phi == f64::MAX { 0.0 } else { min_phi },
mip: best_partition,
partitions_evaluated: evaluated,
total_partitions,
algorithm: PhiAlgorithm::GeoMIP,
elapsed: start.elapsed(),
convergence,
})
}
fn algorithm(&self) -> PhiAlgorithm {
PhiAlgorithm::GeoMIP
}
fn estimate_cost(&self, n: usize) -> u64 {
((1u64 << n) - 2) / 2
}
}
pub fn partition_information_loss_emd(
tpm: &TransitionMatrix,
state: usize,
partition: &Bipartition,
arena: &PhiArena,
) -> f64 {
let n = tpm.n;
let set_a = partition.set_a();
let set_b = partition.set_b();
let whole_dist = &tpm.data[state * n..(state + 1) * n];
let tpm_a = tpm.marginalize(&set_a);
let tpm_b = tpm.marginalize(&set_b);
let state_a = map_state_to_subsystem_local(state, &set_a);
let state_b = map_state_to_subsystem_local(state, &set_b);
let dist_a = &tpm_a.data[state_a * tpm_a.n..(state_a + 1) * tpm_a.n];
let dist_b = &tpm_b.data[state_b * tpm_b.n..(state_b + 1) * tpm_b.n];
let product = arena.alloc_slice::<f64>(n);
compute_product_local(dist_a, &set_a, dist_b, &set_b, product, n);
let sum: f64 = product.iter().sum();
if sum > 1e-15 {
let inv_sum = 1.0 / sum;
for p in product.iter_mut() {
*p *= inv_sum;
}
}
let loss = emd_l1(whole_dist, product).max(0.0);
arena.reset();
loss
}
fn map_state_to_subsystem_local(state: usize, indices: &[usize]) -> usize {
let mut sub_state = 0;
for (bit, &idx) in indices.iter().enumerate() {
if state & (1 << idx) != 0 {
sub_state |= 1 << bit;
}
}
sub_state % indices.len().max(1)
}
fn compute_product_local(
dist_a: &[f64],
set_a: &[usize],
dist_b: &[f64],
set_b: &[usize],
output: &mut [f64],
n: usize,
) {
let ka = set_a.len();
let kb = set_b.len();
for global_state in 0..n {
let mut sa = 0usize;
for (bit, &idx) in set_a.iter().enumerate() {
if global_state & (1 << idx) != 0 {
sa |= 1 << bit;
}
}
let mut sb = 0usize;
for (bit, &idx) in set_b.iter().enumerate() {
if global_state & (1 << idx) != 0 {
sb |= 1 << bit;
}
}
let pa = if sa < ka { dist_a[sa] } else { 0.0 };
let pb = if sb < kb { dist_b[sb] } else { 0.0 };
output[global_state] = pa * pb;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn and_gate_tpm() -> TransitionMatrix {
#[rustfmt::skip]
let data = vec![
0.5, 0.25, 0.25, 0.0,
0.5, 0.25, 0.25, 0.0,
0.5, 0.25, 0.25, 0.0,
0.0, 0.0, 0.0, 1.0,
];
TransitionMatrix::new(4, data)
}
fn disconnected_tpm() -> TransitionMatrix {
#[rustfmt::skip]
let data = vec![
0.5, 0.5, 0.0, 0.0,
0.5, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.5, 0.5,
];
TransitionMatrix::new(4, data)
}
#[test]
fn gray_code_iter_count() {
let count = GrayCodePartitionIter::new(4).count();
assert_eq!(count, 6);
}
#[test]
fn gray_code_consecutive_differ_by_one() {
let partitions: Vec<(Bipartition, u32)> = GrayCodePartitionIter::new(5).collect();
for i in 1..partitions.len() {
let diff = partitions[i].0.mask ^ partitions[i - 1].0.mask;
assert!(
diff.count_ones() <= 2,
"Gray code partitions at {i} differ by {} bits",
diff.count_ones()
);
}
}
#[test]
fn canonical_partition_symmetric() {
let c1 = canonical_partition(0b0011, 4);
let c2 = canonical_partition(0b1100, 4);
assert_eq!(c1, c2);
}
#[test]
fn geomip_disconnected_is_zero() {
let tpm = disconnected_tpm();
let budget = ComputeBudget::exact();
let engine = GeoMipPhiEngine::default();
let result = engine.compute_phi(&tpm, Some(0), &budget).unwrap();
assert!(
result.phi < 1e-6,
"disconnected should have Φ ≈ 0, got {}",
result.phi
);
}
#[test]
fn geomip_and_gate() {
let tpm = and_gate_tpm();
let budget = ComputeBudget::exact();
let engine = GeoMipPhiEngine::default();
let result = engine.compute_phi(&tpm, Some(3), &budget).unwrap();
assert!(result.phi >= 0.0);
}
#[test]
fn geomip_fewer_evaluations_than_exact() {
let tpm = and_gate_tpm();
let budget = ComputeBudget::exact();
let exact_result =
crate::phi::ExactPhiEngine.compute_phi(&tpm, Some(0), &budget).unwrap();
let geomip_result =
GeoMipPhiEngine::default().compute_phi(&tpm, Some(0), &budget).unwrap();
assert!(
geomip_result.partitions_evaluated <= exact_result.partitions_evaluated,
"GeoMIP evaluated {} vs exact {}",
geomip_result.partitions_evaluated,
exact_result.partitions_evaluated
);
}
#[test]
fn emd_loss_nonnegative() {
let tpm = and_gate_tpm();
let partition = Bipartition { mask: 0b0011, n: 4 };
let arena = PhiArena::with_capacity(1024);
let loss = partition_information_loss_emd(&tpm, 0, &partition, &arena);
assert!(loss >= 0.0, "EMD loss should be ≥ 0, got {loss}");
}
#[test]
fn emd_loss_disconnected_zero() {
let tpm = disconnected_tpm();
let partition = Bipartition {
mask: 0b0011,
n: 4,
};
let arena = PhiArena::with_capacity(1024);
let loss = partition_information_loss_emd(&tpm, 0, &partition, &arena);
assert!(loss < 1e-6, "disconnected EMD loss should be ≈ 0, got {loss}");
}
}