1use crate::algorithms::connectivity::connected_components;
4use crate::core::{Graph, IgraphError, IgraphResult, rng::SplitMix64};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FrGrid {
9 Grid,
11 NoGrid,
13 Auto,
15}
16
17#[derive(Debug, Clone, Default)]
19pub struct FrBounds {
20 pub minx: Option<Vec<f64>>,
21 pub maxx: Option<Vec<f64>>,
22 pub miny: Option<Vec<f64>>,
23 pub maxy: Option<Vec<f64>>,
24}
25
26#[derive(Debug, Clone)]
28pub struct FrParams {
29 pub niter: u32,
31 pub start_temp: Option<f64>,
33 pub grid: FrGrid,
35 pub weights: Option<Vec<f64>>,
37 pub bounds: FrBounds,
39 pub seed_coords: Option<Vec<(f64, f64)>>,
41 pub rng_seed: u64,
43}
44
45impl Default for FrParams {
46 fn default() -> Self {
47 Self {
48 niter: 500,
49 start_temp: None,
50 grid: FrGrid::Auto,
51 weights: None,
52 bounds: FrBounds::default(),
53 seed_coords: None,
54 rng_seed: 42,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Default)]
61pub struct FrBounds3d {
62 pub minx: Option<Vec<f64>>,
63 pub maxx: Option<Vec<f64>>,
64 pub miny: Option<Vec<f64>>,
65 pub maxy: Option<Vec<f64>>,
66 pub minz: Option<Vec<f64>>,
67 pub maxz: Option<Vec<f64>>,
68}
69
70#[derive(Debug, Clone)]
72pub struct FrParams3d {
73 pub niter: u32,
74 pub start_temp: Option<f64>,
75 pub weights: Option<Vec<f64>>,
76 pub bounds: FrBounds3d,
77 pub seed_coords: Option<Vec<(f64, f64, f64)>>,
78 pub rng_seed: u64,
79}
80
81impl Default for FrParams3d {
82 fn default() -> Self {
83 Self {
84 niter: 500,
85 start_temp: None,
86 weights: None,
87 bounds: FrBounds3d::default(),
88 seed_coords: None,
89 rng_seed: 42,
90 }
91 }
92}
93
94pub fn layout_fruchterman_reingold(
122 graph: &Graph,
123 params: &FrParams,
124) -> IgraphResult<Vec<(f64, f64)>> {
125 let vcount = graph.vcount() as usize;
126 let ecount = graph.ecount() as usize;
127
128 if let Some(ref w) = params.weights {
129 if w.len() != ecount {
130 return Err(IgraphError::InvalidArgument(
131 "weight vector length must equal edge count".into(),
132 ));
133 }
134 if ecount > 0 && w.iter().any(|&x| x <= 0.0) {
135 return Err(IgraphError::InvalidArgument(
136 "weights must be positive for Fruchterman-Reingold layout".into(),
137 ));
138 }
139 }
140
141 validate_bounds_2d(vcount, ¶ms.bounds)?;
142
143 if let Some(ref sc) = params.seed_coords {
144 if sc.len() != vcount {
145 return Err(IgraphError::InvalidArgument(
146 "seed_coords length must equal vertex count".into(),
147 ));
148 }
149 }
150
151 if vcount == 0 {
152 return Ok(Vec::new());
153 }
154
155 let start_temp = params.start_temp.unwrap_or_else(|| (vcount as f64).sqrt());
156
157 let use_grid = match params.grid {
158 FrGrid::Grid => true,
159 FrGrid::NoGrid => false,
160 FrGrid::Auto => vcount > 1000,
161 };
162
163 if use_grid {
164 fr_grid_2d(graph, params, start_temp)
165 } else {
166 fr_naive_2d(graph, params, start_temp)
167 }
168}
169
170pub fn layout_fruchterman_reingold_3d(
189 graph: &Graph,
190 params: &FrParams3d,
191) -> IgraphResult<Vec<(f64, f64, f64)>> {
192 let vcount = graph.vcount() as usize;
193 let ecount = graph.ecount() as usize;
194
195 if let Some(ref w) = params.weights {
196 if w.len() != ecount {
197 return Err(IgraphError::InvalidArgument(
198 "weight vector length must equal edge count".into(),
199 ));
200 }
201 if ecount > 0 && w.iter().any(|&x| x <= 0.0) {
202 return Err(IgraphError::InvalidArgument(
203 "weights must be positive for Fruchterman-Reingold layout".into(),
204 ));
205 }
206 }
207
208 validate_bounds_3d(vcount, ¶ms.bounds)?;
209
210 if let Some(ref sc) = params.seed_coords {
211 if sc.len() != vcount {
212 return Err(IgraphError::InvalidArgument(
213 "seed_coords length must equal vertex count".into(),
214 ));
215 }
216 }
217
218 if vcount == 0 {
219 return Ok(Vec::new());
220 }
221
222 let start_temp = params.start_temp.unwrap_or_else(|| (vcount as f64).sqrt());
223 fr_naive_3d(graph, params, start_temp)
224}
225
226fn fr_naive_2d(graph: &Graph, params: &FrParams, start_temp: f64) -> IgraphResult<Vec<(f64, f64)>> {
229 let vcount = graph.vcount() as usize;
230 let ecount = graph.ecount() as usize;
231
232 let cc = connected_components(graph)?;
233 let connected = cc.count == 1;
234 let big_c = if connected {
235 0.0
236 } else {
237 vcount as f64 * (vcount as f64).sqrt()
238 };
239
240 let mut pos: Vec<(f64, f64)> = if let Some(ref sc) = params.seed_coords {
241 sc.clone()
242 } else {
243 init_random_bounded_2d(vcount, ¶ms.bounds, params.rng_seed)
244 };
245
246 let mut rng = SplitMix64::new(params.rng_seed.wrapping_add(0xDEAD_BEEF));
247 let mut dispx = vec![0.0f64; vcount];
248 let mut dispy = vec![0.0f64; vcount];
249 let niter = params.niter;
250 let diff_temp = if niter > 0 {
251 start_temp / niter as f64
252 } else {
253 0.0
254 };
255 let mut temp = start_temp;
256
257 for _ in 0..niter {
258 dispx.iter_mut().for_each(|d| *d = 0.0);
259 dispy.iter_mut().for_each(|d| *d = 0.0);
260
261 if connected {
263 for v in 0..vcount {
264 for u in (v + 1)..vcount {
265 let mut dx = pos[v].0 - pos[u].0;
266 let mut dy = pos[v].1 - pos[u].1;
267 let mut dlen = dx * dx + dy * dy;
268 while dlen == 0.0 {
269 dx = rng.gen_unit() * 2e-9 - 1e-9;
270 dy = rng.gen_unit() * 2e-9 - 1e-9;
271 dlen = dx * dx + dy * dy;
272 }
273 dispx[v] += dx / dlen;
274 dispy[v] += dy / dlen;
275 dispx[u] -= dx / dlen;
276 dispy[u] -= dy / dlen;
277 }
278 }
279 } else {
280 for v in 0..vcount {
281 for u in (v + 1)..vcount {
282 let mut dx = pos[v].0 - pos[u].0;
283 let mut dy = pos[v].1 - pos[u].1;
284 let mut dlen = dx * dx + dy * dy;
285 while dlen == 0.0 {
286 dx = rng.gen_unit() * 2e-9 - 1e-9;
287 dy = rng.gen_unit() * 2e-9 - 1e-9;
288 dlen = dx * dx + dy * dy;
289 }
290 let rdlen = dlen.sqrt();
291 let factor = (big_c - dlen * rdlen) / (dlen * big_c);
292 dispx[v] += dx * factor;
293 dispy[v] += dy * factor;
294 dispx[u] -= dx * factor;
295 dispy[u] -= dy * factor;
296 }
297 }
298 }
299
300 for e in 0..ecount {
302 let (from, to) = graph.edge(e as u32)?;
303 let v = from as usize;
304 let u = to as usize;
305 let dx = pos[v].0 - pos[u].0;
306 let dy = pos[v].1 - pos[u].1;
307 let w = params.weights.as_ref().map_or(1.0, |ws| ws[e]);
308 let dlen = (dx * dx + dy * dy).sqrt() * w;
309 dispx[v] -= dx * dlen;
310 dispy[v] -= dy * dlen;
311 dispx[u] += dx * dlen;
312 dispy[u] += dy * dlen;
313 }
314
315 for v in 0..vcount {
317 let dx = dispx[v] + rng.gen_unit() * 2e-9 - 1e-9;
318 let dy = dispy[v] + rng.gen_unit() * 2e-9 - 1e-9;
319 let displen = (dx * dx + dy * dy).sqrt();
320 let (cdx, cdy) = if displen > temp {
321 (dx * temp / displen, dy * temp / displen)
322 } else {
323 (dx, dy)
324 };
325 if displen > 0.0 {
326 pos[v].0 += cdx;
327 pos[v].1 += cdy;
328 }
329 apply_bounds_2d(&mut pos[v], v, ¶ms.bounds);
330 }
331
332 temp -= diff_temp;
333 }
334
335 Ok(pos)
336}
337
338fn fr_grid_2d(graph: &Graph, params: &FrParams, start_temp: f64) -> IgraphResult<Vec<(f64, f64)>> {
339 let vcount = graph.vcount() as usize;
340 let ecount = graph.ecount() as usize;
341 let width = (vcount as f64).sqrt();
342 let cellsize = 2.0f64;
343
344 let mut pos: Vec<(f64, f64)> = if let Some(ref sc) = params.seed_coords {
345 sc.clone()
346 } else {
347 init_random_bounded_2d(vcount, ¶ms.bounds, params.rng_seed)
348 };
349
350 let mut rng = SplitMix64::new(params.rng_seed.wrapping_add(0xDEAD_BEEF));
351 let mut dispx = vec![0.0f64; vcount];
352 let mut dispy = vec![0.0f64; vcount];
353 let niter = params.niter;
354 let diff_temp = if niter > 0 {
355 start_temp / niter as f64
356 } else {
357 0.0
358 };
359 let mut temp = start_temp;
360
361 let half_w = width / 2.0;
362 let cell_threshold_sq = cellsize * cellsize;
363
364 for _ in 0..niter {
365 dispx.iter_mut().for_each(|d| *d = 0.0);
366 dispy.iter_mut().for_each(|d| *d = 0.0);
367
368 let grid = Grid2d::build(&pos, -half_w, half_w, -half_w, half_w, cellsize);
370 for v in 0..vcount {
371 let neighbors = grid.nearby_vertices(v, &pos);
372 for &u in &neighbors {
373 if u <= v {
374 continue;
375 }
376 let mut dx = pos[v].0 - pos[u].0;
377 let mut dy = pos[v].1 - pos[u].1;
378 let mut dlen = dx * dx + dy * dy;
379 while dlen == 0.0 {
380 dx = rng.gen_unit() * 2e-9 - 1e-9;
381 dy = rng.gen_unit() * 2e-9 - 1e-9;
382 dlen = dx * dx + dy * dy;
383 }
384 if dlen < cell_threshold_sq {
385 dispx[v] += dx / dlen;
386 dispy[v] += dy / dlen;
387 dispx[u] -= dx / dlen;
388 dispy[u] -= dy / dlen;
389 }
390 }
391 }
392
393 for e in 0..ecount {
395 let (from, to) = graph.edge(e as u32)?;
396 let v = from as usize;
397 let u = to as usize;
398 let dx = pos[v].0 - pos[u].0;
399 let dy = pos[v].1 - pos[u].1;
400 let w = params.weights.as_ref().map_or(1.0, |ws| ws[e]);
401 let dlen = (dx * dx + dy * dy).sqrt() * w;
402 dispx[v] -= dx * dlen;
403 dispy[v] -= dy * dlen;
404 dispx[u] += dx * dlen;
405 dispy[u] += dy * dlen;
406 }
407
408 for v in 0..vcount {
410 let dx = dispx[v] + rng.gen_unit() * 2e-9 - 1e-9;
411 let dy = dispy[v] + rng.gen_unit() * 2e-9 - 1e-9;
412 let displen = (dx * dx + dy * dy).sqrt();
413 let (cdx, cdy) = if displen > temp {
414 (dx * temp / displen, dy * temp / displen)
415 } else {
416 (dx, dy)
417 };
418 if displen > 0.0 {
419 pos[v].0 += cdx;
420 pos[v].1 += cdy;
421 }
422 apply_bounds_2d(&mut pos[v], v, ¶ms.bounds);
423 }
424
425 temp -= diff_temp;
426 }
427
428 Ok(pos)
429}
430
431fn fr_naive_3d(
432 graph: &Graph,
433 params: &FrParams3d,
434 start_temp: f64,
435) -> IgraphResult<Vec<(f64, f64, f64)>> {
436 let vcount = graph.vcount() as usize;
437 let ecount = graph.ecount() as usize;
438
439 let cc = connected_components(graph)?;
440 let connected = cc.count == 1;
441 let big_c = if connected {
442 0.0
443 } else {
444 vcount as f64 * (vcount as f64).sqrt()
445 };
446
447 let mut pos: Vec<(f64, f64, f64)> = if let Some(ref sc) = params.seed_coords {
448 sc.clone()
449 } else {
450 init_random_bounded_3d(vcount, ¶ms.bounds, params.rng_seed)
451 };
452
453 let mut rng = SplitMix64::new(params.rng_seed.wrapping_add(0xDEAD_BEEF));
454 let mut dispx = vec![0.0f64; vcount];
455 let mut dispy = vec![0.0f64; vcount];
456 let mut dispz = vec![0.0f64; vcount];
457 let niter = params.niter;
458 let diff_temp = if niter > 0 {
459 start_temp / niter as f64
460 } else {
461 0.0
462 };
463 let mut temp = start_temp;
464
465 for _ in 0..niter {
466 dispx.iter_mut().for_each(|d| *d = 0.0);
467 dispy.iter_mut().for_each(|d| *d = 0.0);
468 dispz.iter_mut().for_each(|d| *d = 0.0);
469
470 if connected {
472 for v in 0..vcount {
473 for u in (v + 1)..vcount {
474 let mut dx = pos[v].0 - pos[u].0;
475 let mut dy = pos[v].1 - pos[u].1;
476 let mut dz = pos[v].2 - pos[u].2;
477 let mut dlen = dx * dx + dy * dy + dz * dz;
478 while dlen == 0.0 {
479 dx = rng.gen_unit() * 2e-9 - 1e-9;
480 dy = rng.gen_unit() * 2e-9 - 1e-9;
481 dz = rng.gen_unit() * 2e-9 - 1e-9;
482 dlen = dx * dx + dy * dy + dz * dz;
483 }
484 dispx[v] += dx / dlen;
485 dispy[v] += dy / dlen;
486 dispz[v] += dz / dlen;
487 dispx[u] -= dx / dlen;
488 dispy[u] -= dy / dlen;
489 dispz[u] -= dz / dlen;
490 }
491 }
492 } else {
493 for v in 0..vcount {
494 for u in (v + 1)..vcount {
495 let mut dx = pos[v].0 - pos[u].0;
496 let mut dy = pos[v].1 - pos[u].1;
497 let mut dz = pos[v].2 - pos[u].2;
498 let mut dlen = dx * dx + dy * dy + dz * dz;
499 while dlen == 0.0 {
500 dx = rng.gen_unit() * 2e-9 - 1e-9;
501 dy = rng.gen_unit() * 2e-9 - 1e-9;
502 dz = rng.gen_unit() * 2e-9 - 1e-9;
503 dlen = dx * dx + dy * dy + dz * dz;
504 }
505 let rdlen = dlen.sqrt();
506 let factor = (big_c - dlen * rdlen) / (dlen * big_c);
507 dispx[v] += dx * factor;
508 dispy[v] += dy * factor;
509 dispz[v] += dz * factor;
510 dispx[u] -= dx * factor;
511 dispy[u] -= dy * factor;
512 dispz[u] -= dz * factor;
513 }
514 }
515 }
516
517 for e in 0..ecount {
519 let (from, to) = graph.edge(e as u32)?;
520 let v = from as usize;
521 let u = to as usize;
522 let dx = pos[v].0 - pos[u].0;
523 let dy = pos[v].1 - pos[u].1;
524 let dz = pos[v].2 - pos[u].2;
525 let w = params.weights.as_ref().map_or(1.0, |ws| ws[e]);
526 let dlen = (dx * dx + dy * dy + dz * dz).sqrt() * w;
527 dispx[v] -= dx * dlen;
528 dispy[v] -= dy * dlen;
529 dispz[v] -= dz * dlen;
530 dispx[u] += dx * dlen;
531 dispy[u] += dy * dlen;
532 dispz[u] += dz * dlen;
533 }
534
535 for v in 0..vcount {
537 let dx = dispx[v] + rng.gen_unit() * 2e-9 - 1e-9;
538 let dy = dispy[v] + rng.gen_unit() * 2e-9 - 1e-9;
539 let dz = dispz[v] + rng.gen_unit() * 2e-9 - 1e-9;
540 let displen = (dx * dx + dy * dy + dz * dz).sqrt();
541 let (cdx, cdy, cdz) = if displen > temp {
542 let scale = temp / displen;
543 (dx * scale, dy * scale, dz * scale)
544 } else {
545 (dx, dy, dz)
546 };
547 if displen > 0.0 {
548 pos[v].0 += cdx;
549 pos[v].1 += cdy;
550 pos[v].2 += cdz;
551 }
552 apply_bounds_3d(&mut pos[v], v, ¶ms.bounds);
553 }
554
555 temp -= diff_temp;
556 }
557
558 Ok(pos)
559}
560
561fn validate_bounds_2d(vcount: usize, bounds: &FrBounds) -> IgraphResult<()> {
564 if let Some(ref v) = bounds.minx {
565 if v.len() != vcount {
566 return Err(IgraphError::InvalidArgument("minx length mismatch".into()));
567 }
568 }
569 if let Some(ref v) = bounds.maxx {
570 if v.len() != vcount {
571 return Err(IgraphError::InvalidArgument("maxx length mismatch".into()));
572 }
573 }
574 if let Some(ref v) = bounds.miny {
575 if v.len() != vcount {
576 return Err(IgraphError::InvalidArgument("miny length mismatch".into()));
577 }
578 }
579 if let Some(ref v) = bounds.maxy {
580 if v.len() != vcount {
581 return Err(IgraphError::InvalidArgument("maxy length mismatch".into()));
582 }
583 }
584 if let (Some(lo), Some(hi)) = (&bounds.minx, &bounds.maxx) {
585 if lo.iter().zip(hi.iter()).any(|(a, b)| a > b) {
586 return Err(IgraphError::InvalidArgument(
587 "minx must not exceed maxx".into(),
588 ));
589 }
590 }
591 if let (Some(lo), Some(hi)) = (&bounds.miny, &bounds.maxy) {
592 if lo.iter().zip(hi.iter()).any(|(a, b)| a > b) {
593 return Err(IgraphError::InvalidArgument(
594 "miny must not exceed maxy".into(),
595 ));
596 }
597 }
598 Ok(())
599}
600
601fn validate_bounds_3d(vcount: usize, bounds: &FrBounds3d) -> IgraphResult<()> {
602 for (name, vec) in [
603 ("minx", &bounds.minx),
604 ("maxx", &bounds.maxx),
605 ("miny", &bounds.miny),
606 ("maxy", &bounds.maxy),
607 ("minz", &bounds.minz),
608 ("maxz", &bounds.maxz),
609 ] {
610 if let Some(v) = vec {
611 if v.len() != vcount {
612 let msg = match name {
613 "minx" => "minx length mismatch",
614 "maxx" => "maxx length mismatch",
615 "miny" => "miny length mismatch",
616 "maxy" => "maxy length mismatch",
617 "minz" => "minz length mismatch",
618 _ => "maxz length mismatch",
619 };
620 return Err(IgraphError::InvalidArgument(msg.into()));
621 }
622 }
623 }
624 if let (Some(lo), Some(hi)) = (&bounds.minx, &bounds.maxx) {
625 if lo.iter().zip(hi.iter()).any(|(a, b)| a > b) {
626 return Err(IgraphError::InvalidArgument(
627 "minx must not exceed maxx".into(),
628 ));
629 }
630 }
631 if let (Some(lo), Some(hi)) = (&bounds.miny, &bounds.maxy) {
632 if lo.iter().zip(hi.iter()).any(|(a, b)| a > b) {
633 return Err(IgraphError::InvalidArgument(
634 "miny must not exceed maxy".into(),
635 ));
636 }
637 }
638 if let (Some(lo), Some(hi)) = (&bounds.minz, &bounds.maxz) {
639 if lo.iter().zip(hi.iter()).any(|(a, b)| a > b) {
640 return Err(IgraphError::InvalidArgument(
641 "minz must not exceed maxz".into(),
642 ));
643 }
644 }
645 Ok(())
646}
647
648fn init_random_bounded_2d(vcount: usize, bounds: &FrBounds, seed: u64) -> Vec<(f64, f64)> {
649 let mut rng = SplitMix64::new(seed);
650 (0..vcount)
651 .map(|i| {
652 let lo_x = bounds.minx.as_ref().map_or(-1.0, |v| v[i]);
653 let hi_x = bounds.maxx.as_ref().map_or(1.0, |v| v[i]);
654 let lo_y = bounds.miny.as_ref().map_or(-1.0, |v| v[i]);
655 let hi_y = bounds.maxy.as_ref().map_or(1.0, |v| v[i]);
656 let x = lo_x + rng.gen_unit() * (hi_x - lo_x);
657 let y = lo_y + rng.gen_unit() * (hi_y - lo_y);
658 (x, y)
659 })
660 .collect()
661}
662
663fn init_random_bounded_3d(vcount: usize, bounds: &FrBounds3d, seed: u64) -> Vec<(f64, f64, f64)> {
664 let mut rng = SplitMix64::new(seed);
665 (0..vcount)
666 .map(|i| {
667 let lo_x = bounds.minx.as_ref().map_or(-1.0, |v| v[i]);
668 let hi_x = bounds.maxx.as_ref().map_or(1.0, |v| v[i]);
669 let lo_y = bounds.miny.as_ref().map_or(-1.0, |v| v[i]);
670 let hi_y = bounds.maxy.as_ref().map_or(1.0, |v| v[i]);
671 let lo_z = bounds.minz.as_ref().map_or(-1.0, |v| v[i]);
672 let hi_z = bounds.maxz.as_ref().map_or(1.0, |v| v[i]);
673 let x = lo_x + rng.gen_unit() * (hi_x - lo_x);
674 let y = lo_y + rng.gen_unit() * (hi_y - lo_y);
675 let z = lo_z + rng.gen_unit() * (hi_z - lo_z);
676 (x, y, z)
677 })
678 .collect()
679}
680
681fn apply_bounds_2d(pos: &mut (f64, f64), i: usize, bounds: &FrBounds) {
682 if let Some(ref v) = bounds.minx {
683 if pos.0 < v[i] {
684 pos.0 = v[i];
685 }
686 }
687 if let Some(ref v) = bounds.maxx {
688 if pos.0 > v[i] {
689 pos.0 = v[i];
690 }
691 }
692 if let Some(ref v) = bounds.miny {
693 if pos.1 < v[i] {
694 pos.1 = v[i];
695 }
696 }
697 if let Some(ref v) = bounds.maxy {
698 if pos.1 > v[i] {
699 pos.1 = v[i];
700 }
701 }
702}
703
704fn apply_bounds_3d(pos: &mut (f64, f64, f64), i: usize, bounds: &FrBounds3d) {
705 if let Some(ref v) = bounds.minx {
706 if pos.0 < v[i] {
707 pos.0 = v[i];
708 }
709 }
710 if let Some(ref v) = bounds.maxx {
711 if pos.0 > v[i] {
712 pos.0 = v[i];
713 }
714 }
715 if let Some(ref v) = bounds.miny {
716 if pos.1 < v[i] {
717 pos.1 = v[i];
718 }
719 }
720 if let Some(ref v) = bounds.maxy {
721 if pos.1 > v[i] {
722 pos.1 = v[i];
723 }
724 }
725 if let Some(ref v) = bounds.minz {
726 if pos.2 < v[i] {
727 pos.2 = v[i];
728 }
729 }
730 if let Some(ref v) = bounds.maxz {
731 if pos.2 > v[i] {
732 pos.2 = v[i];
733 }
734 }
735}
736
737struct Grid2d {
739 cells: Vec<Vec<usize>>,
740 nx: usize,
741 ny: usize,
742 x_min: f64,
743 y_min: f64,
744 cellsize: f64,
745}
746
747impl Grid2d {
748 fn build(
749 pos: &[(f64, f64)],
750 x_min: f64,
751 x_max: f64,
752 y_min: f64,
753 y_max: f64,
754 cellsize: f64,
755 ) -> Self {
756 let nx = ((x_max - x_min) / cellsize).ceil().max(1.0) as usize;
757 let ny = ((y_max - y_min) / cellsize).ceil().max(1.0) as usize;
758 let mut cells = vec![Vec::new(); nx * ny];
759 for (i, &(x, y)) in pos.iter().enumerate() {
760 let cx = ((x - x_min) / cellsize).floor() as isize;
761 let cy = ((y - y_min) / cellsize).floor() as isize;
762 let cx = cx.clamp(0, nx as isize - 1) as usize;
763 let cy = cy.clamp(0, ny as isize - 1) as usize;
764 cells[cy * nx + cx].push(i);
765 }
766 Grid2d {
767 cells,
768 nx,
769 ny,
770 x_min,
771 y_min,
772 cellsize,
773 }
774 }
775
776 fn nearby_vertices(&self, v: usize, pos: &[(f64, f64)]) -> Vec<usize> {
777 let (x, y) = pos[v];
778 let cx = ((x - self.x_min) / self.cellsize).floor() as isize;
779 let cy = ((y - self.y_min) / self.cellsize).floor() as isize;
780 let mut result = Vec::new();
781 for dy in -1..=1i64 {
782 for dx in -1..=1i64 {
783 let nx = cx + dx as isize;
784 let ny = cy + dy as isize;
785 if nx >= 0 && nx < self.nx as isize && ny >= 0 && ny < self.ny as isize {
786 let idx = ny as usize * self.nx + nx as usize;
787 for &u in &self.cells[idx] {
788 if u != v {
789 result.push(u);
790 }
791 }
792 }
793 }
794 }
795 result
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 fn make_cycle(n: u32) -> Graph {
804 let mut g = Graph::with_vertices(n);
805 for i in 0..n {
806 g.add_edge(i, (i + 1) % n).unwrap();
807 }
808 g
809 }
810
811 fn make_path(n: u32) -> Graph {
812 let mut g = Graph::with_vertices(n);
813 for i in 0..n - 1 {
814 g.add_edge(i, i + 1).unwrap();
815 }
816 g
817 }
818
819 #[test]
820 fn empty_graph() {
821 let g = Graph::with_vertices(0);
822 let coords = layout_fruchterman_reingold(&g, &FrParams::default()).unwrap();
823 assert!(coords.is_empty());
824 }
825
826 #[test]
827 fn single_vertex() {
828 let g = Graph::with_vertices(1);
829 let coords = layout_fruchterman_reingold(&g, &FrParams::default()).unwrap();
830 assert_eq!(coords.len(), 1);
831 }
832
833 #[test]
834 fn cycle5_produces_finite_coords() {
835 let g = make_cycle(5);
836 let coords = layout_fruchterman_reingold(&g, &FrParams::default()).unwrap();
837 assert_eq!(coords.len(), 5);
838 for &(x, y) in &coords {
839 assert!(x.is_finite());
840 assert!(y.is_finite());
841 }
842 }
843
844 #[test]
845 fn disconnected_graph_doesnt_panic() {
846 let mut g = Graph::with_vertices(6);
847 g.add_edge(0, 1).unwrap();
848 g.add_edge(2, 3).unwrap();
849 g.add_edge(4, 5).unwrap();
850 let coords = layout_fruchterman_reingold(&g, &FrParams::default()).unwrap();
851 assert_eq!(coords.len(), 6);
852 for &(x, y) in &coords {
853 assert!(x.is_finite());
854 assert!(y.is_finite());
855 }
856 }
857
858 #[test]
859 fn grid_mode_works() {
860 let g = make_path(10);
861 let params = FrParams {
862 grid: FrGrid::Grid,
863 niter: 50,
864 ..Default::default()
865 };
866 let coords = layout_fruchterman_reingold(&g, ¶ms).unwrap();
867 assert_eq!(coords.len(), 10);
868 for &(x, y) in &coords {
869 assert!(x.is_finite());
870 assert!(y.is_finite());
871 }
872 }
873
874 #[test]
875 fn weighted_edges() {
876 let g = make_cycle(4);
877 let params = FrParams {
878 weights: Some(vec![1.0, 2.0, 1.0, 2.0]),
879 niter: 100,
880 ..Default::default()
881 };
882 let coords = layout_fruchterman_reingold(&g, ¶ms).unwrap();
883 assert_eq!(coords.len(), 4);
884 }
885
886 #[test]
887 fn invalid_weights_rejected() {
888 let g = make_cycle(4);
889 let params = FrParams {
890 weights: Some(vec![1.0, -1.0, 1.0, 1.0]),
891 ..Default::default()
892 };
893 assert!(layout_fruchterman_reingold(&g, ¶ms).is_err());
894 }
895
896 #[test]
897 fn bounds_constrain_output() {
898 let g = make_path(5);
899 let params = FrParams {
900 niter: 200,
901 bounds: FrBounds {
902 minx: Some(vec![0.0; 5]),
903 maxx: Some(vec![1.0; 5]),
904 miny: Some(vec![0.0; 5]),
905 maxy: Some(vec![1.0; 5]),
906 },
907 ..Default::default()
908 };
909 let coords = layout_fruchterman_reingold(&g, ¶ms).unwrap();
910 for &(x, y) in &coords {
911 assert!((0.0..=1.0).contains(&x), "x={x} out of bounds");
912 assert!((0.0..=1.0).contains(&y), "y={y} out of bounds");
913 }
914 }
915
916 #[test]
917 fn layout_3d_basic() {
918 let g = make_cycle(5);
919 let coords = layout_fruchterman_reingold_3d(&g, &FrParams3d::default()).unwrap();
920 assert_eq!(coords.len(), 5);
921 for &(x, y, z) in &coords {
922 assert!(x.is_finite());
923 assert!(y.is_finite());
924 assert!(z.is_finite());
925 }
926 }
927
928 #[test]
929 fn seed_coords_used() {
930 let g = make_path(3);
931 let seed = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)];
932 let params = FrParams {
933 niter: 0,
934 seed_coords: Some(seed.clone()),
935 ..Default::default()
936 };
937 let coords = layout_fruchterman_reingold(&g, ¶ms).unwrap();
938 assert_eq!(coords, seed);
939 }
940}