rust_igraph/algorithms/layout/
graphopt.rs1use crate::core::{Graph, IgraphError, IgraphResult};
10
11#[derive(Debug, Clone)]
13pub struct GraphoptParams {
14 pub niter: u32,
16 pub node_charge: f64,
18 pub node_mass: f64,
20 pub spring_length: f64,
22 pub spring_constant: f64,
24 pub max_sa_movement: f64,
26}
27
28impl Default for GraphoptParams {
29 fn default() -> Self {
30 Self {
31 niter: 500,
32 node_charge: 0.001,
33 node_mass: 30.0,
34 spring_length: 0.0,
35 spring_constant: 1.0,
36 max_sa_movement: 5.0,
37 }
38 }
39}
40
41pub fn layout_graphopt(
82 graph: &Graph,
83 seed: Option<&[[f64; 2]]>,
84 params: &GraphoptParams,
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.node_mass == 0.0 {
92 return Err(IgraphError::InvalidArgument(
93 "node_mass must be non-zero".into(),
94 ));
95 }
96 if let Some(s) = seed {
97 if s.len() != n {
98 return Err(IgraphError::InvalidArgument(format!(
99 "seed length ({}) must equal vertex count ({})",
100 s.len(),
101 n
102 )));
103 }
104 }
105
106 let no_edges = graph.ecount();
107 let apply_electric = params.node_charge != 0.0;
108
109 const COULOMBS_CONSTANT: f64 = 8_987_500_000.0;
111
112 let mut edges: Vec<(usize, usize)> = Vec::with_capacity(no_edges);
114 for eid in 0..no_edges as u32 {
115 if let Ok((src, tgt)) = graph.edge(eid) {
116 edges.push((src as usize, tgt as usize));
117 }
118 }
119
120 let mut pos = if let Some(s) = seed {
122 s.to_vec()
123 } else {
124 let mut rng = SplitMix64::new(42);
125 (0..n)
126 .map(|_| [rng.next_uniform() - 0.5, rng.next_uniform() - 0.5])
127 .collect()
128 };
129
130 let mut forces_x = vec![0.0_f64; n];
131 let mut forces_y = vec![0.0_f64; n];
132
133 for _iter in 0..params.niter {
134 for fx in forces_x.iter_mut() {
136 *fx = 0.0;
137 }
138 for fy in forces_y.iter_mut() {
139 *fy = 0.0;
140 }
141
142 if apply_electric {
144 for this in 0..n {
145 for other in (this + 1)..n {
146 let dx = pos[this][0] - pos[other][0];
147 let dy = pos[this][1] - pos[other][1];
148 let distance = (dx * dx + dy * dy).sqrt();
149
150 if distance == 0.0 || distance >= 500.0 {
151 continue;
152 }
153
154 let directed_force = COULOMBS_CONSTANT
155 * (params.node_charge * params.node_charge)
156 / (distance * distance);
157
158 let fx = directed_force * dx.abs() / distance;
159 let fy = directed_force * dy.abs() / distance;
160
161 let sign_x = if pos[other][0] < pos[this][0] {
163 1.0
164 } else {
165 -1.0
166 };
167 let sign_y = if pos[other][1] < pos[this][1] {
168 1.0
169 } else {
170 -1.0
171 };
172
173 forces_x[this] -= sign_x * fx;
174 forces_y[this] -= sign_y * fy;
175 forces_x[other] += sign_x * fx;
176 forces_y[other] += sign_y * fy;
177 }
178 }
179 }
180
181 for &(src, tgt) in &edges {
183 let dx = pos[src][0] - pos[tgt][0];
184 let dy = pos[src][1] - pos[tgt][1];
185 let distance = (dx * dx + dy * dy).sqrt();
186
187 if distance == 0.0 {
188 continue;
189 }
190
191 let displacement = (distance - params.spring_length).abs();
192 let directed_force = -params.spring_constant * displacement;
193
194 if distance == params.spring_length {
196 continue;
197 }
198
199 let fx_abs = directed_force.abs() * dx.abs() / distance;
200 let fy_abs = directed_force.abs() * dy.abs() / distance;
201
202 let sign_x = if pos[tgt][0] < pos[src][0] { 1.0 } else { -1.0 };
204 let sign_y = if pos[tgt][1] < pos[src][1] { 1.0 } else { -1.0 };
205
206 let spring_sign = if distance > params.spring_length {
208 1.0
209 } else {
210 -1.0
211 };
212
213 let hfx = 0.5 * spring_sign * sign_x * fx_abs;
215 let hfy = 0.5 * spring_sign * sign_y * fy_abs;
216
217 forces_x[src] += hfx;
219 forces_y[src] += hfy;
220 forces_x[tgt] -= hfx;
221 forces_y[tgt] -= hfy;
222 }
223
224 for v in 0..n {
226 let mut x_move = forces_x[v] / params.node_mass;
227 let mut y_move = forces_y[v] / params.node_mass;
228
229 x_move = x_move.clamp(-params.max_sa_movement, params.max_sa_movement);
230 y_move = y_move.clamp(-params.max_sa_movement, params.max_sa_movement);
231
232 pos[v][0] += x_move;
233 pos[v][1] += y_move;
234 }
235 }
236
237 Ok(pos)
238}
239
240struct SplitMix64 {
245 state: u64,
246}
247
248impl SplitMix64 {
249 fn new(seed: u64) -> Self {
250 Self { state: seed }
251 }
252
253 fn next_u64(&mut self) -> u64 {
254 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
255 let mut z = self.state;
256 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
257 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
258 z ^ (z >> 31)
259 }
260
261 fn next_uniform(&mut self) -> f64 {
262 (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
263 }
264}
265
266#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_graphopt_empty() {
276 let g = Graph::with_vertices(0);
277 let params = GraphoptParams::default();
278 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
279 assert!(pos.is_empty());
280 }
281
282 #[test]
283 fn test_graphopt_single() {
284 let g = Graph::with_vertices(1);
285 let params = GraphoptParams::default();
286 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
287 assert_eq!(pos.len(), 1);
288 assert!(pos[0][0].is_finite());
289 }
290
291 #[test]
292 fn test_graphopt_triangle() {
293 let mut g = Graph::with_vertices(3);
294 g.add_edge(0, 1).unwrap();
295 g.add_edge(1, 2).unwrap();
296 g.add_edge(2, 0).unwrap();
297 let params = GraphoptParams {
298 niter: 100,
299 ..GraphoptParams::default()
300 };
301 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
302 assert_eq!(pos.len(), 3);
303 for p in &pos {
304 assert!(p[0].is_finite() && p[1].is_finite());
305 }
306 }
307
308 #[test]
309 fn test_graphopt_path() {
310 let mut g = Graph::with_vertices(5);
311 for i in 0..4 {
312 g.add_edge(i, i + 1).unwrap();
313 }
314 let params = GraphoptParams {
315 niter: 50,
316 ..GraphoptParams::default()
317 };
318 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
319 assert_eq!(pos.len(), 5);
320 for p in &pos {
321 assert!(p[0].is_finite() && p[1].is_finite());
322 }
323 }
324
325 #[test]
326 fn test_graphopt_with_seed() {
327 let mut g = Graph::with_vertices(3);
328 g.add_edge(0, 1).unwrap();
329 g.add_edge(1, 2).unwrap();
330 let seed = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 0.866]];
331 let params = GraphoptParams {
332 niter: 50,
333 ..GraphoptParams::default()
334 };
335 let pos = layout_graphopt(&g, Some(&seed), ¶ms).unwrap();
336 assert_eq!(pos.len(), 3);
337 }
338
339 #[test]
340 fn test_graphopt_seed_wrong_length() {
341 let g = Graph::with_vertices(3);
342 let seed = vec![[0.0, 0.0]];
343 let params = GraphoptParams::default();
344 assert!(layout_graphopt(&g, Some(&seed), ¶ms).is_err());
345 }
346
347 #[test]
348 fn test_graphopt_zero_mass() {
349 let g = Graph::with_vertices(3);
350 let params = GraphoptParams {
351 node_mass: 0.0,
352 ..GraphoptParams::default()
353 };
354 assert!(layout_graphopt(&g, None, ¶ms).is_err());
355 }
356
357 #[test]
358 fn test_graphopt_no_charge() {
359 let mut g = Graph::with_vertices(4);
360 g.add_edge(0, 1).unwrap();
361 g.add_edge(1, 2).unwrap();
362 g.add_edge(2, 3).unwrap();
363 g.add_edge(3, 0).unwrap();
364 let params = GraphoptParams {
365 niter: 50,
366 node_charge: 0.0,
367 ..GraphoptParams::default()
368 };
369 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
370 assert_eq!(pos.len(), 4);
371 for p in &pos {
372 assert!(p[0].is_finite() && p[1].is_finite());
373 }
374 }
375
376 #[test]
377 fn test_graphopt_deterministic() {
378 let mut g = Graph::with_vertices(4);
379 g.add_edge(0, 1).unwrap();
380 g.add_edge(1, 2).unwrap();
381 g.add_edge(2, 3).unwrap();
382 g.add_edge(3, 0).unwrap();
383 let params = GraphoptParams {
384 niter: 50,
385 ..GraphoptParams::default()
386 };
387 let pos1 = layout_graphopt(&g, None, ¶ms).unwrap();
388 let pos2 = layout_graphopt(&g, None, ¶ms).unwrap();
389 for i in 0..4 {
390 assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-10);
391 assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-10);
392 }
393 }
394
395 #[test]
396 fn test_graphopt_no_edges() {
397 let g = Graph::with_vertices(4);
398 let params = GraphoptParams {
399 niter: 50,
400 ..GraphoptParams::default()
401 };
402 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
403 assert_eq!(pos.len(), 4);
404 for p in &pos {
405 assert!(p[0].is_finite() && p[1].is_finite());
406 }
407 }
408
409 #[test]
410 fn test_graphopt_vertices_spread() {
411 let mut g = Graph::with_vertices(4);
412 g.add_edge(0, 1).unwrap();
413 g.add_edge(2, 3).unwrap();
414 let params = GraphoptParams {
415 niter: 100,
416 ..GraphoptParams::default()
417 };
418 let pos = layout_graphopt(&g, None, ¶ms).unwrap();
419 let mut all_same = true;
421 for i in 1..4 {
422 if (pos[i][0] - pos[0][0]).abs() > 1e-6 || (pos[i][1] - pos[0][1]).abs() > 1e-6 {
423 all_same = false;
424 break;
425 }
426 }
427 assert!(!all_same, "all vertices collapsed to the same point");
428 }
429}