ruvector_consciousness/
emergence.rs1use crate::error::{ConsciousnessError, ValidationError};
16use crate::simd::{entropy, kl_divergence};
17use crate::traits::EmergenceEngine;
18use crate::types::{ComputeBudget, EmergenceResult, TransitionMatrix};
19
20use std::time::Instant;
21
22pub fn effective_information(tpm: &TransitionMatrix) -> Result<f64, ConsciousnessError> {
37 let n = tpm.n;
38 if n < 2 {
39 return Err(ValidationError::EmptySystem.into());
40 }
41
42 let uniform: Vec<f64> = vec![1.0 / n as f64; n];
43 let mut ei_sum = 0.0f64;
44
45 for state in 0..n {
46 let row = &tpm.data[state * n..(state + 1) * n];
47 ei_sum += kl_divergence(row, &uniform);
48 }
49
50 Ok(ei_sum / n as f64)
51}
52
53pub fn determinism(tpm: &TransitionMatrix) -> f64 {
58 let n = tpm.n;
59 let h_max = (n as f64).ln();
60 let mut avg_entropy = 0.0f64;
61
62 for state in 0..n {
63 let row = &tpm.data[state * n..(state + 1) * n];
64 avg_entropy += entropy(row);
65 }
66 avg_entropy /= n as f64;
67
68 h_max - avg_entropy
69}
70
71pub fn degeneracy(tpm: &TransitionMatrix) -> f64 {
75 let n = tpm.n;
76 let h_max = (n as f64).ln();
77
78 let mut marginal = vec![0.0f64; n];
80 for state in 0..n {
81 for j in 0..n {
82 marginal[j] += tpm.get(state, j);
83 }
84 }
85 let inv_n = 1.0 / n as f64;
86 for m in &mut marginal {
87 *m *= inv_n;
88 }
89
90 h_max - entropy(&marginal)
91}
92
93pub fn coarse_grain(tpm: &TransitionMatrix, mapping: &[usize]) -> TransitionMatrix {
102 assert_eq!(mapping.len(), tpm.n);
103
104 let n_macro = mapping.iter().copied().max().unwrap_or(0) + 1;
105 let mut macro_tpm = vec![0.0f64; n_macro * n_macro];
106 let mut counts = vec![0usize; n_macro]; for micro_from in 0..tpm.n {
109 let macro_from = mapping[micro_from];
110 counts[macro_from] += 1;
111
112 for micro_to in 0..tpm.n {
113 let macro_to = mapping[micro_to];
114 macro_tpm[macro_from * n_macro + macro_to] += tpm.get(micro_from, micro_to);
115 }
116 }
117
118 for macro_from in 0..n_macro {
120 if counts[macro_from] > 0 {
121 let inv = 1.0 / counts[macro_from] as f64;
122 for macro_to in 0..n_macro {
123 macro_tpm[macro_from * n_macro + macro_to] *= inv;
124 }
125 }
126 }
127
128 TransitionMatrix::new(n_macro, macro_tpm)
129}
130
131pub struct CausalEmergenceEngine {
138 max_macro_states: usize,
139}
140
141impl CausalEmergenceEngine {
142 pub fn new(max_macro_states: usize) -> Self {
143 Self { max_macro_states }
144 }
145}
146
147impl Default for CausalEmergenceEngine {
148 fn default() -> Self {
149 Self {
150 max_macro_states: 16,
151 }
152 }
153}
154
155impl EmergenceEngine for CausalEmergenceEngine {
156 fn compute_emergence(
157 &self,
158 tpm: &TransitionMatrix,
159 budget: &ComputeBudget,
160 ) -> Result<EmergenceResult, ConsciousnessError> {
161 let start = Instant::now();
162 let n = tpm.n;
163
164 if n < 2 {
165 return Err(ValidationError::EmptySystem.into());
166 }
167
168 let ei_micro = effective_information(tpm)?;
170 let det_micro = determinism(tpm);
171 let deg_micro = degeneracy(tpm);
172
173 let mut best_ei_macro = ei_micro;
175 let mut best_mapping: Vec<usize> = (0..n).collect(); let min_k = 2.max(self.max_macro_states.min(n));
179 for target_k in (2..n).rev() {
180 if target_k < min_k && best_ei_macro > ei_micro {
181 break; }
183 if start.elapsed() > budget.max_time {
184 break;
185 }
186
187 let mapping = greedy_merge(tpm, target_k);
189 let macro_tpm = coarse_grain(tpm, &mapping);
190
191 if let Ok(ei) = effective_information(¯o_tpm) {
192 if ei > best_ei_macro {
193 best_ei_macro = ei;
194 best_mapping = mapping;
195 }
196 }
197 }
198
199 Ok(EmergenceResult {
200 ei_micro,
201 ei_macro: best_ei_macro,
202 causal_emergence: best_ei_macro - ei_micro,
203 coarse_graining: best_mapping,
204 determinism: det_micro,
205 degeneracy: deg_micro,
206 elapsed: start.elapsed(),
207 })
208 }
209
210 fn effective_information(
211 &self,
212 tpm: &TransitionMatrix,
213 ) -> Result<f64, ConsciousnessError> {
214 effective_information(tpm)
215 }
216}
217
218fn greedy_merge(tpm: &TransitionMatrix, target_k: usize) -> Vec<usize> {
221 let n = tpm.n;
222 let mut mapping: Vec<usize> = (0..n).collect();
223 let mut current_k = n;
224
225 while current_k > target_k {
226 let mut best_dist = f64::MAX;
228 let mut best_i = 0;
229 let mut best_j = 1;
230
231 let macro_ids: Vec<usize> = {
232 let mut ids: Vec<usize> = mapping.to_vec();
233 ids.sort_unstable();
234 ids.dedup();
235 ids
236 };
237
238 for (ai, &mi) in macro_ids.iter().enumerate() {
239 for &mj in macro_ids[ai + 1..].iter() {
240 let dist = state_distribution_distance(tpm, &mapping, mi, mj);
242 if dist < best_dist {
243 best_dist = dist;
244 best_i = mi;
245 best_j = mj;
246 }
247 }
248 }
249
250 for m in &mut mapping {
252 if *m == best_j {
253 *m = best_i;
254 }
255 }
256
257 let mut unique: Vec<usize> = mapping.to_vec();
259 unique.sort_unstable();
260 unique.dedup();
261 for m in &mut mapping {
262 *m = unique.iter().position(|&u| u == *m).unwrap();
263 }
264
265 current_k = unique.len();
266 }
267
268 mapping
269}
270
271fn state_distribution_distance(
273 tpm: &TransitionMatrix,
274 mapping: &[usize],
275 macro_a: usize,
276 macro_b: usize,
277) -> f64 {
278 let n = tpm.n;
279 let mut avg_a = vec![0.0f64; n];
280 let mut avg_b = vec![0.0f64; n];
281 let mut count_a = 0usize;
282 let mut count_b = 0usize;
283
284 for micro in 0..n {
285 if mapping[micro] == macro_a {
286 for j in 0..n {
287 avg_a[j] += tpm.get(micro, j);
288 }
289 count_a += 1;
290 } else if mapping[micro] == macro_b {
291 for j in 0..n {
292 avg_b[j] += tpm.get(micro, j);
293 }
294 count_b += 1;
295 }
296 }
297
298 if count_a > 0 {
299 let inv = 1.0 / count_a as f64;
300 for a in &mut avg_a {
301 *a *= inv;
302 }
303 }
304 if count_b > 0 {
305 let inv = 1.0 / count_b as f64;
306 for b in &mut avg_b {
307 *b *= inv;
308 }
309 }
310
311 let mut dist = 0.0f64;
313 for j in 0..n {
314 let d = avg_a[j] - avg_b[j];
315 dist += d * d;
316 }
317 dist.sqrt()
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 fn identity_tpm(n: usize) -> TransitionMatrix {
325 TransitionMatrix::identity(n)
326 }
327
328 fn uniform_tpm(n: usize) -> TransitionMatrix {
329 let val = 1.0 / n as f64;
330 let data = vec![val; n * n];
331 TransitionMatrix::new(n, data)
332 }
333
334 #[test]
335 fn ei_identity_is_max() {
336 let tpm = identity_tpm(4);
338 let ei = effective_information(&tpm).unwrap();
339 let h_max = (4.0f64).ln();
340 assert!(
341 (ei - h_max).abs() < 1e-6,
342 "identity TPM should have EI = log(n), got {ei}"
343 );
344 }
345
346 #[test]
347 fn ei_uniform_is_zero() {
348 let tpm = uniform_tpm(4);
350 let ei = effective_information(&tpm).unwrap();
351 assert!(ei.abs() < 1e-6, "uniform TPM should have EI ≈ 0, got {ei}");
352 }
353
354 #[test]
355 fn determinism_identity_is_max() {
356 let tpm = identity_tpm(4);
357 let det = determinism(&tpm);
358 let h_max = (4.0f64).ln();
359 assert!((det - h_max).abs() < 1e-6);
360 }
361
362 #[test]
363 fn degeneracy_identity_is_zero() {
364 let tpm = identity_tpm(4);
366 let deg = degeneracy(&tpm);
367 assert!(deg.abs() < 1e-6, "identity TPM degeneracy should be 0, got {deg}");
368 }
369
370 #[test]
371 fn coarse_grain_identity() {
372 let tpm = identity_tpm(4);
373 let mapping = vec![0, 0, 1, 1]; let macro_tpm = coarse_grain(&tpm, &mapping);
375 assert_eq!(macro_tpm.n, 2);
376 assert!((macro_tpm.get(0, 0) - 1.0).abs() < 1e-6);
379 }
380
381 #[test]
382 fn causal_emergence_runs() {
383 let tpm = identity_tpm(4);
384 let budget = ComputeBudget::fast();
385 let engine = CausalEmergenceEngine::default();
386 let result = engine.compute_emergence(&tpm, &budget).unwrap();
387 assert!(result.ei_micro >= 0.0);
388 assert!(result.causal_emergence.is_finite());
389 }
390}