1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use shivya_morphic::DynamicGibbsAgent;
use crate::harsanyi::LocalCoalitionSolver;
use crate::field::OnsagerFlowRegulator;
pub struct OnsagerCollectiveEnsemble {
pub agents: Vec<DynamicGibbsAgent>,
// Adjacency lists: adjacent_nodes[i] lists neighbor indexes of agent i
pub adjacent_nodes: Vec<Vec<usize>>,
pub regulator: OnsagerFlowRegulator,
}
impl OnsagerCollectiveEnsemble {
pub fn new(
agents: Vec<DynamicGibbsAgent>,
adjacent_nodes: Vec<Vec<usize>>,
base_coupling: f64,
) -> Self {
let num_nodes = agents.len();
let regulator = OnsagerFlowRegulator::new(num_nodes, base_coupling);
Self {
agents,
adjacent_nodes,
regulator,
}
}
// Exec one step of concurrent belief updating, parameter migration, and return F_collective
pub fn step(
&mut self,
observations: &[Vec<f64>],
lr: f64,
max_iters: usize,
tol: f64,
flow_rate: f64,
) -> f64 {
let n = self.agents.len();
let mut free_energies = vec![0.0; n];
// 1. Concurrently update agent beliefs based on local sensory observations
for i in 0..n {
free_energies[i] = self.agents[i].update_beliefs(&observations[i], lr, max_iters, tol);
}
// 2. Perform Onsager reciprocal parameter migration
let mut beliefs: Vec<Vec<f64>> = self.agents.iter().map(|a| a.mu_q.clone()).collect();
let flows = self.regulator.compute_parameter_flows(&beliefs);
self.regulator.apply_parameter_migration(&mut beliefs, &flows, flow_rate);
// Write the updated/migrated beliefs back to agents
for i in 0..n {
self.agents[i].mu_q = beliefs[i].clone();
}
// 3. Compute Collective Free Energy incorporating Harsanyi Dividends
let total_f = free_energies.iter().sum::<f64>();
let mut sum_dividends = 0.0;
for i in 0..n {
let solver = LocalCoalitionSolver::new(i, self.adjacent_nodes[i].clone());
// Define local coalitional value V(C)
// If nodes inside coalition are synergistic (close beliefs), they yield positive value.
// If they are antagonistic (divergent beliefs), they yield negative value (repulsion).
let val_fn = |mask: u8| -> f64 {
let nodes = solver.mask_to_nodes(mask);
if nodes.len() < 2 {
return 0.0;
}
let mut synergy = 0.0;
for idx_a in 0..nodes.len() {
for idx_b in (idx_a + 1)..nodes.len() {
let node_a = nodes[idx_a];
let node_b = nodes[idx_b];
let b_a = &beliefs[node_a];
let b_b = &beliefs[node_b];
// Compute squared distance between belief coordinates
let len = b_a.len().min(b_b.len());
let dist_sq: f64 = b_a.iter().zip(b_b.iter())
.take(len)
.map(|(&x, &y)| (x - y).powi(2))
.sum();
// Synergistic threshold of 1.0. If dist < 1.0, positive synergy. Else, negative.
if dist_sq < 1.0 {
synergy += 2.0 * (1.0 - dist_sq);
} else {
// Antagonistic repulsion scaling
synergy -= 4.0 * (dist_sq - 1.0);
}
}
}
synergy
};
let dividends = solver.calculate_dividends(val_fn);
for (mask, div) in dividends {
// Focus on cooperative interaction coalitions of size >= 2
if mask.count_ones() >= 2 {
sum_dividends += div;
}
}
}
// F_collective = Sum(F_i) - Sum(Cooperative Dividends)
// Highly synergistic systems subtract dividends (lower F_collective).
// Antagonistic systems add penalty (raise F_collective).
total_f - sum_dividends
}
}
#[cfg(test)]
mod tests {
use super::*;
use shivya_morphic::DynamicGibbsAgent;
fn create_agent(initial_belief: f64) -> DynamicGibbsAgent {
DynamicGibbsAgent::new(
2, 1, 2,
vec![initial_belief, 0.0], // mu_prior
vec![vec![10.0, 0.0], vec![0.0, 10.0]], // sigma_prior
vec![vec![1.0, 0.0], vec![0.0, 1.0]], // g_s
vec![vec![0.1, 0.0], vec![0.0, 0.1]], // sigma_s_0
vec![vec![0.0], vec![0.0]], // w
vec![vec![0.0], vec![0.0]], // m
vec![0.0, 0.0], // mu_pref
vec![vec![1.0, 0.0], vec![0.0, 1.0]], // sigma_pref
5.0, // tau_novelty threshold
)
}
#[test]
fn test_synergy_vs_antagonism_energy_dynamics() {
// --- CASE 1: Synergistic Coalition (agents have identical/close beliefs) ---
let mut synergistic_ensemble = OnsagerCollectiveEnsemble::new(
vec![create_agent(0.1), create_agent(0.1)],
vec![vec![1], vec![0]], // Bidirectional adjacent connection
0.5,
);
let obs = vec![vec![0.1, 0.0], vec![0.1, 0.0]];
let f_synergy = synergistic_ensemble.step(&obs, 0.1, 10, 1e-4, 0.1);
// --- CASE 2: Antagonistic Coalition (agents have highly divergent beliefs) ---
let mut antagonistic_ensemble = OnsagerCollectiveEnsemble::new(
vec![create_agent(0.1), create_agent(5.0)],
vec![vec![1], vec![0]], // Connection
0.5,
);
let obs_antag = vec![vec![0.1, 0.0], vec![5.0, 0.0]];
let f_antag = antagonistic_ensemble.step(&obs_antag, 0.1, 10, 1e-4, 0.1);
// Collective Free Energy for Case 2 should be significantly higher due to negative Harsanyi dividends / repulsion penalty!
println!("F_synergy: {}, F_antagonism: {}", f_synergy, f_antag);
assert!(f_antag > f_synergy, "Antagonistic system interaction must increase collective energy (repulsion), while synergistic interaction minimizes it.");
}
}