rust_igraph/algorithms/layout/
gem.rs1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
11
12#[derive(Debug, Clone)]
14pub struct GemParams {
15 pub maxiter: u32,
18 pub temp_max: f64,
20 pub temp_min: f64,
22 pub temp_init: f64,
24}
25
26impl GemParams {
27 pub fn for_graph(n: u32) -> Self {
29 let nf = f64::from(n);
30 Self {
31 maxiter: 40u32.saturating_mul(n).saturating_mul(n),
32 temp_max: nf,
33 temp_min: 0.1,
34 temp_init: nf.sqrt(),
35 }
36 }
37}
38
39pub fn layout_gem(
82 graph: &Graph,
83 seed: Option<&[[f64; 2]]>,
84 params: &GemParams,
85) -> IgraphResult<Vec<[f64; 2]>> {
86 let n = graph.vcount() as usize;
87 if n == 0 {
88 return Ok(Vec::new());
89 }
90
91 if params.temp_max <= 0.0 {
92 return Err(IgraphError::InvalidArgument(
93 "temp_max must be positive".into(),
94 ));
95 }
96 if params.temp_min <= 0.0 {
97 return Err(IgraphError::InvalidArgument(
98 "temp_min must be positive".into(),
99 ));
100 }
101 if params.temp_init <= 0.0 {
102 return Err(IgraphError::InvalidArgument(
103 "temp_init must be positive".into(),
104 ));
105 }
106 if params.temp_max < params.temp_init || params.temp_init < params.temp_min {
107 return Err(IgraphError::InvalidArgument(
108 "requires temp_min <= temp_init <= temp_max".into(),
109 ));
110 }
111
112 if let Some(s) = seed {
113 if s.len() != n {
114 return Err(IgraphError::InvalidArgument(format!(
115 "seed length ({}) must equal vertex count ({})",
116 s.len(),
117 n
118 )));
119 }
120 }
121
122 let elen_des2: f64 = 128.0 * 128.0;
124 let gamma: f64 = 1.0 / 16.0;
125 let alpha_o: f64 = std::f64::consts::PI;
126 let alpha_r: f64 = std::f64::consts::PI / 3.0;
127 let sigma_o: f64 = 1.0 / 3.0;
128 let sigma_r: f64 = 1.0 / (2.0 * n as f64);
129
130 let mut phi = vec![0.0_f64; n];
132 for v in 0..n {
133 let deg = graph.degree(v as VertexId).unwrap_or(0) as f64;
134 phi[v] = deg * (deg / 2.0 + 1.0);
135 }
136
137 let mut pos = if let Some(s) = seed {
139 s.to_vec()
140 } else {
141 let width_half = n as f64 * 100.0;
142 let mut rng = SplitMix64::new(42);
143 (0..n)
144 .map(|_| {
145 [
146 rng.next_uniform() * 2.0 * width_half - width_half,
147 rng.next_uniform() * 2.0 * width_half - width_half,
148 ]
149 })
150 .collect()
151 };
152
153 let mut barycenter_x: f64 = pos.iter().map(|p| p[0]).sum();
155 let mut barycenter_y: f64 = pos.iter().map(|p| p[1]).sum();
156
157 let mut impulse_x = vec![0.0_f64; n];
159 let mut impulse_y = vec![0.0_f64; n];
160 let mut temp = vec![params.temp_init; n];
161 let mut skew_gauge = vec![0.0_f64; n];
162
163 let mut perm: Vec<usize> = (0..n).collect();
165 let mut perm_pointer: usize = 0;
166 let mut rng = SplitMix64::new(123);
167
168 let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
170 let ecount = graph.ecount();
171 for eid in 0..ecount as u32 {
172 if let Ok((src, tgt)) = graph.edge(eid) {
173 if src != tgt {
174 adj[src as usize].push(tgt);
175 adj[tgt as usize].push(src);
176 }
177 }
178 }
179
180 let mut temp_global = params.temp_init * n as f64;
181 let mut maxiter = params.maxiter;
182
183 while temp_global > params.temp_min * n as f64 && maxiter > 0 {
184 if perm_pointer == 0 {
186 fisher_yates_shuffle(&mut perm, &mut rng);
187 perm_pointer = n;
188 }
189 perm_pointer -= 1;
190 let v = perm[perm_pointer];
191
192 let nf = n as f64;
194 let mut px = (barycenter_x / nf - pos[v][0]) * gamma * phi[v];
195 let mut py = (barycenter_y / nf - pos[v][1]) * gamma * phi[v];
196
197 px += rng.next_uniform() * 64.0 - 32.0;
199 py += rng.next_uniform() * 64.0 - 32.0;
200
201 for u in 0..n {
203 if u == v {
204 continue;
205 }
206 let dx = pos[v][0] - pos[u][0];
207 let dy = pos[v][1] - pos[u][1];
208 let dist2 = dx * dx + dy * dy;
209 if dist2 != 0.0 {
210 px += dx * elen_des2 / dist2;
211 py += dy * elen_des2 / dist2;
212 }
213 }
214
215 for &u in &adj[v] {
217 let ui = u as usize;
218 let dx = pos[v][0] - pos[ui][0];
219 let dy = pos[v][1] - pos[ui][1];
220 let dist2 = dx * dx + dy * dy;
221 if phi[v] != 0.0 {
222 px -= dx * dist2 / (elen_des2 * phi[v]);
223 py -= dy * dist2 / (elen_des2 * phi[v]);
224 }
225 }
226
227 if px != 0.0 || py != 0.0 {
229 let plen = (px * px + py * py).sqrt();
230 px *= temp[v] / plen;
231 py *= temp[v] / plen;
232 pos[v][0] += px;
233 pos[v][1] += py;
234 barycenter_x += px;
235 barycenter_y += py;
236 }
237
238 let pvx = impulse_x[v];
240 let pvy = impulse_y[v];
241 if pvx != 0.0 || pvy != 0.0 {
242 let beta = (pvy - py).atan2(pvx - px);
243 let sin_beta = beta.sin();
244 let sign_sin_beta = if sin_beta > 0.0 {
245 1.0
246 } else if sin_beta < 0.0 {
247 -1.0
248 } else {
249 0.0
250 };
251 let cos_beta = beta.cos();
252 let abs_cos_beta = cos_beta.abs();
253 let old_temp = temp[v];
254
255 if sin_beta >= (std::f64::consts::FRAC_PI_2 + alpha_r / 2.0).sin() {
256 skew_gauge[v] += sigma_r * sign_sin_beta;
257 }
258 if abs_cos_beta >= (alpha_o / 2.0).cos() {
259 temp[v] *= sigma_o * cos_beta;
260 }
261 temp[v] *= 1.0 - skew_gauge[v].abs();
262 if temp[v] > params.temp_max {
263 temp[v] = params.temp_max;
264 }
265 if temp[v] < 0.0 {
266 temp[v] = 0.0;
267 }
268 impulse_x[v] = px;
269 impulse_y[v] = py;
270 temp_global += temp[v] - old_temp;
271 }
272
273 maxiter -= 1;
274 }
275
276 Ok(pos)
277}
278
279struct SplitMix64 {
284 state: u64,
285}
286
287impl SplitMix64 {
288 fn new(seed: u64) -> Self {
289 Self { state: seed }
290 }
291
292 fn next_u64(&mut self) -> u64 {
293 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
294 let mut z = self.state;
295 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
296 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
297 z ^ (z >> 31)
298 }
299
300 fn next_uniform(&mut self) -> f64 {
301 (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
302 }
303}
304
305fn fisher_yates_shuffle(perm: &mut [usize], rng: &mut SplitMix64) {
306 let n = perm.len();
307 for i in (1..n).rev() {
308 let j = (rng.next_u64() as usize) % (i + 1);
309 perm.swap(i, j);
310 }
311}
312
313#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_gem_empty() {
323 let g = Graph::with_vertices(0);
324 let params = GemParams::for_graph(0);
325 let pos = layout_gem(&g, None, ¶ms).unwrap();
326 assert!(pos.is_empty());
327 }
328
329 #[test]
330 fn test_gem_single_vertex() {
331 let g = Graph::with_vertices(1);
332 let params = GemParams::for_graph(1);
333 let pos = layout_gem(&g, None, ¶ms).unwrap();
334 assert_eq!(pos.len(), 1);
335 assert!(pos[0][0].is_finite());
336 assert!(pos[0][1].is_finite());
337 }
338
339 #[test]
340 fn test_gem_triangle() {
341 let mut g = Graph::with_vertices(3);
342 g.add_edge(0, 1).unwrap();
343 g.add_edge(1, 2).unwrap();
344 g.add_edge(2, 0).unwrap();
345 let params = GemParams::for_graph(3);
346 let pos = layout_gem(&g, None, ¶ms).unwrap();
347 assert_eq!(pos.len(), 3);
348 for p in &pos {
349 assert!(p[0].is_finite());
350 assert!(p[1].is_finite());
351 }
352 }
353
354 #[test]
355 fn test_gem_path() {
356 let mut g = Graph::with_vertices(5);
357 for i in 0..4 {
358 g.add_edge(i, i + 1).unwrap();
359 }
360 let params = GemParams::for_graph(5);
361 let pos = layout_gem(&g, None, ¶ms).unwrap();
362 assert_eq!(pos.len(), 5);
363 for p in &pos {
364 assert!(p[0].is_finite());
365 assert!(p[1].is_finite());
366 }
367 }
368
369 #[test]
370 fn test_gem_no_overlap() {
371 let mut g = Graph::with_vertices(4);
372 g.add_edge(0, 1).unwrap();
373 g.add_edge(1, 2).unwrap();
374 g.add_edge(2, 3).unwrap();
375 g.add_edge(3, 0).unwrap();
376 let params = GemParams::for_graph(4);
377 let pos = layout_gem(&g, None, ¶ms).unwrap();
378 let mut all_same = true;
380 for i in 1..4 {
381 if (pos[i][0] - pos[0][0]).abs() > 1e-6 || (pos[i][1] - pos[0][1]).abs() > 1e-6 {
382 all_same = false;
383 break;
384 }
385 }
386 assert!(!all_same, "all vertices collapsed to the same point");
387 }
388
389 #[test]
390 fn test_gem_with_seed() {
391 let mut g = Graph::with_vertices(3);
392 g.add_edge(0, 1).unwrap();
393 g.add_edge(1, 2).unwrap();
394 let seed = vec![[0.0, 0.0], [100.0, 0.0], [50.0, 86.6]];
395 let params = GemParams::for_graph(3);
396 let pos = layout_gem(&g, Some(&seed), ¶ms).unwrap();
397 assert_eq!(pos.len(), 3);
398 for p in &pos {
399 assert!(p[0].is_finite());
400 assert!(p[1].is_finite());
401 }
402 }
403
404 #[test]
405 fn test_gem_seed_wrong_length() {
406 let g = Graph::with_vertices(3);
407 let seed = vec![[0.0, 0.0], [1.0, 0.0]];
408 let params = GemParams::for_graph(3);
409 let result = layout_gem(&g, Some(&seed), ¶ms);
410 assert!(result.is_err());
411 }
412
413 #[test]
414 fn test_gem_invalid_temp() {
415 let g = Graph::with_vertices(3);
416 let params = GemParams {
417 maxiter: 100,
418 temp_max: -1.0,
419 temp_min: 0.1,
420 temp_init: 1.0,
421 };
422 assert!(layout_gem(&g, None, ¶ms).is_err());
423
424 let params2 = GemParams {
425 maxiter: 100,
426 temp_max: 10.0,
427 temp_min: 5.0,
428 temp_init: 2.0,
429 };
430 assert!(layout_gem(&g, None, ¶ms2).is_err());
431 }
432
433 #[test]
434 fn test_gem_deterministic() {
435 let mut g = Graph::with_vertices(4);
436 g.add_edge(0, 1).unwrap();
437 g.add_edge(1, 2).unwrap();
438 g.add_edge(2, 3).unwrap();
439 let params = GemParams::for_graph(4);
440 let pos1 = layout_gem(&g, None, ¶ms).unwrap();
441 let pos2 = layout_gem(&g, None, ¶ms).unwrap();
442 for i in 0..4 {
443 assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-10);
444 assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-10);
445 }
446 }
447
448 #[test]
449 fn test_gem_disconnected() {
450 let mut g = Graph::with_vertices(4);
451 g.add_edge(0, 1).unwrap();
452 g.add_edge(2, 3).unwrap();
453 let params = GemParams::for_graph(4);
454 let pos = layout_gem(&g, None, ¶ms).unwrap();
455 assert_eq!(pos.len(), 4);
456 for p in &pos {
457 assert!(p[0].is_finite());
458 assert!(p[1].is_finite());
459 }
460 }
461
462 #[test]
463 fn test_gem_star() {
464 let mut g = Graph::with_vertices(6);
465 for i in 1..6 {
466 g.add_edge(0, i).unwrap();
467 }
468 let params = GemParams {
469 maxiter: 1000,
470 temp_max: 6.0,
471 temp_min: 0.1,
472 temp_init: 2.4,
473 };
474 let pos = layout_gem(&g, None, ¶ms).unwrap();
475 assert_eq!(pos.len(), 6);
476 for p in &pos {
477 assert!(p[0].is_finite());
478 assert!(p[1].is_finite());
479 }
480 }
481}