Skip to main content

rust_igraph/algorithms/layout/
kamada_kawai.rs

1//! Kamada-Kawai spring layout (ALGO-LO-003).
2//!
3//! Energy-minimization layout based on graph-theoretic distances.
4//! Reference: Kamada & Kawai, "An Algorithm for Drawing General
5//! Undirected Graphs", Information Processing Letters 31 (1989).
6
7use crate::algorithms::paths::floyd_warshall::floyd_warshall_distances;
8use crate::core::{Graph, IgraphError, IgraphResult};
9
10// Threshold for near-zero gradient (skip move) — matches igraph C.
11const KK_EPS: f64 = 1e-13;
12const KK3D_EPS: f64 = 1e-8;
13
14/// Parameters for the 2D Kamada-Kawai layout.
15#[derive(Debug, Clone)]
16pub struct KkParams {
17    /// Maximum number of iterations. Default: `10 * vcount`.
18    pub maxiter: usize,
19    /// Stop when the maximum energy-gradient magnitude squared falls
20    /// below this threshold. Zero means run for exactly `maxiter` iterations.
21    pub epsilon: f64,
22    /// Spring constant K. Default: `vcount` as f64.
23    pub kkconst: f64,
24    /// Optional initial positions (row per vertex: `[x, y]`).
25    /// If `None`, a scaled circle layout is used.
26    pub seed: Option<Vec<[f64; 2]>>,
27}
28
29/// Per-vertex coordinate bounds for the 2D KK layout.
30#[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/// Parameters for the 3D Kamada-Kawai layout.
39#[derive(Debug, Clone)]
40pub struct KkParams3d {
41    /// Maximum number of iterations. Default: `10 * vcount`.
42    pub maxiter: usize,
43    /// Stop when the maximum energy-gradient magnitude squared falls
44    /// below this threshold.
45    pub epsilon: f64,
46    /// Spring constant K. Default: `vcount` as f64.
47    pub kkconst: f64,
48    /// Optional initial positions (row per vertex: `[x, y, z]`).
49    /// If `None`, a scaled sphere layout is used.
50    pub seed: Option<Vec<[f64; 3]>>,
51}
52
53/// Per-vertex coordinate bounds for the 3D KK layout.
54#[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
64/// Compute 2D Kamada-Kawai layout.
65///
66/// Places vertices so that graph-theoretic distance is reflected in
67/// Euclidean distance. Works well for small-to-medium graphs (O(V²)
68/// memory for distance matrices).
69///
70/// # Arguments
71///
72/// * `graph` — input graph (treated as undirected for shortest paths).
73/// * `weights` — optional edge weights interpreted as edge *lengths*
74///   (higher weight → vertices farther apart). Must be positive.
75/// * `params` — algorithm parameters; use [`KkParams::default_for`] for
76///   reasonable defaults.
77/// * `bounds` — optional per-vertex coordinate bounds.
78///
79/// # Examples
80///
81/// ```
82/// use rust_igraph::{Graph, layout_kamada_kawai, KkParams};
83///
84/// let mut g = Graph::with_vertices(4);
85/// g.add_edge(0, 1).unwrap();
86/// g.add_edge(1, 2).unwrap();
87/// g.add_edge(2, 3).unwrap();
88/// g.add_edge(3, 0).unwrap();
89///
90/// let params = KkParams::default_for(4);
91/// let pos = layout_kamada_kawai(&g, None, &params, None).unwrap();
92/// assert_eq!(pos.len(), 4);
93/// ```
94pub 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    // Initialize positions
127    let mut pos = match &params.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    // Clamp initial positions to bounds
146    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    // All-pairs shortest paths
155    let mut dij = all_pairs_distances(graph, weights)?;
156
157    // Find max finite distance, replace infinite with max
158    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    // Build kij and lij matrices (flat, row-major)
167    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    // Initialize gradient vectors D1 (∂E/∂x_m), D2 (∂E/∂y_m)
184    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    // Main iteration
193    for _iter in 0..params.maxiter {
194        // Find vertex with maximum delta (energy gradient magnitude²)
195        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        // Build the 2×2 system coefficients (Hessian of energy w.r.t. m)
204        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        // Solve 2×2: [Axx Axy; Axy Ayy] * [dx; dy] = [Ax; Ay]
226        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        // Apply bounds
241        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        // Update gradient incrementally
265        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            // Remove old contribution to i's gradient, add new
282            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            // Accumulate m's new gradient
288            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
298/// Compute 3D Kamada-Kawai layout.
299///
300/// Three-dimensional variant; same algorithm but solves a 3×3 system
301/// per iteration using Cramer's rule.
302///
303/// # Examples
304///
305/// ```
306/// use rust_igraph::{Graph, layout_kamada_kawai_3d, KkParams3d};
307///
308/// let mut g = Graph::with_vertices(4);
309/// g.add_edge(0, 1).unwrap();
310/// g.add_edge(1, 2).unwrap();
311/// g.add_edge(2, 3).unwrap();
312/// g.add_edge(3, 0).unwrap();
313///
314/// let params = KkParams3d::default_for(4);
315/// let pos = layout_kamada_kawai_3d(&g, None, &params, None).unwrap();
316/// assert_eq!(pos.len(), 4);
317/// assert_eq!(pos[0].len(), 3); // x, y, z
318/// ```
319pub 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 &params.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    // Initialize gradients
393    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        // Build 3×3 Hessian
414        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        // Solve 3×3 via Cramer's rule
446        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        // Apply bounds
465        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        // Incremental gradient update
499        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
536// ═══════════════════════════════════════════════════════════════════
537// Helpers
538// ═══════════════════════════════════════════════════════════════════
539
540impl KkParams {
541    /// Reasonable defaults for a graph with `n` vertices.
542    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    /// Reasonable defaults for a graph with `n` vertices.
554    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        // Scale into bounding box if provided
599        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
662/// Find max finite distance and replace all infinite entries with it.
663fn 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    // Replace infinite distances with max_dij (disconnected components)
674    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/// 3×3 determinant via Sarrus' rule.
754#[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// ═══════════════════════════════════════════════════════════════════
851// Tests
852// ═══════════════════════════════════════════════════════════════════
853
854#[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, &params, 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, &params, None).unwrap();
890        assert_eq!(pos.len(), 2);
891        // Should be separated
892        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, &params, None).unwrap();
906        assert_eq!(pos.len(), 5);
907        // All positions should be finite
908        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, &params, None).unwrap();
924        // Centroid should be near origin
925        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]; // 0-2 edge is "long"
938        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), &params, None).unwrap();
945        // Distance 0→2 via direct edge (weight 5) vs 0→1→2 (weight 2)
946        // Shortest path 0→2 is min(5, 2)=2, so 0-2 should be closer than weight 5 suggests
947        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        // d02 should be roughly twice d01 (path distance 2 vs 1)
950        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, &params, 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]), &params, 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, &params, 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, &params, 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, &params, 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, &params, 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        // Two isolated components
1047        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, &params, None).unwrap();
1052        assert_eq!(pos.len(), 4);
1053        // All should be finite — disconnected vertices get max_dij distance
1054        for p in &pos {
1055            assert!(p[0].is_finite());
1056            assert!(p[1].is_finite());
1057        }
1058    }
1059}