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    ///
543    /// # Examples
544    ///
545    /// ```
546    /// use rust_igraph::KkParams;
547    ///
548    /// let params = KkParams::default_for(100);
549    /// assert_eq!(params.maxiter, 1000);
550    /// ```
551    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    /// Reasonable defaults for a graph with `n` vertices.
563    ///
564    /// # Examples
565    ///
566    /// ```
567    /// use rust_igraph::KkParams3d;
568    ///
569    /// let params = KkParams3d::default_for(50);
570    /// assert_eq!(params.maxiter, 500);
571    /// ```
572    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        // Scale into bounding box if provided
617        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
680/// Find max finite distance and replace all infinite entries with it.
681fn 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    // Replace infinite distances with max_dij (disconnected components)
692    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/// 3×3 determinant via Sarrus' rule.
772#[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// ═══════════════════════════════════════════════════════════════════
869// Tests
870// ═══════════════════════════════════════════════════════════════════
871
872#[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, &params, 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, &params, None).unwrap();
908        assert_eq!(pos.len(), 2);
909        // Should be separated
910        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, &params, None).unwrap();
924        assert_eq!(pos.len(), 5);
925        // All positions should be finite
926        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, &params, None).unwrap();
942        // Centroid should be near origin
943        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]; // 0-2 edge is "long"
956        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), &params, None).unwrap();
963        // Distance 0→2 via direct edge (weight 5) vs 0→1→2 (weight 2)
964        // Shortest path 0→2 is min(5, 2)=2, so 0-2 should be closer than weight 5 suggests
965        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        // d02 should be roughly twice d01 (path distance 2 vs 1)
968        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, &params, 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]), &params, 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, &params, 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, &params, 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, &params, 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, &params, 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        // Two isolated components
1065        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, &params, None).unwrap();
1070        assert_eq!(pos.len(), 4);
1071        // All should be finite — disconnected vertices get max_dij distance
1072        for p in &pos {
1073            assert!(p[0].is_finite());
1074            assert!(p[1].is_finite());
1075        }
1076    }
1077}