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