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 {
552 Self {
553 maxiter: 10 * n.max(1),
554 epsilon: 0.0,
555 kkconst: n as f64,
556 seed: None,
557 }
558 }
559}
560
561impl KkParams3d {
562 pub fn default_for(n: usize) -> Self {
573 Self {
574 maxiter: 10 * n.max(1),
575 epsilon: 0.0,
576 kkconst: n as f64,
577 seed: None,
578 }
579 }
580}
581
582fn initial_circle_2d(n: usize) -> Vec<[f64; 2]> {
583 let l0 = (n as f64).sqrt();
584 let scale = 0.36 * l0;
585 let mut pos = Vec::with_capacity(n);
586 for i in 0..n {
587 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
588 pos.push([scale * angle.cos(), scale * angle.sin()]);
589 }
590 pos
591}
592
593fn initial_sphere_3d(n: usize) -> Vec<[f64; 3]> {
594 let l0 = (n as f64).sqrt();
595 let scale = 0.36 * l0;
596 let mut pos = Vec::with_capacity(n);
597 let golden_ratio = 0.5 * (1.0 + 5.0_f64.sqrt());
598 for i in 0..n {
599 let theta = 2.0 * std::f64::consts::PI * (i as f64) / golden_ratio;
600 let phi = (1.0 - 2.0 * (i as f64 + 0.5) / n as f64).acos();
601 pos.push([
602 scale * phi.sin() * theta.cos(),
603 scale * phi.sin() * theta.sin(),
604 scale * phi.cos(),
605 ]);
606 }
607 pos
608}
609
610fn initial_bounded_2d(n: usize, bounds: Option<&KkBounds>) -> Vec<[f64; 2]> {
611 let mut pos = Vec::with_capacity(n);
612 for i in 0..n {
613 let t = i as f64 / n.max(1) as f64;
614 let angle = 2.0 * std::f64::consts::PI * t;
615 let (mut x, mut y) = (angle.cos(), angle.sin());
616 if let Some(b) = bounds {
618 let (lo_x, hi_x) = bounds_range_x(b, i);
619 let (lo_y, hi_y) = bounds_range_y(b, i);
620 x = lo_x + (x + 1.0) * 0.5 * (hi_x - lo_x);
621 y = lo_y + (y + 1.0) * 0.5 * (hi_y - lo_y);
622 }
623 pos.push([x, y]);
624 }
625 pos
626}
627
628fn bounds_range_x(b: &KkBounds, i: usize) -> (f64, f64) {
629 let lo = b.minx.as_ref().map_or(-1.0, |v| v[i]);
630 let hi = b.maxx.as_ref().map_or(1.0, |v| v[i]);
631 (lo, hi)
632}
633
634fn bounds_range_y(b: &KkBounds, i: usize) -> (f64, f64) {
635 let lo = b.miny.as_ref().map_or(-1.0, |v| v[i]);
636 let hi = b.maxy.as_ref().map_or(1.0, |v| v[i]);
637 (lo, hi)
638}
639
640fn clamp_positions_2d(pos: &mut [[f64; 2]], bounds: &KkBounds) {
641 for (i, p) in pos.iter_mut().enumerate() {
642 if let Some(ref v) = bounds.minx {
643 if p[0] < v[i] {
644 p[0] = v[i];
645 }
646 }
647 if let Some(ref v) = bounds.maxx {
648 if p[0] > v[i] {
649 p[0] = v[i];
650 }
651 }
652 if let Some(ref v) = bounds.miny {
653 if p[1] < v[i] {
654 p[1] = v[i];
655 }
656 }
657 if let Some(ref v) = bounds.maxy {
658 if p[1] > v[i] {
659 p[1] = v[i];
660 }
661 }
662 }
663}
664
665fn all_pairs_distances(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
666 let n = graph.vcount() as usize;
667 let dist_matrix = floyd_warshall_distances(graph, weights)?;
668
669 let mut flat = vec![f64::INFINITY; n * n];
670 for i in 0..n {
671 for j in 0..n {
672 if let Some(d) = dist_matrix[i][j] {
673 flat[i * n + j] = d;
674 }
675 }
676 }
677 Ok(flat)
678}
679
680fn find_max_finite(dij: &mut [f64], n: usize) -> f64 {
682 let mut max_d = 0.0_f64;
683 for i in 0..n {
684 for j in (i + 1)..n {
685 let d = dij[i * n + j];
686 if d.is_finite() && d > max_d {
687 max_d = d;
688 }
689 }
690 }
691 for val in dij.iter_mut() {
693 if !val.is_finite() {
694 *val = max_d;
695 }
696 }
697 max_d
698}
699
700fn gradient_2d(m: usize, pos: &[[f64; 2]], kij: &[f64], lij: &[f64], n: usize) -> (f64, f64) {
701 let mut gx = 0.0;
702 let mut gy = 0.0;
703 for i in 0..n {
704 if i == m {
705 continue;
706 }
707 let dx = pos[m][0] - pos[i][0];
708 let dy = pos[m][1] - pos[i][1];
709 let dist = (dx * dx + dy * dy).sqrt();
710 if dist < f64::EPSILON {
711 continue;
712 }
713 let k = kij[m * n + i];
714 let l = lij[m * n + i];
715 gx += k * (dx - l * dx / dist);
716 gy += k * (dy - l * dy / dist);
717 }
718 (gx, gy)
719}
720
721fn gradient_3d(m: usize, pos: &[[f64; 3]], kij: &[f64], lij: &[f64], n: usize) -> (f64, f64, f64) {
722 let mut gx = 0.0;
723 let mut gy = 0.0;
724 let mut gz = 0.0;
725 for i in 0..n {
726 if i == m {
727 continue;
728 }
729 let dx = pos[m][0] - pos[i][0];
730 let dy = pos[m][1] - pos[i][1];
731 let dz = pos[m][2] - pos[i][2];
732 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
733 if dist < f64::EPSILON {
734 continue;
735 }
736 let k = kij[m * n + i];
737 let l = lij[m * n + i];
738 gx += k * (dx - l * dx / dist);
739 gy += k * (dy - l * dy / dist);
740 gz += k * (dz - l * dz / dist);
741 }
742 (gx, gy, gz)
743}
744
745fn select_max_delta_2d(d1: &[f64], d2: &[f64], n: usize) -> (usize, f64) {
746 let mut m = 0;
747 let mut max_delta = -1.0_f64;
748 for i in 0..n {
749 let delta = d1[i] * d1[i] + d2[i] * d2[i];
750 if delta > max_delta {
751 m = i;
752 max_delta = delta;
753 }
754 }
755 (m, max_delta)
756}
757
758fn select_max_delta_3d(d1: &[f64], d2: &[f64], d3: &[f64], n: usize) -> (usize, f64) {
759 let mut m = 0;
760 let mut max_delta = -1.0_f64;
761 for i in 0..n {
762 let delta = d1[i] * d1[i] + d2[i] * d2[i] + d3[i] * d3[i];
763 if delta > max_delta {
764 m = i;
765 max_delta = delta;
766 }
767 }
768 (m, max_delta)
769}
770
771#[allow(clippy::too_many_arguments, clippy::many_single_char_names)]
773fn det3(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64, g: f64, h: f64, i: f64) -> f64 {
774 a * e * i + b * f * g + c * d * h - c * e * g - b * d * i - a * f * h
775}
776
777fn validate_bounds_2d(n: usize, bounds: Option<&KkBounds>) -> IgraphResult<()> {
778 let Some(b) = bounds else { return Ok(()) };
779 if let Some(ref v) = b.minx {
780 if v.len() != n {
781 return Err(IgraphError::InvalidArgument("minx length mismatch".into()));
782 }
783 }
784 if let Some(ref v) = b.maxx {
785 if v.len() != n {
786 return Err(IgraphError::InvalidArgument("maxx length mismatch".into()));
787 }
788 }
789 if let Some(ref v) = b.miny {
790 if v.len() != n {
791 return Err(IgraphError::InvalidArgument("miny length mismatch".into()));
792 }
793 }
794 if let Some(ref v) = b.maxy {
795 if v.len() != n {
796 return Err(IgraphError::InvalidArgument("maxy length mismatch".into()));
797 }
798 }
799 if let (Some(lo), Some(hi)) = (&b.minx, &b.maxx) {
800 for i in 0..n {
801 if lo[i] > hi[i] {
802 return Err(IgraphError::InvalidArgument(
803 "minx must not exceed maxx".into(),
804 ));
805 }
806 }
807 }
808 if let (Some(lo), Some(hi)) = (&b.miny, &b.maxy) {
809 for i in 0..n {
810 if lo[i] > hi[i] {
811 return Err(IgraphError::InvalidArgument(
812 "miny must not exceed maxy".into(),
813 ));
814 }
815 }
816 }
817 Ok(())
818}
819
820fn validate_bounds_3d(n: usize, bounds: Option<&KkBounds3d>) -> IgraphResult<()> {
821 let Some(b) = bounds else { return Ok(()) };
822 for (name, v) in [
823 ("minx", &b.minx),
824 ("maxx", &b.maxx),
825 ("miny", &b.miny),
826 ("maxy", &b.maxy),
827 ("minz", &b.minz),
828 ("maxz", &b.maxz),
829 ] {
830 if let Some(vec) = v {
831 if vec.len() != n {
832 return Err(IgraphError::InvalidArgument(format!(
833 "{name} length mismatch"
834 )));
835 }
836 }
837 }
838 if let (Some(lo), Some(hi)) = (&b.minx, &b.maxx) {
839 for i in 0..n {
840 if lo[i] > hi[i] {
841 return Err(IgraphError::InvalidArgument(
842 "minx must not exceed maxx".into(),
843 ));
844 }
845 }
846 }
847 if let (Some(lo), Some(hi)) = (&b.miny, &b.maxy) {
848 for i in 0..n {
849 if lo[i] > hi[i] {
850 return Err(IgraphError::InvalidArgument(
851 "miny must not exceed maxy".into(),
852 ));
853 }
854 }
855 }
856 if let (Some(lo), Some(hi)) = (&b.minz, &b.maxz) {
857 for i in 0..n {
858 if lo[i] > hi[i] {
859 return Err(IgraphError::InvalidArgument(
860 "minz must not exceed maxz".into(),
861 ));
862 }
863 }
864 }
865 Ok(())
866}
867
868#[cfg(test)]
873mod tests {
874 use super::*;
875 use crate::core::VertexId;
876
877 fn path_graph(n: usize) -> Graph {
878 let mut g = Graph::with_vertices(n as VertexId);
879 for i in 0..(n - 1) {
880 g.add_edge(i as VertexId, (i + 1) as VertexId).unwrap();
881 }
882 g
883 }
884
885 fn cycle_graph(n: usize) -> Graph {
886 let mut g = Graph::with_vertices(n as VertexId);
887 for i in 0..n {
888 g.add_edge(i as VertexId, ((i + 1) % n) as VertexId)
889 .unwrap();
890 }
891 g
892 }
893
894 #[test]
895 fn test_kk_single_vertex() {
896 let g = Graph::with_vertices(1);
897 let params = KkParams::default_for(1);
898 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
899 assert_eq!(pos.len(), 1);
900 }
901
902 #[test]
903 fn test_kk_two_vertices() {
904 let mut g = Graph::with_vertices(2);
905 g.add_edge(0, 1).unwrap();
906 let params = KkParams::default_for(2);
907 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
908 assert_eq!(pos.len(), 2);
909 let dist = ((pos[0][0] - pos[1][0]).powi(2) + (pos[0][1] - pos[1][1]).powi(2)).sqrt();
911 assert!(dist > 0.01, "vertices should be separated, got dist={dist}");
912 }
913
914 #[test]
915 fn test_kk_path_maintains_order() {
916 let g = path_graph(5);
917 let params = KkParams {
918 maxiter: 200,
919 epsilon: 1e-4,
920 kkconst: 5.0,
921 seed: None,
922 };
923 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
924 assert_eq!(pos.len(), 5);
925 for p in &pos {
927 assert!(p[0].is_finite());
928 assert!(p[1].is_finite());
929 }
930 }
931
932 #[test]
933 fn test_kk_cycle_symmetric() {
934 let g = cycle_graph(6);
935 let params = KkParams {
936 maxiter: 300,
937 epsilon: 1e-6,
938 kkconst: 6.0,
939 seed: None,
940 };
941 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
942 let cx: f64 = pos.iter().map(|p| p[0]).sum::<f64>() / 6.0;
944 let cy: f64 = pos.iter().map(|p| p[1]).sum::<f64>() / 6.0;
945 assert!(cx.abs() < 0.5, "centroid x={cx}");
946 assert!(cy.abs() < 0.5, "centroid y={cy}");
947 }
948
949 #[test]
950 fn test_kk_weighted() {
951 let mut g = Graph::with_vertices(3);
952 g.add_edge(0, 1).unwrap();
953 g.add_edge(1, 2).unwrap();
954 g.add_edge(0, 2).unwrap();
955 let weights = [1.0, 1.0, 5.0]; let params = KkParams {
957 maxiter: 200,
958 epsilon: 1e-6,
959 kkconst: 3.0,
960 seed: None,
961 };
962 let pos = layout_kamada_kawai(&g, Some(&weights), ¶ms, None).unwrap();
963 let d01 = ((pos[0][0] - pos[1][0]).powi(2) + (pos[0][1] - pos[1][1]).powi(2)).sqrt();
966 let d02 = ((pos[0][0] - pos[2][0]).powi(2) + (pos[0][1] - pos[2][1]).powi(2)).sqrt();
967 assert!(d02 > d01 * 0.5, "d02={d02}, d01={d01}");
969 assert!(pos[0][0].is_finite());
970 }
971
972 #[test]
973 fn test_kk_bounds() {
974 let g = path_graph(3);
975 let params = KkParams::default_for(3);
976 let bounds = KkBounds {
977 minx: Some(vec![0.0, 0.0, 0.0]),
978 maxx: Some(vec![1.0, 1.0, 1.0]),
979 miny: Some(vec![0.0, 0.0, 0.0]),
980 maxy: Some(vec![1.0, 1.0, 1.0]),
981 };
982 let pos = layout_kamada_kawai(&g, None, ¶ms, Some(&bounds)).unwrap();
983 for p in &pos {
984 assert!(p[0] >= 0.0 && p[0] <= 1.0, "x={} out of bounds", p[0]);
985 assert!(p[1] >= 0.0 && p[1] <= 1.0, "y={} out of bounds", p[1]);
986 }
987 }
988
989 #[test]
990 fn test_kk_invalid_weights() {
991 let mut g = Graph::with_vertices(2);
992 g.add_edge(0, 1).unwrap();
993 let params = KkParams::default_for(2);
994 let result = layout_kamada_kawai(&g, Some(&[-1.0]), ¶ms, None);
995 assert!(result.is_err());
996 }
997
998 #[test]
999 fn test_kk_invalid_kkconst() {
1000 let g = Graph::with_vertices(2);
1001 let params = KkParams {
1002 maxiter: 10,
1003 epsilon: 0.0,
1004 kkconst: 0.0,
1005 seed: None,
1006 };
1007 let result = layout_kamada_kawai(&g, None, ¶ms, None);
1008 assert!(result.is_err());
1009 }
1010
1011 #[test]
1012 fn test_kk_3d_basic() {
1013 let g = cycle_graph(4);
1014 let params = KkParams3d::default_for(4);
1015 let pos = layout_kamada_kawai_3d(&g, None, ¶ms, None).unwrap();
1016 assert_eq!(pos.len(), 4);
1017 for p in &pos {
1018 assert!(p[0].is_finite());
1019 assert!(p[1].is_finite());
1020 assert!(p[2].is_finite());
1021 }
1022 }
1023
1024 #[test]
1025 fn test_kk_3d_bounds() {
1026 let g = path_graph(3);
1027 let params = KkParams3d::default_for(3);
1028 let bounds = KkBounds3d {
1029 minx: Some(vec![-1.0, -1.0, -1.0]),
1030 maxx: Some(vec![1.0, 1.0, 1.0]),
1031 miny: Some(vec![-1.0, -1.0, -1.0]),
1032 maxy: Some(vec![1.0, 1.0, 1.0]),
1033 minz: Some(vec![-1.0, -1.0, -1.0]),
1034 maxz: Some(vec![1.0, 1.0, 1.0]),
1035 };
1036 let pos = layout_kamada_kawai_3d(&g, None, ¶ms, Some(&bounds)).unwrap();
1037 for p in &pos {
1038 assert!(p[0] >= -1.0 && p[0] <= 1.0);
1039 assert!(p[1] >= -1.0 && p[1] <= 1.0);
1040 assert!(p[2] >= -1.0 && p[2] <= 1.0);
1041 }
1042 }
1043
1044 #[test]
1045 fn test_kk_seed() {
1046 let g = path_graph(3);
1047 let seed = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
1048 let params = KkParams {
1049 maxiter: 100,
1050 epsilon: 1e-6,
1051 kkconst: 3.0,
1052 seed: Some(seed),
1053 };
1054 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
1055 assert_eq!(pos.len(), 3);
1056 for p in &pos {
1057 assert!(p[0].is_finite());
1058 assert!(p[1].is_finite());
1059 }
1060 }
1061
1062 #[test]
1063 fn test_kk_disconnected() {
1064 let mut g = Graph::with_vertices(4);
1066 g.add_edge(0, 1).unwrap();
1067 g.add_edge(2, 3).unwrap();
1068 let params = KkParams::default_for(4);
1069 let pos = layout_kamada_kawai(&g, None, ¶ms, None).unwrap();
1070 assert_eq!(pos.len(), 4);
1071 for p in &pos {
1073 assert!(p[0].is_finite());
1074 assert!(p[1].is_finite());
1075 }
1076 }
1077}