1use crate::eisenstein::eisenstein_norm;
14use crate::gossip::{GossipNode, GossipMessage};
15use crate::merkle::StateHash;
16use crate::state::ConstraintState;
17use crate::merge::Merge;
18use crate::simulation::SimRng;
19
20#[derive(Debug, Clone)]
22pub struct GeometricNode {
23 pub node: GossipNode,
24 pub position: (i32, i32),
26}
27
28impl GeometricNode {
29 pub fn new(id: &str, position: (i32, i32)) -> Self {
30 Self {
31 node: GossipNode::new(id),
32 position,
33 }
34 }
35
36 pub fn norm(&self) -> i64 {
38 eisenstein_norm(self.position)
39 }
40
41 pub fn distance_to(&self, other: &Self) -> i64 {
43 let (ax, ay) = (self.position.0 as i64, self.position.1 as i64);
44 let (bx, by) = (other.position.0 as i64, other.position.1 as i64);
45 let da = ax - bx;
46 let db = ay - by;
47 let dc = da - db;
48 da.abs().max(db.abs()).max(dc.abs())
49 }
50
51 pub fn constraint_drift(&self) -> u64 {
54 self.node.state.metrics.total_satisfied()
55 + self.node.state.metrics.total_violations()
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct GossipExperiment {
62 pub node_count: usize,
63 pub random_convergence_rounds: Option<u64>,
64 pub geometric_convergence_rounds: Option<u64>,
65 pub random_messages: u64,
66 pub geometric_messages: u64,
67 pub speedup: Option<f64>,
68}
69
70pub fn run_experiment(
72 node_count: usize,
73 seed: u64,
74 max_rounds: u64,
75) -> GossipExperiment {
76 let mut random_nodes: Vec<GeometricNode> = Vec::new();
78 let mut geometric_nodes: Vec<GeometricNode> = Vec::new();
79
80 let mut rng = SimRng::new(seed);
82 for i in 0..node_count {
83 let angle = (i as f64 / node_count as f64) * std::f64::consts::TAU;
84 let r = 5.0 + (rng.next_f64() * 5.0);
85 let x = (r * angle.cos()).round() as i32;
86 let y = (r * angle.sin()).round() as i32;
87
88 random_nodes.push(GeometricNode::new(&format!("n{}", i), (x, y)));
89 geometric_nodes.push(GeometricNode::new(&format!("n{}", i), (x, y)));
90 }
91
92 for (i, (rn, gn)) in random_nodes.iter_mut().zip(geometric_nodes.iter_mut()).enumerate() {
94 rn.node.add_constraint(&format!("c{}", i));
95 rn.node.record_satisfied(100 + i as u64 * 50);
96 gn.node.add_constraint(&format!("c{}", i));
97 gn.node.record_satisfied(100 + i as u64 * 50);
98 }
99
100 let mut random_rng = SimRng::new(seed);
102 let random_result = run_random_gossip(&mut random_nodes, &mut random_rng, max_rounds);
103
104 let mut geometric_rng = SimRng::new(seed);
106 let geometric_result = run_geometric_gossip(&mut geometric_nodes, &mut geometric_rng, max_rounds);
107
108 let speedup = match (random_result.0, geometric_result.0) {
109 (Some(rr), Some(gr)) => Some(rr as f64 / gr as f64),
110 _ => None,
111 };
112
113 GossipExperiment {
114 node_count,
115 random_convergence_rounds: random_result.0,
116 geometric_convergence_rounds: geometric_result.0,
117 random_messages: random_result.1,
118 geometric_messages: geometric_result.1,
119 speedup,
120 }
121}
122
123fn run_random_gossip(
125 nodes: &mut [GeometricNode],
126 rng: &mut SimRng,
127 max_rounds: u64,
128) -> (Option<u64>, u64) {
129 let n = nodes.len();
130 let mut messages = 0u64;
131
132 for round in 1..=max_rounds {
133 for i in 0..n {
134 let peer = rng.random_peer(i, n);
135 let ping = nodes[i].node.ping();
136 messages += 1;
137 let r1 = nodes[peer].node.receive(&ping);
138 for resp in &r1.responses {
139 messages += 1;
140 nodes[i].node.receive(resp);
141 }
142 }
143
144 if check_converged(nodes.iter().map(|n| &n.node).collect()) {
145 return (Some(round), messages);
146 }
147 }
148 (None, messages)
149}
150
151fn run_geometric_gossip(
153 nodes: &mut [GeometricNode],
154 rng: &mut SimRng,
155 max_rounds: u64,
156) -> (Option<u64>, u64) {
157 let n = nodes.len();
158 let mut messages = 0u64;
159
160 for round in 1..=max_rounds {
161 for i in 0..n {
162 let peer = geometric_peer_selection(i, nodes, round, rng);
164 let ping = nodes[i].node.ping();
165 messages += 1;
166 let r1 = nodes[peer].node.receive(&ping);
167 for resp in &r1.responses {
168 messages += 1;
169 nodes[i].node.receive(resp);
170 }
171 }
172
173 if check_converged(nodes.iter().map(|n| &n.node).collect()) {
174 return (Some(round), messages);
175 }
176 }
177 (None, messages)
178}
179
180fn geometric_peer_selection(
182 self_idx: usize,
183 nodes: &[GeometricNode],
184 round: u64,
185 rng: &mut SimRng,
186) -> usize {
187 let self_node = &nodes[self_idx];
188 let n = nodes.len();
189
190 let mut peers: Vec<(usize, i64)> = (0..n)
192 .filter(|&i| i != self_idx)
193 .map(|i| (i, self_node.distance_to(&nodes[i])))
194 .collect();
195 peers.sort_by_key(|(_, d)| *d);
196
197 let radius_fraction = (round as f64 / 10.0).min(1.0);
200 let max_idx = ((peers.len() as f64 * radius_fraction).ceil() as usize).max(1).min(peers.len());
201
202 let idx = (rng.next_u64() as usize) % max_idx;
204 peers[idx].0
205}
206
207fn check_converged(nodes: Vec<&GossipNode>) -> bool {
208 if nodes.len() <= 1 { return true; }
209 let hashes: Vec<_> = nodes.iter().map(|n| n.state_hash()).collect();
210 let first = hashes[0];
211 hashes.iter().all(|h| *h == first)
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_geometric_distance() {
220 let a = GeometricNode::new("a", (0, 0));
221 let b = GeometricNode::new("b", (3, 0));
222 let c = GeometricNode::new("c", (3, 3));
223
224 assert_eq!(a.distance_to(&b), 3);
225 assert_eq!(a.distance_to(&c), 3); assert_eq!(b.distance_to(&c), 3); }
228
229 #[test]
230 fn test_experiment_4_nodes() {
231 let result = run_experiment(4, 42, 30);
232 println!("4 nodes: random={:?} geometric={:?} speedup={:?}",
233 result.random_convergence_rounds,
234 result.geometric_convergence_rounds,
235 result.speedup);
236
237 assert!(result.random_convergence_rounds.is_some());
239 assert!(result.geometric_convergence_rounds.is_some());
240 }
241
242 #[test]
243 fn test_experiment_8_nodes() {
244 let result = run_experiment(8, 42, 50);
245 println!("8 nodes: random={:?} geometric={:?} speedup={:?}",
246 result.random_convergence_rounds,
247 result.geometric_convergence_rounds,
248 result.speedup);
249
250 assert!(result.random_convergence_rounds.is_some());
251 assert!(result.geometric_convergence_rounds.is_some());
252 }
253
254 #[test]
255 fn test_experiment_16_nodes() {
256 let result = run_experiment(16, 42, 100);
257 println!("16 nodes: random={:?} geometric={:?} speedup={:?}",
258 result.random_convergence_rounds,
259 result.geometric_convergence_rounds,
260 result.speedup);
261
262 assert!(result.random_convergence_rounds.is_some());
263 assert!(result.geometric_convergence_rounds.is_some());
264 }
265
266 #[test]
267 fn test_geometric_scales_better() {
268 let r4 = run_experiment(4, 42, 50);
270 let r8 = run_experiment(8, 42, 50);
271 let r16 = run_experiment(16, 42, 100);
272
273 let r4r = r4.random_convergence_rounds.unwrap() as f64;
274 let r8r = r8.random_convergence_rounds.unwrap() as f64;
275 let r16r = r16.random_convergence_rounds.unwrap() as f64;
276
277 let r4g = r4.geometric_convergence_rounds.unwrap() as f64;
278 let r8g = r8.geometric_convergence_rounds.unwrap() as f64;
279 let r16g = r16.geometric_convergence_rounds.unwrap() as f64;
280
281 println!("Scaling (random): 4→{:.0} 8→{:.0} 16→{:.0}", r4r, r8r, r16r);
282 println!("Scaling (geometric): 4→{:.0} 8→{:.0} 16→{:.0}", r4g, r8g, r16g);
283
284 assert!(r4g <= r4r * 1.5, "Geometric should not be much worse at 4 nodes");
286 }
287}