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    /// Minimum x coordinate per vertex.
33    pub minx: Option<Vec<f64>>,
34    /// Maximum x coordinate per vertex.
35    pub maxx: Option<Vec<f64>>,
36    /// Minimum y coordinate per vertex.
37    pub miny: Option<Vec<f64>>,
38    /// Maximum y coordinate per vertex.
39    pub maxy: Option<Vec<f64>>,
40}
41
42/// Parameters for the 3D Kamada-Kawai layout.
43#[derive(Debug, Clone)]
44pub struct KkParams3d {
45    /// Maximum number of iterations. Default: `10 * vcount`.
46    pub maxiter: usize,
47    /// Stop when the maximum energy-gradient magnitude squared falls
48    /// below this threshold.
49    pub epsilon: f64,
50    /// Spring constant K. Default: `vcount` as f64.
51    pub kkconst: f64,
52    /// Optional initial positions (row per vertex: `[x, y, z]`).
53    /// If `None`, a scaled sphere layout is used.
54    pub seed: Option<Vec<[f64; 3]>>,
55}
56
57/// Per-vertex coordinate bounds for the 3D KK layout.
58#[derive(Debug, Clone, Default)]
59pub struct KkBounds3d {
60    /// Minimum x coordinate per vertex.
61    pub minx: Option<Vec<f64>>,
62    /// Maximum x coordinate per vertex.
63    pub maxx: Option<Vec<f64>>,
64    /// Minimum y coordinate per vertex.
65    pub miny: Option<Vec<f64>>,
66    /// Maximum y coordinate per vertex.
67    pub maxy: Option<Vec<f64>>,
68    /// Minimum z coordinate per vertex.
69    pub minz: Option<Vec<f64>>,
70    /// Maximum z coordinate per vertex.
71    pub maxz: Option<Vec<f64>>,
72}
73
74/// Compute 2D Kamada-Kawai layout.
75///
76/// Places vertices so that graph-theoretic distance is reflected in
77/// Euclidean distance. Works well for small-to-medium graphs (O(V²)
78/// memory for distance matrices).
79///
80/// # Arguments
81///
82/// * `graph` — input graph (treated as undirected for shortest paths).
83/// * `weights` — optional edge weights interpreted as edge *lengths*
84///   (higher weight → vertices farther apart). Must be positive.
85/// * `params` — algorithm parameters; use [`KkParams::default_for`] for
86///   reasonable defaults.
87/// * `bounds` — optional per-vertex coordinate bounds.
88///
89/// # Examples
90///
91/// ```
92/// use rust_igraph::{Graph, layout_kamada_kawai, KkParams};
93///
94/// let mut g = Graph::with_vertices(4);
95/// g.add_edge(0, 1).unwrap();
96/// g.add_edge(1, 2).unwrap();
97/// g.add_edge(2, 3).unwrap();
98/// g.add_edge(3, 0).unwrap();
99///
100/// let params = KkParams::default_for(4);
101/// let pos = layout_kamada_kawai(&g, None, &params, None).unwrap();
102/// assert_eq!(pos.len(), 4);
103/// ```
104pub 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    // Initialize positions
137    let mut pos = match &params.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    // Clamp initial positions to bounds
156    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    // All-pairs shortest paths
165    let mut dij = all_pairs_distances(graph, weights)?;
166
167    // Find max finite distance, replace infinite with max
168    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    // Build kij and lij matrices (flat, row-major)
177    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    // Initialize gradient vectors D1 (∂E/∂x_m), D2 (∂E/∂y_m)
194    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    // Main iteration
203    for _iter in 0..params.maxiter {
204        // Find vertex with maximum delta (energy gradient magnitude²)
205        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        // Build the 2×2 system coefficients (Hessian of energy w.r.t. m)
214        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        // Solve 2×2: [Axx Axy; Axy Ayy] * [dx; dy] = [Ax; Ay]
236        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        // Apply bounds
251        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        // Update gradient incrementally
275        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            // Remove old contribution to i's gradient, add new
292            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            // Accumulate m's new gradient
298            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
308/// Compute 3D Kamada-Kawai layout.
309///
310/// Three-dimensional variant; same algorithm but solves a 3×3 system
311/// per iteration using Cramer's rule.
312///
313/// # Examples
314///
315/// ```
316/// use rust_igraph::{Graph, layout_kamada_kawai_3d, KkParams3d};
317///
318/// let mut g = Graph::with_vertices(4);
319/// g.add_edge(0, 1).unwrap();
320/// g.add_edge(1, 2).unwrap();
321/// g.add_edge(2, 3).unwrap();
322/// g.add_edge(3, 0).unwrap();
323///
324/// let params = KkParams3d::default_for(4);
325/// let pos = layout_kamada_kawai_3d(&g, None, &params, None).unwrap();
326/// assert_eq!(pos.len(), 4);
327/// assert_eq!(pos[0].len(), 3); // x, y, z
328/// ```
329pub 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 &params.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    // Initialize gradients
403    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        // Build 3×3 Hessian
424        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        // Solve 3×3 via Cramer's rule
456        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        // Apply bounds
475        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        // Incremental gradient update
509        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
546// ═══════════════════════════════════════════════════════════════════
547// Helpers
548// ═══════════════════════════════════════════════════════════════════
549
550impl KkParams {
551    /// Reasonable defaults for a graph with `n` vertices.
552    ///
553    /// # Examples
554    ///
555    /// ```
556    /// use rust_igraph::KkParams;
557    ///
558    /// let params = KkParams::default_for(100);
559    /// assert_eq!(params.maxiter, 1000);
560    /// ```
561    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    /// Reasonable defaults for a graph with `n` vertices.
573    ///
574    /// # Examples
575    ///
576    /// ```
577    /// use rust_igraph::KkParams3d;
578    ///
579    /// let params = KkParams3d::default_for(50);
580    /// assert_eq!(params.maxiter, 500);
581    /// ```
582    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        // Scale into bounding box if provided
627        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
690/// Find max finite distance and replace all infinite entries with it.
691fn 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    // Replace infinite distances with max_dij (disconnected components)
702    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/// 3×3 determinant via Sarrus' rule.
782#[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// ═══════════════════════════════════════════════════════════════════
879// Tests
880// ═══════════════════════════════════════════════════════════════════
881
882#[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, &params, 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, &params, None).unwrap();
918        assert_eq!(pos.len(), 2);
919        // Should be separated
920        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, &params, None).unwrap();
934        assert_eq!(pos.len(), 5);
935        // All positions should be finite
936        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, &params, None).unwrap();
952        // Centroid should be near origin
953        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]; // 0-2 edge is "long"
966        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), &params, None).unwrap();
973        // Distance 0→2 via direct edge (weight 5) vs 0→1→2 (weight 2)
974        // Shortest path 0→2 is min(5, 2)=2, so 0-2 should be closer than weight 5 suggests
975        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        // d02 should be roughly twice d01 (path distance 2 vs 1)
978        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, &params, 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]), &params, 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, &params, 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, &params, 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, &params, 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, &params, 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        // Two isolated components
1075        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, &params, None).unwrap();
1080        assert_eq!(pos.len(), 4);
1081        // All should be finite — disconnected vertices get max_dij distance
1082        for p in &pos {
1083            assert!(p[0].is_finite());
1084            assert!(p[1].is_finite());
1085        }
1086    }
1087}