1use crate::algorithms::paths::floyd_warshall::floyd_warshall_distances;
8use crate::core::{Graph, IgraphError, IgraphResult};
9
10const KK_EPS: f64 = 1e-13;
12const KK3D_EPS: f64 = 1e-8;
13
14#[derive(Debug, Clone)]
16pub struct KkParams {
17 pub maxiter: usize,
19 pub epsilon: f64,
22 pub kkconst: f64,
24 pub seed: Option<Vec<[f64; 2]>>,
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct KkBounds {
32 pub minx: Option<Vec<f64>>,
33 pub maxx: Option<Vec<f64>>,
34 pub miny: Option<Vec<f64>>,
35 pub maxy: Option<Vec<f64>>,
36}
37
38#[derive(Debug, Clone)]
40pub struct KkParams3d {
41 pub maxiter: usize,
43 pub epsilon: f64,
46 pub kkconst: f64,
48 pub seed: Option<Vec<[f64; 3]>>,
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct KkBounds3d {
56 pub minx: Option<Vec<f64>>,
57 pub maxx: Option<Vec<f64>>,
58 pub miny: Option<Vec<f64>>,
59 pub maxy: Option<Vec<f64>>,
60 pub minz: Option<Vec<f64>>,
61 pub maxz: Option<Vec<f64>>,
62}
63
64pub fn layout_kamada_kawai(
95 graph: &Graph,
96 weights: Option<&[f64]>,
97 params: &KkParams,
98 bounds: Option<&KkBounds>,
99) -> IgraphResult<Vec<[f64; 2]>> {
100 let n = graph.vcount() as usize;
101
102 if params.kkconst <= 0.0 {
103 return Err(IgraphError::InvalidArgument(
104 "kkconst must be positive".into(),
105 ));
106 }
107 if let Some(ws) = weights {
108 if ws.len() != graph.ecount() {
109 return Err(IgraphError::InvalidArgument(
110 "weights length must equal edge count".into(),
111 ));
112 }
113 if graph.ecount() > 0 {
114 for &w in ws {
115 if w <= 0.0 || w.is_nan() {
116 return Err(IgraphError::InvalidArgument(
117 "weights must be positive for Kamada-Kawai layout".into(),
118 ));
119 }
120 }
121 }
122 }
123
124 validate_bounds_2d(n, bounds)?;
125
126 let mut pos = match ¶ms.seed {
128 Some(seed) => {
129 if seed.len() != n {
130 return Err(IgraphError::InvalidArgument(
131 "seed length must equal vertex count".into(),
132 ));
133 }
134 seed.clone()
135 }
136 None => {
137 if bounds.is_some() {
138 initial_bounded_2d(n, bounds)
139 } else {
140 initial_circle_2d(n)
141 }
142 }
143 };
144
145 if let Some(b) = bounds {
147 clamp_positions_2d(&mut pos, b);
148 }
149
150 if n <= 1 {
151 return Ok(pos);
152 }
153
154 let mut dij = all_pairs_distances(graph, weights)?;
156
157 let max_dij = find_max_finite(&mut dij, n);
159 if max_dij == 0.0 {
160 return Ok(pos);
161 }
162
163 let l0 = (n as f64).sqrt();
164 let big_l = l0 / max_dij;
165
166 let mut kij = vec![0.0_f64; n * n];
168 let mut lij = vec![0.0_f64; n * n];
169 for i in 0..n {
170 for j in 0..n {
171 if i == j {
172 continue;
173 }
174 let d = dij[i * n + j];
175 let d2 = d * d;
176 if d2 > 0.0 {
177 kij[i * n + j] = params.kkconst / d2;
178 lij[i * n + j] = big_l * d;
179 }
180 }
181 }
182
183 let mut d1 = vec![0.0_f64; n];
185 let mut d2 = vec![0.0_f64; n];
186 for m in 0..n {
187 let (gx, gy) = gradient_2d(m, &pos, &kij, &lij, n);
188 d1[m] = gx;
189 d2[m] = gy;
190 }
191
192 for _iter in 0..params.maxiter {
194 let (m, max_delta) = select_max_delta_2d(&d1, &d2, n);
196 if max_delta < params.epsilon {
197 break;
198 }
199
200 let old_x = pos[m][0];
201 let old_y = pos[m][1];
202
203 let mut axx = 0.0_f64;
205 let mut axy = 0.0_f64;
206 let mut ayy = 0.0_f64;
207 for i in 0..n {
208 if i == m {
209 continue;
210 }
211 let dx = old_x - pos[i][0];
212 let dy = old_y - pos[i][1];
213 let dist = (dx * dx + dy * dy).sqrt();
214 let den = dist * (dx * dx + dy * dy);
215 let k_mi = kij[m * n + i];
216 let l_mi = lij[m * n + i];
217 axx += k_mi * (1.0 - l_mi * dy * dy / den);
218 axy += k_mi * l_mi * dx * dy / den;
219 ayy += k_mi * (1.0 - l_mi * dx * dx / den);
220 }
221
222 let ax = -d1[m];
223 let ay = -d2[m];
224
225 let (delta_x, delta_y) = if ax * ax + ay * ay < KK_EPS * KK_EPS {
227 (0.0, 0.0)
228 } else {
229 let det = axx * ayy - axy * axy;
230 if det.abs() < f64::EPSILON {
231 (0.0, 0.0)
232 } else {
233 ((ax * ayy - ay * axy) / det, (axx * ay - axy * ax) / det)
234 }
235 };
236
237 let mut new_x = old_x + delta_x;
238 let mut new_y = old_y + delta_y;
239
240 if let Some(b) = bounds {
242 if let Some(ref minx) = b.minx {
243 if new_x < minx[m] {
244 new_x = minx[m];
245 }
246 }
247 if let Some(ref maxx) = b.maxx {
248 if new_x > maxx[m] {
249 new_x = maxx[m];
250 }
251 }
252 if let Some(ref miny) = b.miny {
253 if new_y < miny[m] {
254 new_y = miny[m];
255 }
256 }
257 if let Some(ref maxy) = b.maxy {
258 if new_y > maxy[m] {
259 new_y = maxy[m];
260 }
261 }
262 }
263
264 d1[m] = 0.0;
266 d2[m] = 0.0;
267 for i in 0..n {
268 if i == m {
269 continue;
270 }
271 let old_dx = old_x - pos[i][0];
272 let old_dy = old_y - pos[i][1];
273 let old_dist = (old_dx * old_dx + old_dy * old_dy).sqrt();
274 let new_dx = new_x - pos[i][0];
275 let new_dy = new_y - pos[i][1];
276 let new_dist = (new_dx * new_dx + new_dy * new_dy).sqrt();
277
278 let k_mi = kij[m * n + i];
279 let l_mi = lij[m * n + i];
280
281 d1[i] -= k_mi * (-old_dx + l_mi * old_dx / old_dist);
283 d2[i] -= k_mi * (-old_dy + l_mi * old_dy / old_dist);
284 d1[i] += k_mi * (-new_dx + l_mi * new_dx / new_dist);
285 d2[i] += k_mi * (-new_dy + l_mi * new_dy / new_dist);
286
287 d1[m] += k_mi * (new_dx - l_mi * new_dx / new_dist);
289 d2[m] += k_mi * (new_dy - l_mi * new_dy / new_dist);
290 }
291
292 pos[m] = [new_x, new_y];
293 }
294
295 Ok(pos)
296}
297
298pub fn layout_kamada_kawai_3d(
320 graph: &Graph,
321 weights: Option<&[f64]>,
322 params: &KkParams3d,
323 bounds: Option<&KkBounds3d>,
324) -> IgraphResult<Vec<[f64; 3]>> {
325 let n = graph.vcount() as usize;
326
327 if params.kkconst <= 0.0 {
328 return Err(IgraphError::InvalidArgument(
329 "kkconst must be positive".into(),
330 ));
331 }
332 if let Some(ws) = weights {
333 if ws.len() != graph.ecount() {
334 return Err(IgraphError::InvalidArgument(
335 "weights length must equal edge count".into(),
336 ));
337 }
338 if graph.ecount() > 0 {
339 for &w in ws {
340 if w <= 0.0 || w.is_nan() {
341 return Err(IgraphError::InvalidArgument(
342 "weights must be positive for Kamada-Kawai layout".into(),
343 ));
344 }
345 }
346 }
347 }
348
349 validate_bounds_3d(n, bounds)?;
350
351 let mut pos = match ¶ms.seed {
352 Some(seed) => {
353 if seed.len() != n {
354 return Err(IgraphError::InvalidArgument(
355 "seed length must equal vertex count".into(),
356 ));
357 }
358 seed.clone()
359 }
360 None => initial_sphere_3d(n),
361 };
362
363 if n <= 1 {
364 return Ok(pos);
365 }
366
367 let mut dij = all_pairs_distances(graph, weights)?;
368 let max_dij = find_max_finite(&mut dij, n);
369 if max_dij == 0.0 {
370 return Ok(pos);
371 }
372
373 let l0 = (n as f64).sqrt();
374 let big_l = l0 / max_dij;
375
376 let mut kij = vec![0.0_f64; n * n];
377 let mut lij = vec![0.0_f64; n * n];
378 for i in 0..n {
379 for j in 0..n {
380 if i == j {
381 continue;
382 }
383 let d = dij[i * n + j];
384 let d2 = d * d;
385 if d2 > 0.0 {
386 kij[i * n + j] = params.kkconst / d2;
387 lij[i * n + j] = big_l * d;
388 }
389 }
390 }
391
392 let mut d1 = vec![0.0_f64; n];
394 let mut d2 = vec![0.0_f64; n];
395 let mut d3 = vec![0.0_f64; n];
396 for m in 0..n {
397 let (gx, gy, gz) = gradient_3d(m, &pos, &kij, &lij, n);
398 d1[m] = gx;
399 d2[m] = gy;
400 d3[m] = gz;
401 }
402
403 for _iter in 0..params.maxiter {
404 let (m, max_delta) = select_max_delta_3d(&d1, &d2, &d3, n);
405 if max_delta < params.epsilon {
406 break;
407 }
408
409 let old_x = pos[m][0];
410 let old_y = pos[m][1];
411 let old_z = pos[m][2];
412
413 let mut axx = 0.0_f64;
415 let mut axy = 0.0_f64;
416 let mut axz = 0.0_f64;
417 let mut ayy = 0.0_f64;
418 let mut ayz = 0.0_f64;
419 let mut azz = 0.0_f64;
420
421 for i in 0..n {
422 if i == m {
423 continue;
424 }
425 let dx = old_x - pos[i][0];
426 let dy = old_y - pos[i][1];
427 let dz = old_z - pos[i][2];
428 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
429 let den = dist * (dx * dx + dy * dy + dz * dz);
430 let k_mi = kij[m * n + i];
431 let l_mi = lij[m * n + i];
432
433 axx += k_mi * (1.0 - l_mi * (dy * dy + dz * dz) / den);
434 ayy += k_mi * (1.0 - l_mi * (dx * dx + dz * dz) / den);
435 azz += k_mi * (1.0 - l_mi * (dx * dx + dy * dy) / den);
436 axy += k_mi * l_mi * dx * dy / den;
437 axz += k_mi * l_mi * dx * dz / den;
438 ayz += k_mi * l_mi * dy * dz / den;
439 }
440
441 let ax = -d1[m];
442 let ay = -d2[m];
443 let az = -d3[m];
444
445 let (delta_x, delta_y, delta_z) = if ax * ax + ay * ay + az * az < KK3D_EPS * KK3D_EPS {
447 (0.0, 0.0, 0.0)
448 } else {
449 let detnum = det3(axx, axy, axz, axy, ayy, ayz, axz, ayz, azz);
450 if detnum.abs() < f64::EPSILON {
451 (0.0, 0.0, 0.0)
452 } else {
453 let dx = det3(ax, ay, az, axy, ayy, ayz, axz, ayz, azz) / detnum;
454 let dy = det3(axx, axy, axz, ax, ay, az, axz, ayz, azz) / detnum;
455 let dz = det3(axx, axy, axz, axy, ayy, ayz, ax, ay, az) / detnum;
456 (dx, dy, dz)
457 }
458 };
459
460 let mut new_x = old_x + delta_x;
461 let mut new_y = old_y + delta_y;
462 let mut new_z = old_z + delta_z;
463
464 if let Some(b) = bounds {
466 if let Some(ref v) = b.minx {
467 if new_x < v[m] {
468 new_x = v[m];
469 }
470 }
471 if let Some(ref v) = b.maxx {
472 if new_x > v[m] {
473 new_x = v[m];
474 }
475 }
476 if let Some(ref v) = b.miny {
477 if new_y < v[m] {
478 new_y = v[m];
479 }
480 }
481 if let Some(ref v) = b.maxy {
482 if new_y > v[m] {
483 new_y = v[m];
484 }
485 }
486 if let Some(ref v) = b.minz {
487 if new_z < v[m] {
488 new_z = v[m];
489 }
490 }
491 if let Some(ref v) = b.maxz {
492 if new_z > v[m] {
493 new_z = v[m];
494 }
495 }
496 }
497
498 d1[m] = 0.0;
500 d2[m] = 0.0;
501 d3[m] = 0.0;
502 for i in 0..n {
503 if i == m {
504 continue;
505 }
506 let old_dx = old_x - pos[i][0];
507 let old_dy = old_y - pos[i][1];
508 let old_dz = old_z - pos[i][2];
509 let old_dist = (old_dx * old_dx + old_dy * old_dy + old_dz * old_dz).sqrt();
510 let new_dx = new_x - pos[i][0];
511 let new_dy = new_y - pos[i][1];
512 let new_dz = new_z - pos[i][2];
513 let new_dist = (new_dx * new_dx + new_dy * new_dy + new_dz * new_dz).sqrt();
514
515 let k_mi = kij[m * n + i];
516 let l_mi = lij[m * n + i];
517
518 d1[i] -= k_mi * (-old_dx + l_mi * old_dx / old_dist);
519 d2[i] -= k_mi * (-old_dy + l_mi * old_dy / old_dist);
520 d3[i] -= k_mi * (-old_dz + l_mi * old_dz / old_dist);
521 d1[i] += k_mi * (-new_dx + l_mi * new_dx / new_dist);
522 d2[i] += k_mi * (-new_dy + l_mi * new_dy / new_dist);
523 d3[i] += k_mi * (-new_dz + l_mi * new_dz / new_dist);
524
525 d1[m] += k_mi * (new_dx - l_mi * new_dx / new_dist);
526 d2[m] += k_mi * (new_dy - l_mi * new_dy / new_dist);
527 d3[m] += k_mi * (new_dz - l_mi * new_dz / new_dist);
528 }
529
530 pos[m] = [new_x, new_y, new_z];
531 }
532
533 Ok(pos)
534}
535
536impl KkParams {
541 pub fn default_for(n: usize) -> Self {
543 Self {
544 maxiter: 10 * n.max(1),
545 epsilon: 0.0,
546 kkconst: n as f64,
547 seed: None,
548 }
549 }
550}
551
552impl KkParams3d {
553 pub fn default_for(n: usize) -> Self {
555 Self {
556 maxiter: 10 * n.max(1),
557 epsilon: 0.0,
558 kkconst: n as f64,
559 seed: None,
560 }
561 }
562}
563
564fn initial_circle_2d(n: usize) -> Vec<[f64; 2]> {
565 let l0 = (n as f64).sqrt();
566 let scale = 0.36 * l0;
567 let mut pos = Vec::with_capacity(n);
568 for i in 0..n {
569 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
570 pos.push([scale * angle.cos(), scale * angle.sin()]);
571 }
572 pos
573}
574
575fn initial_sphere_3d(n: usize) -> Vec<[f64; 3]> {
576 let l0 = (n as f64).sqrt();
577 let scale = 0.36 * l0;
578 let mut pos = Vec::with_capacity(n);
579 let golden_ratio = 0.5 * (1.0 + 5.0_f64.sqrt());
580 for i in 0..n {
581 let theta = 2.0 * std::f64::consts::PI * (i as f64) / golden_ratio;
582 let phi = (1.0 - 2.0 * (i as f64 + 0.5) / n as f64).acos();
583 pos.push([
584 scale * phi.sin() * theta.cos(),
585 scale * phi.sin() * theta.sin(),
586 scale * phi.cos(),
587 ]);
588 }
589 pos
590}
591
592fn initial_bounded_2d(n: usize, bounds: Option<&KkBounds>) -> Vec<[f64; 2]> {
593 let mut pos = Vec::with_capacity(n);
594 for i in 0..n {
595 let t = i as f64 / n.max(1) as f64;
596 let angle = 2.0 * std::f64::consts::PI * t;
597 let (mut x, mut y) = (angle.cos(), angle.sin());
598 if let Some(b) = bounds {
600 let (lo_x, hi_x) = bounds_range_x(b, i);
601 let (lo_y, hi_y) = bounds_range_y(b, i);
602 x = lo_x + (x + 1.0) * 0.5 * (hi_x - lo_x);
603 y = lo_y + (y + 1.0) * 0.5 * (hi_y - lo_y);
604 }
605 pos.push([x, y]);
606 }
607 pos
608}
609
610fn bounds_range_x(b: &KkBounds, i: usize) -> (f64, f64) {
611 let lo = b.minx.as_ref().map_or(-1.0, |v| v[i]);
612 let hi = b.maxx.as_ref().map_or(1.0, |v| v[i]);
613 (lo, hi)
614}
615
616fn bounds_range_y(b: &KkBounds, i: usize) -> (f64, f64) {
617 let lo = b.miny.as_ref().map_or(-1.0, |v| v[i]);
618 let hi = b.maxy.as_ref().map_or(1.0, |v| v[i]);
619 (lo, hi)
620}
621
622fn clamp_positions_2d(pos: &mut [[f64; 2]], bounds: &KkBounds) {
623 for (i, p) in pos.iter_mut().enumerate() {
624 if let Some(ref v) = bounds.minx {
625 if p[0] < v[i] {
626 p[0] = v[i];
627 }
628 }
629 if let Some(ref v) = bounds.maxx {
630 if p[0] > v[i] {
631 p[0] = v[i];
632 }
633 }
634 if let Some(ref v) = bounds.miny {
635 if p[1] < v[i] {
636 p[1] = v[i];
637 }
638 }
639 if let Some(ref v) = bounds.maxy {
640 if p[1] > v[i] {
641 p[1] = v[i];
642 }
643 }
644 }
645}
646
647fn all_pairs_distances(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
648 let n = graph.vcount() as usize;
649 let dist_matrix = floyd_warshall_distances(graph, weights)?;
650
651 let mut flat = vec![f64::INFINITY; n * n];
652 for i in 0..n {
653 for j in 0..n {
654 if let Some(d) = dist_matrix[i][j] {
655 flat[i * n + j] = d;
656 }
657 }
658 }
659 Ok(flat)
660}
661
662fn find_max_finite(dij: &mut [f64], n: usize) -> f64 {
664 let mut max_d = 0.0_f64;
665 for i in 0..n {
666 for j in (i + 1)..n {
667 let d = dij[i * n + j];
668 if d.is_finite() && d > max_d {
669 max_d = d;
670 }
671 }
672 }
673 for val in dij.iter_mut() {
675 if !val.is_finite() {
676 *val = max_d;
677 }
678 }
679 max_d
680}
681
682fn gradient_2d(m: usize, pos: &[[f64; 2]], kij: &[f64], lij: &[f64], n: usize) -> (f64, f64) {
683 let mut gx = 0.0;
684 let mut gy = 0.0;
685 for i in 0..n {
686 if i == m {
687 continue;
688 }
689 let dx = pos[m][0] - pos[i][0];
690 let dy = pos[m][1] - pos[i][1];
691 let dist = (dx * dx + dy * dy).sqrt();
692 if dist < f64::EPSILON {
693 continue;
694 }
695 let k = kij[m * n + i];
696 let l = lij[m * n + i];
697 gx += k * (dx - l * dx / dist);
698 gy += k * (dy - l * dy / dist);
699 }
700 (gx, gy)
701}
702
703fn gradient_3d(m: usize, pos: &[[f64; 3]], kij: &[f64], lij: &[f64], n: usize) -> (f64, f64, f64) {
704 let mut gx = 0.0;
705 let mut gy = 0.0;
706 let mut gz = 0.0;
707 for i in 0..n {
708 if i == m {
709 continue;
710 }
711 let dx = pos[m][0] - pos[i][0];
712 let dy = pos[m][1] - pos[i][1];
713 let dz = pos[m][2] - pos[i][2];
714 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
715 if dist < f64::EPSILON {
716 continue;
717 }
718 let k = kij[m * n + i];
719 let l = lij[m * n + i];
720 gx += k * (dx - l * dx / dist);
721 gy += k * (dy - l * dy / dist);
722 gz += k * (dz - l * dz / dist);
723 }
724 (gx, gy, gz)
725}
726
727fn select_max_delta_2d(d1: &[f64], d2: &[f64], n: usize) -> (usize, f64) {
728 let mut m = 0;
729 let mut max_delta = -1.0_f64;
730 for i in 0..n {
731 let delta = d1[i] * d1[i] + d2[i] * d2[i];
732 if delta > max_delta {
733 m = i;
734 max_delta = delta;
735 }
736 }
737 (m, max_delta)
738}
739
740fn select_max_delta_3d(d1: &[f64], d2: &[f64], d3: &[f64], n: usize) -> (usize, f64) {
741 let mut m = 0;
742 let mut max_delta = -1.0_f64;
743 for i in 0..n {
744 let delta = d1[i] * d1[i] + d2[i] * d2[i] + d3[i] * d3[i];
745 if delta > max_delta {
746 m = i;
747 max_delta = delta;
748 }
749 }
750 (m, max_delta)
751}
752
753#[allow(clippy::too_many_arguments, clippy::many_single_char_names)]
755fn det3(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64, g: f64, h: f64, i: f64) -> f64 {
756 a * e * i + b * f * g + c * d * h - c * e * g - b * d * i - a * f * h
757}
758
759fn validate_bounds_2d(n: usize, bounds: Option<&KkBounds>) -> IgraphResult<()> {
760 let Some(b) = bounds else { return Ok(()) };
761 if let Some(ref v) = b.minx {
762 if v.len() != n {
763 return Err(IgraphError::InvalidArgument("minx length mismatch".into()));
764 }
765 }
766 if let Some(ref v) = b.maxx {
767 if v.len() != n {
768 return Err(IgraphError::InvalidArgument("maxx length mismatch".into()));
769 }
770 }
771 if let Some(ref v) = b.miny {
772 if v.len() != n {
773 return Err(IgraphError::InvalidArgument("miny length mismatch".into()));
774 }
775 }
776 if let Some(ref v) = b.maxy {
777 if v.len() != n {
778 return Err(IgraphError::InvalidArgument("maxy length mismatch".into()));
779 }
780 }
781 if let (Some(lo), Some(hi)) = (&b.minx, &b.maxx) {
782 for i in 0..n {
783 if lo[i] > hi[i] {
784 return Err(IgraphError::InvalidArgument(
785 "minx must not exceed maxx".into(),
786 ));
787 }
788 }
789 }
790 if let (Some(lo), Some(hi)) = (&b.miny, &b.maxy) {
791 for i in 0..n {
792 if lo[i] > hi[i] {
793 return Err(IgraphError::InvalidArgument(
794 "miny must not exceed maxy".into(),
795 ));
796 }
797 }
798 }
799 Ok(())
800}
801
802fn validate_bounds_3d(n: usize, bounds: Option<&KkBounds3d>) -> IgraphResult<()> {
803 let Some(b) = bounds else { return Ok(()) };
804 for (name, v) in [
805 ("minx", &b.minx),
806 ("maxx", &b.maxx),
807 ("miny", &b.miny),
808 ("maxy", &b.maxy),
809 ("minz", &b.minz),
810 ("maxz", &b.maxz),
811 ] {
812 if let Some(vec) = v {
813 if vec.len() != n {
814 return Err(IgraphError::InvalidArgument(format!(
815 "{name} length mismatch"
816 )));
817 }
818 }
819 }
820 if let (Some(lo), Some(hi)) = (&b.minx, &b.maxx) {
821 for i in 0..n {
822 if lo[i] > hi[i] {
823 return Err(IgraphError::InvalidArgument(
824 "minx must not exceed maxx".into(),
825 ));
826 }
827 }
828 }
829 if let (Some(lo), Some(hi)) = (&b.miny, &b.maxy) {
830 for i in 0..n {
831 if lo[i] > hi[i] {
832 return Err(IgraphError::InvalidArgument(
833 "miny must not exceed maxy".into(),
834 ));
835 }
836 }
837 }
838 if let (Some(lo), Some(hi)) = (&b.minz, &b.maxz) {
839 for i in 0..n {
840 if lo[i] > hi[i] {
841 return Err(IgraphError::InvalidArgument(
842 "minz must not exceed maxz".into(),
843 ));
844 }
845 }
846 }
847 Ok(())
848}
849
850#[cfg(test)]
855mod tests {
856 use super::*;
857 use crate::core::VertexId;
858
859 fn path_graph(n: usize) -> Graph {
860 let mut g = Graph::with_vertices(n as VertexId);
861 for i in 0..(n - 1) {
862 g.add_edge(i as VertexId, (i + 1) as VertexId).unwrap();
863 }
864 g
865 }
866
867 fn cycle_graph(n: usize) -> Graph {
868 let mut g = Graph::with_vertices(n as VertexId);
869 for i in 0..n {
870 g.add_edge(i as VertexId, ((i + 1) % n) as VertexId)
871 .unwrap();
872 }
873 g
874 }
875
876 #[test]
877 fn test_kk_single_vertex() {
878 let g = Graph::with_vertices(1);
879 let params = KkParams::default_for(1);
880 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
881 assert_eq!(pos.len(), 1);
882 }
883
884 #[test]
885 fn test_kk_two_vertices() {
886 let mut g = Graph::with_vertices(2);
887 g.add_edge(0, 1).unwrap();
888 let params = KkParams::default_for(2);
889 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
890 assert_eq!(pos.len(), 2);
891 let dist = ((pos[0][0] - pos[1][0]).powi(2) + (pos[0][1] - pos[1][1]).powi(2)).sqrt();
893 assert!(dist > 0.01, "vertices should be separated, got dist={dist}");
894 }
895
896 #[test]
897 fn test_kk_path_maintains_order() {
898 let g = path_graph(5);
899 let params = KkParams {
900 maxiter: 200,
901 epsilon: 1e-4,
902 kkconst: 5.0,
903 seed: None,
904 };
905 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
906 assert_eq!(pos.len(), 5);
907 for p in &pos {
909 assert!(p[0].is_finite());
910 assert!(p[1].is_finite());
911 }
912 }
913
914 #[test]
915 fn test_kk_cycle_symmetric() {
916 let g = cycle_graph(6);
917 let params = KkParams {
918 maxiter: 300,
919 epsilon: 1e-6,
920 kkconst: 6.0,
921 seed: None,
922 };
923 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
924 let cx: f64 = pos.iter().map(|p| p[0]).sum::<f64>() / 6.0;
926 let cy: f64 = pos.iter().map(|p| p[1]).sum::<f64>() / 6.0;
927 assert!(cx.abs() < 0.5, "centroid x={cx}");
928 assert!(cy.abs() < 0.5, "centroid y={cy}");
929 }
930
931 #[test]
932 fn test_kk_weighted() {
933 let mut g = Graph::with_vertices(3);
934 g.add_edge(0, 1).unwrap();
935 g.add_edge(1, 2).unwrap();
936 g.add_edge(0, 2).unwrap();
937 let weights = [1.0, 1.0, 5.0]; let params = KkParams {
939 maxiter: 200,
940 epsilon: 1e-6,
941 kkconst: 3.0,
942 seed: None,
943 };
944 let pos = layout_kamada_kawai(&g, Some(&weights), ¶ms, None).unwrap();
945 let d01 = ((pos[0][0] - pos[1][0]).powi(2) + (pos[0][1] - pos[1][1]).powi(2)).sqrt();
948 let d02 = ((pos[0][0] - pos[2][0]).powi(2) + (pos[0][1] - pos[2][1]).powi(2)).sqrt();
949 assert!(d02 > d01 * 0.5, "d02={d02}, d01={d01}");
951 assert!(pos[0][0].is_finite());
952 }
953
954 #[test]
955 fn test_kk_bounds() {
956 let g = path_graph(3);
957 let params = KkParams::default_for(3);
958 let bounds = KkBounds {
959 minx: Some(vec![0.0, 0.0, 0.0]),
960 maxx: Some(vec![1.0, 1.0, 1.0]),
961 miny: Some(vec![0.0, 0.0, 0.0]),
962 maxy: Some(vec![1.0, 1.0, 1.0]),
963 };
964 let pos = layout_kamada_kawai(&g, None, ¶ms, Some(&bounds)).unwrap();
965 for p in &pos {
966 assert!(p[0] >= 0.0 && p[0] <= 1.0, "x={} out of bounds", p[0]);
967 assert!(p[1] >= 0.0 && p[1] <= 1.0, "y={} out of bounds", p[1]);
968 }
969 }
970
971 #[test]
972 fn test_kk_invalid_weights() {
973 let mut g = Graph::with_vertices(2);
974 g.add_edge(0, 1).unwrap();
975 let params = KkParams::default_for(2);
976 let result = layout_kamada_kawai(&g, Some(&[-1.0]), ¶ms, None);
977 assert!(result.is_err());
978 }
979
980 #[test]
981 fn test_kk_invalid_kkconst() {
982 let g = Graph::with_vertices(2);
983 let params = KkParams {
984 maxiter: 10,
985 epsilon: 0.0,
986 kkconst: 0.0,
987 seed: None,
988 };
989 let result = layout_kamada_kawai(&g, None, ¶ms, None);
990 assert!(result.is_err());
991 }
992
993 #[test]
994 fn test_kk_3d_basic() {
995 let g = cycle_graph(4);
996 let params = KkParams3d::default_for(4);
997 let pos = layout_kamada_kawai_3d(&g, None, ¶ms, None).unwrap();
998 assert_eq!(pos.len(), 4);
999 for p in &pos {
1000 assert!(p[0].is_finite());
1001 assert!(p[1].is_finite());
1002 assert!(p[2].is_finite());
1003 }
1004 }
1005
1006 #[test]
1007 fn test_kk_3d_bounds() {
1008 let g = path_graph(3);
1009 let params = KkParams3d::default_for(3);
1010 let bounds = KkBounds3d {
1011 minx: Some(vec![-1.0, -1.0, -1.0]),
1012 maxx: Some(vec![1.0, 1.0, 1.0]),
1013 miny: Some(vec![-1.0, -1.0, -1.0]),
1014 maxy: Some(vec![1.0, 1.0, 1.0]),
1015 minz: Some(vec![-1.0, -1.0, -1.0]),
1016 maxz: Some(vec![1.0, 1.0, 1.0]),
1017 };
1018 let pos = layout_kamada_kawai_3d(&g, None, ¶ms, Some(&bounds)).unwrap();
1019 for p in &pos {
1020 assert!(p[0] >= -1.0 && p[0] <= 1.0);
1021 assert!(p[1] >= -1.0 && p[1] <= 1.0);
1022 assert!(p[2] >= -1.0 && p[2] <= 1.0);
1023 }
1024 }
1025
1026 #[test]
1027 fn test_kk_seed() {
1028 let g = path_graph(3);
1029 let seed = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
1030 let params = KkParams {
1031 maxiter: 100,
1032 epsilon: 1e-6,
1033 kkconst: 3.0,
1034 seed: Some(seed),
1035 };
1036 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
1037 assert_eq!(pos.len(), 3);
1038 for p in &pos {
1039 assert!(p[0].is_finite());
1040 assert!(p[1].is_finite());
1041 }
1042 }
1043
1044 #[test]
1045 fn test_kk_disconnected() {
1046 let mut g = Graph::with_vertices(4);
1048 g.add_edge(0, 1).unwrap();
1049 g.add_edge(2, 3).unwrap();
1050 let params = KkParams::default_for(4);
1051 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
1052 assert_eq!(pos.len(), 4);
1053 for p in &pos {
1055 assert!(p[0].is_finite());
1056 assert!(p[1].is_finite());
1057 }
1058 }
1059}