Skip to main content

renormalization_group/
block_spin.rs

1//! # Block-Spin Coarse-Graining Transformations
2//!
3//! Group nearby agents into blocks and compute block properties
4//! (mean belief, aggregate constraint). This is the fundamental
5//! RG operation: integrating out short-scale degrees of freedom.
6
7use serde::{Deserialize, Serialize};
8
9/// Represents a single agent in the fleet.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Agent {
12    /// Unique identifier
13    pub id: usize,
14    /// Position in space (1D for simplicity, generalizable)
15    pub position: Vec<f64>,
16    /// Agent's "spin" or belief state (continuous)
17    pub belief: f64,
18    /// Local constraint value (e.g., resource budget, bandwidth)
19    pub constraint: f64,
20}
21
22/// A block of agents after coarse-graining.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Block {
25    /// Block identifier
26    pub id: usize,
27    /// Constituent agent IDs
28    pub agents: Vec<usize>,
29    /// Center of mass position
30    pub center: Vec<f64>,
31    /// Mean belief (magnetization analog)
32    pub mean_belief: f64,
33    /// Aggregate constraint
34    pub aggregate_constraint: f64,
35    /// Number of agents in block
36    pub size: usize,
37}
38
39/// Configuration for a block transformation.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BlockTransform {
42    /// Current scale level
43    pub scale: usize,
44    /// Number of agents per block
45    pub block_size: usize,
46}
47
48/// Strategy for grouping agents into blocks.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum BlockingStrategy {
51    /// Sequential grouping: agents 0..b, b..2b, etc.
52    Sequential,
53    /// Spatial grouping based on nearest neighbors
54    SpatialNearest,
55    /// Majority rule: block spin = sign of mean belief
56    MajorityRule,
57}
58
59impl BlockTransform {
60    /// Create a new block transform at given scale with given block size.
61    pub fn new(scale: usize, block_size: usize) -> Self {
62        Self { scale, block_size }
63    }
64
65    /// Apply the block transformation to a set of agents.
66    /// Returns the coarse-grained blocks.
67    pub fn transform(&self, agents: &[Agent], strategy: BlockingStrategy) -> Vec<Block> {
68        match strategy {
69            BlockingStrategy::Sequential => self.sequential_blocking(agents),
70            BlockingStrategy::SpatialNearest => self.spatial_blocking(agents),
71            BlockingStrategy::MajorityRule => self.majority_rule_blocking(agents),
72        }
73    }
74
75    fn sequential_blocking(&self, agents: &[Agent]) -> Vec<Block> {
76        let b = self.block_size.max(1);
77        let n = agents.len();
78        let mut blocks = Vec::new();
79        let mut block_id = 0;
80
81        let mut i = 0;
82        while i < n {
83            let end = (i + b).min(n);
84            let block_agents: Vec<usize> = (i..end).map(|j| agents[j].id).collect();
85            let count = end - i;
86
87            let mean_belief: f64 = (i..end).map(|j| agents[j].belief).sum::<f64>() / count as f64;
88            let agg_constraint: f64 = (i..end).map(|j| agents[j].constraint).sum();
89
90            let ndim = agents[i].position.len();
91            let center: Vec<f64> = (0..ndim)
92                .map(|d| (i..end).map(|j| agents[j].position[d]).sum::<f64>() / count as f64)
93                .collect();
94
95            blocks.push(Block {
96                id: block_id,
97                agents: block_agents,
98                center,
99                mean_belief,
100                aggregate_constraint: agg_constraint,
101                size: count,
102            });
103            block_id += 1;
104            i += b;
105        }
106        blocks
107    }
108
109    fn spatial_blocking(&self, agents: &[Agent]) -> Vec<Block> {
110        // For 1D: sort by position, then sequential block
111        let mut indexed: Vec<usize> = (0..agents.len()).collect();
112        indexed.sort_by(|&a, &b| {
113            let pa = &agents[a].position;
114            let pb = &agents[b].position;
115            // Lexicographic comparison
116            for i in 0..pa.len().min(pb.len()) {
117                let cmp = pa[i].partial_cmp(&pb[i]).unwrap_or(std::cmp::Ordering::Equal);
118                if cmp != std::cmp::Ordering::Equal {
119                    return cmp;
120                }
121            }
122            std::cmp::Ordering::Equal
123        });
124
125        let sorted_agents: Vec<Agent> = indexed.iter().map(|&i| agents[i].clone()).collect();
126        let b = self.block_size.max(1);
127        let n = sorted_agents.len();
128        let mut blocks = Vec::new();
129        let mut block_id = 0;
130
131        let mut i = 0;
132        while i < n {
133            let end = (i + b).min(n);
134            let count = end - i;
135            let block_agents: Vec<usize> = (i..end).map(|j| sorted_agents[j].id).collect();
136
137            let mean_belief: f64 = (i..end).map(|j| sorted_agents[j].belief).sum::<f64>() / count as f64;
138            let agg_constraint: f64 = (i..end).map(|j| sorted_agents[j].constraint).sum();
139
140            let ndim = sorted_agents[i].position.len();
141            let center: Vec<f64> = (0..ndim)
142                .map(|d| (i..end).map(|j| sorted_agents[j].position[d]).sum::<f64>() / count as f64)
143                .collect();
144
145            blocks.push(Block {
146                id: block_id,
147                agents: block_agents,
148                center,
149                mean_belief,
150                aggregate_constraint: agg_constraint,
151                size: count,
152            });
153            block_id += 1;
154            i += b;
155        }
156        blocks
157    }
158
159    fn majority_rule_blocking(&self, agents: &[Agent]) -> Vec<Block> {
160        let b = self.block_size.max(1);
161        let n = agents.len();
162        let mut blocks = Vec::new();
163        let mut block_id = 0;
164
165        let mut i = 0;
166        while i < n {
167            let end = (i + b).min(n);
168            let count = end - i;
169            let block_agents: Vec<usize> = (i..end).map(|j| agents[j].id).collect();
170
171            let mean_belief: f64 = (i..end).map(|j| agents[j].belief).sum::<f64>() / count as f64;
172            // Majority rule: block spin is ±1
173            let block_spin = if mean_belief >= 0.0 { 1.0 } else { -1.0 };
174            let agg_constraint: f64 = (i..end).map(|j| agents[j].constraint).sum();
175
176            let ndim = agents[i].position.len();
177            let center: Vec<f64> = (0..ndim)
178                .map(|d| (i..end).map(|j| agents[j].position[d]).sum::<f64>() / count as f64)
179                .collect();
180
181            blocks.push(Block {
182                id: block_id,
183                agents: block_agents,
184                center,
185                mean_belief: block_spin,
186                aggregate_constraint: agg_constraint,
187                size: count,
188            });
189            block_id += 1;
190            i += b;
191        }
192        blocks
193    }
194}
195
196/// Convert blocks back into agents for the next RG step.
197/// Each block becomes a single agent with the block's properties.
198pub fn blocks_to_agents(blocks: &[Block]) -> Vec<Agent> {
199    blocks
200        .iter()
201        .map(|b| Agent {
202            id: b.id,
203            position: b.center.clone(),
204            belief: b.mean_belief,
205            constraint: b.aggregate_constraint,
206        })
207        .collect()
208}
209
210/// Compute the total magnetization (mean belief) of a set of agents.
211pub fn magnetization(agents: &[Agent]) -> f64 {
212    if agents.is_empty() {
213        return 0.0;
214    }
215    agents.iter().map(|a| a.belief).sum::<f64>() / agents.len() as f64
216}
217
218/// Compute the total constraint (sum of all agent constraints).
219pub fn total_constraint(agents: &[Agent]) -> f64 {
220    agents.iter().map(|a| a.constraint).sum()
221}
222
223/// Compute the energy of a 1D nearest-neighbor fleet (Ising-like).
224/// E = -J Σ s_i s_{i+1} - h Σ s_i
225pub fn ising_energy(agents: &[Agent], j_coupling: f64, h_field: f64) -> f64 {
226    let n = agents.len();
227    if n == 0 {
228        return 0.0;
229    }
230    let mut energy = 0.0;
231    for i in 0..n {
232        energy -= h_field * agents[i].belief;
233        if i + 1 < n {
234            energy -= j_coupling * agents[i].belief * agents[i + 1].belief;
235        }
236    }
237    energy
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    fn make_uniform_agents(n: usize, belief: f64, constraint: f64) -> Vec<Agent> {
245        (0..n)
246            .map(|i| Agent {
247                id: i,
248                position: vec![i as f64],
249                belief,
250                constraint,
251            })
252            .collect()
253    }
254
255    fn make_alternating_agents(n: usize) -> Vec<Agent> {
256        (0..n)
257            .map(|i| Agent {
258                id: i,
259                position: vec![i as f64],
260                belief: if i % 2 == 0 { 1.0 } else { -1.0 },
261                constraint: 1.0,
262            })
263            .collect()
264    }
265
266    #[test]
267    fn test_block_transform_reduces_count() {
268        let agents = make_uniform_agents(16, 1.0, 1.0);
269        let bt = BlockTransform::new(1, 4);
270        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
271        assert_eq!(blocks.len(), 4); // 16 / 4 = 4 blocks
272    }
273
274    #[test]
275    fn test_block_transform_non_divisible() {
276        let agents = make_uniform_agents(10, 1.0, 1.0);
277        let bt = BlockTransform::new(1, 3);
278        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
279        assert_eq!(blocks.len(), 4); // ceil(10/3) = 4 blocks
280    }
281
282    #[test]
283    fn test_block_mean_belief_uniform() {
284        let agents = make_uniform_agents(8, 0.7, 1.0);
285        let bt = BlockTransform::new(1, 4);
286        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
287        for b in &blocks {
288            assert!((b.mean_belief - 0.7).abs() < 1e-10);
289        }
290    }
291
292    #[test]
293    fn test_block_aggregate_constraint() {
294        let agents = make_uniform_agents(8, 1.0, 2.0);
295        let bt = BlockTransform::new(1, 4);
296        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
297        for b in &blocks {
298            assert!((b.aggregate_constraint - 8.0).abs() < 1e-10); // 4 agents * 2.0
299        }
300    }
301
302    #[test]
303    fn test_block_center_position() {
304        let agents = make_uniform_agents(4, 1.0, 1.0);
305        let bt = BlockTransform::new(1, 2);
306        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
307        assert!((blocks[0].center[0] - 0.5).abs() < 1e-10); // (0+1)/2
308        assert!((blocks[1].center[0] - 2.5).abs() < 1e-10); // (2+3)/2
309    }
310
311    #[test]
312    fn test_sequential_vs_spatial_sorted() {
313        let agents = make_uniform_agents(8, 1.0, 1.0);
314        let bt = BlockTransform::new(1, 2);
315        let seq = bt.transform(&agents, BlockingStrategy::Sequential);
316        let spat = bt.transform(&agents, BlockingStrategy::SpatialNearest);
317        // For already-sorted input, sequential and spatial should give same result
318        assert_eq!(seq.len(), spat.len());
319        for (s, sp) in seq.iter().zip(spat.iter()) {
320            assert!((s.mean_belief - sp.mean_belief).abs() < 1e-10);
321        }
322    }
323
324    #[test]
325    fn test_majority_rule_positive() {
326        let agents = make_uniform_agents(6, 0.8, 1.0);
327        let bt = BlockTransform::new(1, 3);
328        let blocks = bt.transform(&agents, BlockingStrategy::MajorityRule);
329        for b in &blocks {
330            assert!((b.mean_belief - 1.0).abs() < 1e-10);
331        }
332    }
333
334    #[test]
335    fn test_majority_rule_negative() {
336        let agents = make_uniform_agents(6, -0.8, 1.0);
337        let bt = BlockTransform::new(1, 3);
338        let blocks = bt.transform(&agents, BlockingStrategy::MajorityRule);
339        for b in &blocks {
340            assert!((b.mean_belief - (-1.0)).abs() < 1e-10);
341        }
342    }
343
344    #[test]
345    fn test_magnetization_uniform() {
346        let agents = make_uniform_agents(10, 1.0, 1.0);
347        assert!((magnetization(&agents) - 1.0).abs() < 1e-10);
348    }
349
350    #[test]
351    fn test_magnetization_alternating() {
352        let agents = make_alternating_agents(10);
353        assert!(magnetization(&agents).abs() < 1e-10);
354    }
355
356    #[test]
357    fn test_total_constraint_conservation() {
358        let agents = make_uniform_agents(12, 1.0, 3.0);
359        let total_before = total_constraint(&agents);
360
361        let bt = BlockTransform::new(1, 4);
362        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
363        let total_after: f64 = blocks.iter().map(|b| b.aggregate_constraint).sum();
364        assert!((total_before - total_after).abs() < 1e-10);
365    }
366
367    #[test]
368    fn test_blocks_to_agents_roundtrip() {
369        let agents = make_uniform_agents(8, 1.0, 1.0);
370        let bt = BlockTransform::new(1, 4);
371        let blocks = bt.transform(&agents, BlockingStrategy::Sequential);
372        let new_agents = blocks_to_agents(&blocks);
373        assert_eq!(new_agents.len(), 2); // 8/4 = 2
374        assert_eq!(new_agents[0].id, 0);
375        assert_eq!(new_agents[1].id, 1);
376    }
377
378    #[test]
379    fn test_iterative_coarse_graining() {
380        let agents = make_uniform_agents(64, 1.0, 1.0);
381        // Three levels of coarse-graining: 64 -> 16 -> 4 -> 1
382        let bt1 = BlockTransform::new(1, 4);
383        let blocks1 = bt1.transform(&agents, BlockingStrategy::Sequential);
384        assert_eq!(blocks1.len(), 16);
385
386        let agents2 = blocks_to_agents(&blocks1);
387        let bt2 = BlockTransform::new(2, 4);
388        let blocks2 = bt2.transform(&agents2, BlockingStrategy::Sequential);
389        assert_eq!(blocks2.len(), 4);
390
391        let agents3 = blocks_to_agents(&blocks2);
392        let bt3 = BlockTransform::new(3, 4);
393        let blocks3 = bt3.transform(&agents3, BlockingStrategy::Sequential);
394        assert_eq!(blocks3.len(), 1);
395    }
396}