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