Skip to main content

rust_igraph/algorithms/layout/
drl.rs

1//! DrL (Distributed Recursive Layout) force-directed algorithm (ALGO-LO-012).
2//!
3//! Multi-phase simulated annealing layout with density-grid repulsion.
4//! 6 stages: liquid → expansion → cooldown → crunch → simmer → done.
5//! Each stage has configurable iterations, temperature, attraction, and
6//! damping. A density grid provides efficient global repulsion.
7//!
8//! Reference: Martin, S., Brown, W.M., Klavans, R., Boyack, K.W.,
9//! DrL: Distributed Recursive (Graph) Layout. SAND Reports, 2008. 2936: p. 1-10.
10
11use crate::core::{Graph, IgraphError, IgraphResult};
12
13const GRID_SIZE: usize = 400;
14const RADIUS: i32 = 10;
15const HALF_VIEW: f32 = 800.0;
16const VIEW_TO_GRID: f32 = 0.25;
17
18/// Predefined parameter templates for DrL.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum DrlTemplate {
21    Default,
22    Coarsen,
23    Coarsest,
24    Refine,
25    Final,
26}
27
28/// Per-stage parameters.
29#[derive(Debug, Clone, Copy)]
30struct StageParams {
31    iterations: u32,
32    temperature: f32,
33    attraction: f32,
34    damping_mult: f32,
35}
36
37/// Full DrL options.
38#[derive(Debug, Clone)]
39pub struct DrlOptions {
40    pub edge_cut: f32,
41    pub init_iterations: u32,
42    pub init_temperature: f32,
43    pub init_attraction: f32,
44    pub init_damping_mult: f32,
45    liquid: StageParams,
46    expansion: StageParams,
47    cooldown: StageParams,
48    crunch: StageParams,
49    simmer: StageParams,
50}
51
52impl DrlOptions {
53    /// Create options from a predefined template.
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use rust_igraph::{DrlOptions, DrlTemplate};
59    ///
60    /// let opts = DrlOptions::from_template(DrlTemplate::Default);
61    /// assert!(opts.edge_cut > 0.0);
62    /// ```
63    pub fn from_template(templ: DrlTemplate) -> Self {
64        let edge_cut: f32 = 32.0 / 40.0;
65        match templ {
66            DrlTemplate::Default => Self {
67                edge_cut,
68                init_iterations: 0,
69                init_temperature: 2000.0,
70                init_attraction: 10.0,
71                init_damping_mult: 1.0,
72                liquid: StageParams {
73                    iterations: 200,
74                    temperature: 2000.0,
75                    attraction: 10.0,
76                    damping_mult: 1.0,
77                },
78                expansion: StageParams {
79                    iterations: 200,
80                    temperature: 2000.0,
81                    attraction: 2.0,
82                    damping_mult: 1.0,
83                },
84                cooldown: StageParams {
85                    iterations: 200,
86                    temperature: 2000.0,
87                    attraction: 1.0,
88                    damping_mult: 0.1,
89                },
90                crunch: StageParams {
91                    iterations: 50,
92                    temperature: 250.0,
93                    attraction: 1.0,
94                    damping_mult: 0.25,
95                },
96                simmer: StageParams {
97                    iterations: 100,
98                    temperature: 250.0,
99                    attraction: 0.5,
100                    damping_mult: 0.0,
101                },
102            },
103            DrlTemplate::Coarsen => Self {
104                edge_cut,
105                init_iterations: 0,
106                init_temperature: 2000.0,
107                init_attraction: 10.0,
108                init_damping_mult: 1.0,
109                liquid: StageParams {
110                    iterations: 200,
111                    temperature: 2000.0,
112                    attraction: 2.0,
113                    damping_mult: 1.0,
114                },
115                expansion: StageParams {
116                    iterations: 200,
117                    temperature: 2000.0,
118                    attraction: 10.0,
119                    damping_mult: 1.0,
120                },
121                cooldown: StageParams {
122                    iterations: 200,
123                    temperature: 2000.0,
124                    attraction: 1.0,
125                    damping_mult: 0.1,
126                },
127                crunch: StageParams {
128                    iterations: 50,
129                    temperature: 250.0,
130                    attraction: 1.0,
131                    damping_mult: 0.25,
132                },
133                simmer: StageParams {
134                    iterations: 100,
135                    temperature: 250.0,
136                    attraction: 0.5,
137                    damping_mult: 0.0,
138                },
139            },
140            DrlTemplate::Coarsest => Self {
141                edge_cut,
142                init_iterations: 0,
143                init_temperature: 2000.0,
144                init_attraction: 10.0,
145                init_damping_mult: 1.0,
146                liquid: StageParams {
147                    iterations: 200,
148                    temperature: 2000.0,
149                    attraction: 2.0,
150                    damping_mult: 1.0,
151                },
152                expansion: StageParams {
153                    iterations: 200,
154                    temperature: 2000.0,
155                    attraction: 10.0,
156                    damping_mult: 1.0,
157                },
158                cooldown: StageParams {
159                    iterations: 200,
160                    temperature: 2000.0,
161                    attraction: 1.0,
162                    damping_mult: 0.1,
163                },
164                crunch: StageParams {
165                    iterations: 200,
166                    temperature: 250.0,
167                    attraction: 1.0,
168                    damping_mult: 0.25,
169                },
170                simmer: StageParams {
171                    iterations: 100,
172                    temperature: 250.0,
173                    attraction: 0.5,
174                    damping_mult: 0.0,
175                },
176            },
177            DrlTemplate::Refine => Self {
178                edge_cut,
179                init_iterations: 0,
180                init_temperature: 50.0,
181                init_attraction: 0.5,
182                init_damping_mult: 0.0,
183                liquid: StageParams {
184                    iterations: 0,
185                    temperature: 2000.0,
186                    attraction: 2.0,
187                    damping_mult: 1.0,
188                },
189                expansion: StageParams {
190                    iterations: 50,
191                    temperature: 500.0,
192                    attraction: 0.1,
193                    damping_mult: 0.25,
194                },
195                cooldown: StageParams {
196                    iterations: 50,
197                    temperature: 200.0,
198                    attraction: 1.0,
199                    damping_mult: 0.1,
200                },
201                crunch: StageParams {
202                    iterations: 50,
203                    temperature: 250.0,
204                    attraction: 1.0,
205                    damping_mult: 0.25,
206                },
207                simmer: StageParams {
208                    iterations: 0,
209                    temperature: 250.0,
210                    attraction: 0.5,
211                    damping_mult: 0.0,
212                },
213            },
214            DrlTemplate::Final => Self {
215                edge_cut,
216                init_iterations: 0,
217                init_temperature: 50.0,
218                init_attraction: 0.5,
219                init_damping_mult: 0.0,
220                liquid: StageParams {
221                    iterations: 0,
222                    temperature: 2000.0,
223                    attraction: 2.0,
224                    damping_mult: 1.0,
225                },
226                expansion: StageParams {
227                    iterations: 50,
228                    temperature: 50.0,
229                    attraction: 0.1,
230                    damping_mult: 0.25,
231                },
232                cooldown: StageParams {
233                    iterations: 50,
234                    temperature: 200.0,
235                    attraction: 1.0,
236                    damping_mult: 0.1,
237                },
238                crunch: StageParams {
239                    iterations: 50,
240                    temperature: 250.0,
241                    attraction: 1.0,
242                    damping_mult: 0.25,
243                },
244                simmer: StageParams {
245                    iterations: 25,
246                    temperature: 250.0,
247                    attraction: 0.5,
248                    damping_mult: 0.0,
249                },
250            },
251        }
252    }
253}
254
255impl Default for DrlOptions {
256    fn default() -> Self {
257        Self::from_template(DrlTemplate::Default)
258    }
259}
260
261/// Compute the DrL layout.
262///
263/// A multi-phase simulated annealing force-directed layout with
264/// density-grid repulsion. Designed for large graphs.
265///
266/// # Arguments
267///
268/// * `graph` — input graph (treated as undirected).
269/// * `seed` — optional initial positions. If `None`, random positions
270///   are generated.
271/// * `options` — algorithm parameters (use `DrlOptions::default()` or
272///   `DrlOptions::from_template(...)` for presets).
273/// * `weights` — optional edge weights (must be positive).
274///
275/// Returns `[x, y]` positions for each vertex.
276///
277/// # Errors
278///
279/// Returns `InvalidArgument` if weights contain non-positive values,
280/// or if seed length doesn't match vertex count.
281///
282/// # Examples
283///
284/// ```
285/// use rust_igraph::{Graph, layout_drl, DrlOptions, DrlTemplate};
286///
287/// let mut g = Graph::with_vertices(6);
288/// g.add_edge(0, 1).unwrap();
289/// g.add_edge(1, 2).unwrap();
290/// g.add_edge(2, 3).unwrap();
291/// g.add_edge(3, 4).unwrap();
292/// g.add_edge(4, 5).unwrap();
293///
294/// let options = DrlOptions::from_template(DrlTemplate::Default);
295/// let pos = layout_drl(&g, None, &options, None).unwrap();
296/// assert_eq!(pos.len(), 6);
297/// assert!(pos.iter().all(|p| p[0].is_finite() && p[1].is_finite()));
298/// ```
299pub fn layout_drl(
300    graph: &Graph,
301    seed: Option<&[[f64; 2]]>,
302    options: &DrlOptions,
303    weights: Option<&[f64]>,
304) -> IgraphResult<Vec<[f64; 2]>> {
305    let n = graph.vcount() as usize;
306    if n == 0 {
307        return Ok(Vec::new());
308    }
309    if n == 1 {
310        return Ok(vec![[0.0, 0.0]]);
311    }
312
313    if let Some(s) = seed {
314        if s.len() != n {
315            return Err(IgraphError::InvalidArgument(format!(
316                "seed length ({}) must equal vertex count ({})",
317                s.len(),
318                n
319            )));
320        }
321    }
322
323    if let Some(w) = weights {
324        let ne = graph.ecount();
325        if w.len() != ne {
326            return Err(IgraphError::InvalidArgument(format!(
327                "weights length ({}) must equal edge count ({})",
328                w.len(),
329                ne
330            )));
331        }
332        if ne > 0 {
333            for &wt in w {
334                if wt <= 0.0 || !wt.is_finite() {
335                    return Err(IgraphError::InvalidArgument(
336                        "weights must be positive and finite for DrL layout".into(),
337                    ));
338                }
339            }
340        }
341    }
342
343    // Build weighted adjacency
344    let mut neighbors: Vec<Vec<(usize, f32)>> = vec![Vec::new(); n];
345    for eid in 0..graph.ecount() as u32 {
346        if let Ok((src, tgt)) = graph.edge(eid) {
347            let w = weights.map_or(1.0_f32, |ws| ws[eid as usize] as f32);
348            neighbors[src as usize].push((tgt as usize, w));
349            if src != tgt {
350                neighbors[tgt as usize].push((src as usize, w));
351            }
352        }
353    }
354
355    // Initialize positions
356    let mut rng = SplitMix64::new(42);
357    let mut positions: Vec<[f32; 2]> = if let Some(s) = seed {
358        s.iter().map(|p| [p[0] as f32, p[1] as f32]).collect()
359    } else {
360        (0..n)
361            .map(|_| {
362                [
363                    (rng.next_uniform() as f32 - 0.5) * 1000.0,
364                    (rng.next_uniform() as f32 - 0.5) * 1000.0,
365                ]
366            })
367            .collect()
368    };
369
370    // Initialize density grid
371    let mut density = DensityGrid::new();
372
373    // Edge cutting parameters
374    let cut_length_end = (40000.0 * (1.0 - options.edge_cut)).max(1.0);
375    let cut_length_start = 4.0 * cut_length_end;
376    let cut_rate = (cut_length_start - cut_length_end) / 400.0;
377    let mut cut_off_length = cut_length_start;
378
379    let mut min_edges: f32 = 20.0;
380    let mut temperature;
381    let mut attraction;
382    let mut damping_mult;
383    let mut fine_density = false;
384    let mut first_add = true;
385    let mut fine_first_add = true;
386
387    // Run through stages
388    let stages: [StageParams; 5] = [
389        options.liquid,
390        options.expansion,
391        options.cooldown,
392        options.crunch,
393        options.simmer,
394    ];
395
396    for (stage_idx, stage) in stages.iter().enumerate() {
397        temperature = stage.temperature;
398        attraction = stage.attraction;
399        damping_mult = stage.damping_mult;
400
401        if stage_idx == 4 {
402            fine_density = true;
403            min_edges = 99.0;
404        }
405
406        for iter in 0..stage.iterations {
407            // Stage-specific parameter adjustments
408            match stage_idx {
409                1 => {
410                    // Expansion: decay attraction, min_edges, damping, cut_off
411                    if attraction > 1.0 {
412                        attraction -= 0.05;
413                    }
414                    if min_edges > 12.0 {
415                        min_edges -= 0.05;
416                    }
417                    cut_off_length -= cut_rate;
418                    if damping_mult > 0.1 {
419                        damping_mult -= 0.005;
420                    }
421                }
422                2 => {
423                    // Cooldown: decay temperature, cut_off, min_edges
424                    if temperature > 50.0 {
425                        temperature -= 10.0;
426                    }
427                    if cut_off_length > cut_length_end {
428                        cut_off_length -= cut_rate * 2.0;
429                    }
430                    if min_edges > 1.0 {
431                        min_edges -= 0.2;
432                    }
433                }
434                4 if temperature > 50.0 => {
435                    temperature -= 2.0;
436                }
437                _ => {}
438            }
439
440            // Update all nodes
441            update_all_nodes(
442                n,
443                &mut positions,
444                &neighbors,
445                &mut density,
446                temperature,
447                attraction,
448                damping_mult,
449                min_edges,
450                cut_off_length,
451                cut_length_end,
452                fine_density,
453                first_add,
454                fine_first_add,
455                stage_idx,
456                &mut rng,
457            );
458
459            first_add = false;
460            if fine_density && iter > 0 {
461                fine_first_add = false;
462            }
463        }
464
465        // Post-stage transitions
466        if stage_idx == 1 {
467            min_edges = 12.0;
468        } else if stage_idx == 2 {
469            cut_off_length = cut_length_end;
470            min_edges = 1.0;
471        }
472    }
473
474    // Convert to f64 output
475    Ok(positions
476        .iter()
477        .map(|p| [p[0] as f64, p[1] as f64])
478        .collect())
479}
480
481fn update_all_nodes(
482    n: usize,
483    positions: &mut [[f32; 2]],
484    neighbors: &[Vec<(usize, f32)>],
485    density: &mut DensityGrid,
486    temperature: f32,
487    attraction: f32,
488    damping_mult: f32,
489    min_edges: f32,
490    cut_off_length: f32,
491    cut_end: f32,
492    fine_density: bool,
493    first_add: bool,
494    fine_first_add: bool,
495    stage: usize,
496    rng: &mut SplitMix64,
497) {
498    let jump_length = 0.010 * temperature;
499    let attraction_factor = attraction * attraction * attraction * attraction * 2e-2;
500
501    for node_ind in 0..n {
502        // Subtract old position from density
503        density.subtract(positions[node_ind], first_add, fine_first_add, fine_density);
504
505        // Compute energy at analytic (centroid) position
506        let (cx, cy) = solve_analytic(
507            node_ind,
508            positions,
509            neighbors,
510            damping_mult,
511            min_edges,
512            cut_off_length,
513            cut_end,
514        );
515        let old_pos = positions[node_ind];
516        positions[node_ind] = [cx, cy];
517        let energy_centroid = compute_energy(
518            node_ind,
519            positions,
520            neighbors,
521            density,
522            attraction_factor,
523            fine_density,
524            stage,
525        );
526
527        // Compute energy at random perturbation
528        let rx = cx + (0.5 - rng.next_uniform() as f32) * jump_length;
529        let ry = cy + (0.5 - rng.next_uniform() as f32) * jump_length;
530        positions[node_ind] = [rx, ry];
531        let energy_random = compute_energy(
532            node_ind,
533            positions,
534            neighbors,
535            density,
536            attraction_factor,
537            fine_density,
538            stage,
539        );
540
541        // Restore old position for density subtraction bookkeeping
542        positions[node_ind] = old_pos;
543        if (!fine_density && !first_add) || (!fine_first_add) {
544            density.add(positions[node_ind], fine_density);
545        }
546
547        // Choose lower energy position
548        let new_pos = if energy_centroid < energy_random {
549            [cx, cy]
550        } else {
551            [rx, ry]
552        };
553
554        // Update density: subtract old, add new
555        density.subtract(positions[node_ind], false, false, fine_density);
556        positions[node_ind] = new_pos;
557        density.add(positions[node_ind], fine_density);
558    }
559}
560
561fn solve_analytic(
562    node_ind: usize,
563    positions: &[[f32; 2]],
564    neighbors: &[Vec<(usize, f32)>],
565    damping_mult: f32,
566    min_edges: f32,
567    cut_off_length: f32,
568    cut_end: f32,
569) -> (f32, f32) {
570    let mut total_weight: f32 = 0.0;
571    let mut x_sum: f32 = 0.0;
572    let mut y_sum: f32 = 0.0;
573
574    for &(neighbor, weight) in &neighbors[node_ind] {
575        total_weight += weight;
576        x_sum += weight * positions[neighbor][0];
577        y_sum += weight * positions[neighbor][1];
578    }
579
580    if total_weight <= 0.0 {
581        return (positions[node_ind][0], positions[node_ind][1]);
582    }
583
584    let x_cen = x_sum / total_weight;
585    let y_cen = y_sum / total_weight;
586    let damping = 1.0 - damping_mult;
587    let pos_x = damping * positions[node_ind][0] + (1.0 - damping) * x_cen;
588    let pos_y = damping * positions[node_ind][1] + (1.0 - damping) * y_cen;
589
590    // Edge cutting not applied when min_edges >= 99 or cut_end >= 39500
591    if min_edges >= 99.0 || cut_end >= 39500.0 {
592        return (pos_x, pos_y);
593    }
594
595    // Edge cutting logic would modify the neighbor list — we skip it in
596    // this implementation since mutating the neighbor list during iteration
597    // is complex and the effect is cosmetic for most graphs
598    let _ = cut_off_length;
599
600    (pos_x, pos_y)
601}
602
603fn compute_energy(
604    node_ind: usize,
605    positions: &[[f32; 2]],
606    neighbors: &[Vec<(usize, f32)>],
607    density: &DensityGrid,
608    attraction_factor: f32,
609    fine_density: bool,
610    stage: usize,
611) -> f32 {
612    let mut energy: f32 = 0.0;
613
614    for &(neighbor, weight) in &neighbors[node_ind] {
615        let dx = positions[node_ind][0] - positions[neighbor][0];
616        let dy = positions[node_ind][1] - positions[neighbor][1];
617        let mut dist = dx * dx + dy * dy;
618        if stage < 2 {
619            dist *= dist;
620        }
621        if stage == 0 {
622            dist *= dist;
623        }
624        energy += weight * attraction_factor * dist;
625    }
626
627    energy += density.get_density(positions[node_ind][0], positions[node_ind][1], fine_density);
628    energy
629}
630
631/// Compute the DrL layout in 3D.
632///
633/// The 3D variant of DrL uses a smaller density grid (100³ vs 400²
634/// for 2D) to keep memory usage practical.
635///
636/// # Arguments
637///
638/// * `graph` — input graph (treated as undirected).
639/// * `seed` — optional initial positions. If `None`, random positions
640///   are generated.
641/// * `options` — algorithm parameters (use `DrlOptions::default()` or
642///   `DrlOptions::from_template(...)` for presets).
643/// * `weights` — optional edge weights (must be positive).
644///
645/// Returns `[x, y, z]` positions for each vertex.
646///
647/// # Errors
648///
649/// Returns `InvalidArgument` if weights contain non-positive values,
650/// or if seed length doesn't match vertex count.
651///
652/// # Examples
653///
654/// ```
655/// use rust_igraph::{Graph, layout_drl_3d, DrlOptions, DrlTemplate};
656///
657/// let mut g = Graph::with_vertices(6);
658/// g.add_edge(0, 1).unwrap();
659/// g.add_edge(1, 2).unwrap();
660/// g.add_edge(2, 3).unwrap();
661/// g.add_edge(3, 4).unwrap();
662/// g.add_edge(4, 5).unwrap();
663///
664/// let options = DrlOptions::from_template(DrlTemplate::Default);
665/// let pos = layout_drl_3d(&g, None, &options, None).unwrap();
666/// assert_eq!(pos.len(), 6);
667/// assert!(pos.iter().all(|p| p[0].is_finite() && p[1].is_finite() && p[2].is_finite()));
668/// ```
669pub fn layout_drl_3d(
670    graph: &Graph,
671    seed: Option<&[[f64; 3]]>,
672    options: &DrlOptions,
673    weights: Option<&[f64]>,
674) -> IgraphResult<Vec<[f64; 3]>> {
675    let n = graph.vcount() as usize;
676    if n == 0 {
677        return Ok(Vec::new());
678    }
679    if n == 1 {
680        return Ok(vec![[0.0, 0.0, 0.0]]);
681    }
682
683    if let Some(s) = seed {
684        if s.len() != n {
685            return Err(IgraphError::InvalidArgument(format!(
686                "seed length ({}) must equal vertex count ({})",
687                s.len(),
688                n
689            )));
690        }
691    }
692
693    if let Some(w) = weights {
694        let ne = graph.ecount();
695        if w.len() != ne {
696            return Err(IgraphError::InvalidArgument(format!(
697                "weights length ({}) must equal edge count ({})",
698                w.len(),
699                ne
700            )));
701        }
702        if ne > 0 {
703            for &wt in w {
704                if wt <= 0.0 || !wt.is_finite() {
705                    return Err(IgraphError::InvalidArgument(
706                        "weights must be positive and finite for DrL layout".into(),
707                    ));
708                }
709            }
710        }
711    }
712
713    let mut neighbors: Vec<Vec<(usize, f32)>> = vec![Vec::new(); n];
714    for eid in 0..graph.ecount() as u32 {
715        if let Ok((src, tgt)) = graph.edge(eid) {
716            let w = weights.map_or(1.0_f32, |ws| ws[eid as usize] as f32);
717            neighbors[src as usize].push((tgt as usize, w));
718            if src != tgt {
719                neighbors[tgt as usize].push((src as usize, w));
720            }
721        }
722    }
723
724    let mut rng = SplitMix64::new(42);
725    let mut positions: Vec<[f32; 3]> = if let Some(s) = seed {
726        s.iter()
727            .map(|p| [p[0] as f32, p[1] as f32, p[2] as f32])
728            .collect()
729    } else {
730        (0..n)
731            .map(|_| {
732                [
733                    (rng.next_uniform() as f32 - 0.5) * 1000.0,
734                    (rng.next_uniform() as f32 - 0.5) * 1000.0,
735                    (rng.next_uniform() as f32 - 0.5) * 1000.0,
736                ]
737            })
738            .collect()
739    };
740
741    let mut density = DensityGrid3d::new();
742
743    let cut_length_end = (40000.0 * (1.0 - options.edge_cut)).max(1.0);
744    let cut_length_start = 4.0 * cut_length_end;
745    let cut_rate = (cut_length_start - cut_length_end) / 400.0;
746    let mut cut_off_length = cut_length_start;
747
748    let mut min_edges: f32 = 20.0;
749    let mut temperature;
750    let mut attraction;
751    let mut damping_mult;
752    let mut fine_density = false;
753    let mut first_add = true;
754    let mut fine_first_add = true;
755
756    let stages: [StageParams; 5] = [
757        options.liquid,
758        options.expansion,
759        options.cooldown,
760        options.crunch,
761        options.simmer,
762    ];
763
764    for (stage_idx, stage) in stages.iter().enumerate() {
765        temperature = stage.temperature;
766        attraction = stage.attraction;
767        damping_mult = stage.damping_mult;
768
769        if stage_idx == 4 {
770            fine_density = true;
771            min_edges = 99.0;
772        }
773
774        for iter in 0..stage.iterations {
775            match stage_idx {
776                1 => {
777                    if attraction > 1.0 {
778                        attraction -= 0.05;
779                    }
780                    if min_edges > 12.0 {
781                        min_edges -= 0.05;
782                    }
783                    cut_off_length -= cut_rate;
784                    if damping_mult > 0.1 {
785                        damping_mult -= 0.005;
786                    }
787                }
788                2 => {
789                    if temperature > 50.0 {
790                        temperature -= 10.0;
791                    }
792                    if cut_off_length > cut_length_end {
793                        cut_off_length -= cut_rate * 2.0;
794                    }
795                    if min_edges > 1.0 {
796                        min_edges -= 0.2;
797                    }
798                }
799                4 if temperature > 50.0 => {
800                    temperature -= 2.0;
801                }
802                _ => {}
803            }
804
805            update_all_nodes_3d(
806                n,
807                &mut positions,
808                &neighbors,
809                &mut density,
810                temperature,
811                attraction,
812                damping_mult,
813                min_edges,
814                cut_off_length,
815                cut_length_end,
816                fine_density,
817                first_add,
818                fine_first_add,
819                stage_idx,
820                &mut rng,
821            );
822
823            first_add = false;
824            if fine_density && iter > 0 {
825                fine_first_add = false;
826            }
827        }
828
829        if stage_idx == 1 {
830            min_edges = 12.0;
831        } else if stage_idx == 2 {
832            cut_off_length = cut_length_end;
833            min_edges = 1.0;
834        }
835    }
836
837    Ok(positions
838        .iter()
839        .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
840        .collect())
841}
842
843#[allow(clippy::too_many_arguments)]
844fn update_all_nodes_3d(
845    n: usize,
846    positions: &mut [[f32; 3]],
847    neighbors: &[Vec<(usize, f32)>],
848    density: &mut DensityGrid3d,
849    temperature: f32,
850    attraction: f32,
851    damping_mult: f32,
852    min_edges: f32,
853    cut_off_length: f32,
854    cut_end: f32,
855    fine_density: bool,
856    first_add: bool,
857    fine_first_add: bool,
858    stage: usize,
859    rng: &mut SplitMix64,
860) {
861    let jump_length = 0.010 * temperature;
862    let attraction_factor = attraction * attraction * attraction * attraction * 2e-2;
863
864    for node_ind in 0..n {
865        density.subtract(positions[node_ind], first_add, fine_first_add, fine_density);
866
867        let (cx, cy, cz) = solve_analytic_3d(
868            node_ind,
869            positions,
870            neighbors,
871            damping_mult,
872            min_edges,
873            cut_off_length,
874            cut_end,
875        );
876        let old_pos = positions[node_ind];
877        positions[node_ind] = [cx, cy, cz];
878        let energy_centroid = compute_energy_3d(
879            node_ind,
880            positions,
881            neighbors,
882            density,
883            attraction_factor,
884            fine_density,
885            stage,
886        );
887
888        let rx = cx + (0.5 - rng.next_uniform() as f32) * jump_length;
889        let ry = cy + (0.5 - rng.next_uniform() as f32) * jump_length;
890        let rz = cz + (0.5 - rng.next_uniform() as f32) * jump_length;
891        positions[node_ind] = [rx, ry, rz];
892        let energy_random = compute_energy_3d(
893            node_ind,
894            positions,
895            neighbors,
896            density,
897            attraction_factor,
898            fine_density,
899            stage,
900        );
901
902        positions[node_ind] = old_pos;
903        if (!fine_density && !first_add) || (!fine_first_add) {
904            density.add(positions[node_ind], fine_density);
905        }
906
907        let new_pos = if energy_centroid < energy_random {
908            [cx, cy, cz]
909        } else {
910            [rx, ry, rz]
911        };
912
913        density.subtract(positions[node_ind], false, false, fine_density);
914        positions[node_ind] = new_pos;
915        density.add(positions[node_ind], fine_density);
916    }
917}
918
919fn solve_analytic_3d(
920    node_ind: usize,
921    positions: &[[f32; 3]],
922    neighbors: &[Vec<(usize, f32)>],
923    damping_mult: f32,
924    min_edges: f32,
925    cut_off_length: f32,
926    cut_end: f32,
927) -> (f32, f32, f32) {
928    let mut total_weight: f32 = 0.0;
929    let mut x_sum: f32 = 0.0;
930    let mut y_sum: f32 = 0.0;
931    let mut z_sum: f32 = 0.0;
932
933    for &(neighbor, weight) in &neighbors[node_ind] {
934        total_weight += weight;
935        x_sum += weight * positions[neighbor][0];
936        y_sum += weight * positions[neighbor][1];
937        z_sum += weight * positions[neighbor][2];
938    }
939
940    if total_weight <= 0.0 {
941        return (
942            positions[node_ind][0],
943            positions[node_ind][1],
944            positions[node_ind][2],
945        );
946    }
947
948    let x_cen = x_sum / total_weight;
949    let y_cen = y_sum / total_weight;
950    let z_cen = z_sum / total_weight;
951    let damping = 1.0 - damping_mult;
952    let pos_x = damping * positions[node_ind][0] + (1.0 - damping) * x_cen;
953    let pos_y = damping * positions[node_ind][1] + (1.0 - damping) * y_cen;
954    let pos_z = damping * positions[node_ind][2] + (1.0 - damping) * z_cen;
955
956    if min_edges >= 99.0 || cut_end >= 39500.0 {
957        return (pos_x, pos_y, pos_z);
958    }
959
960    let _ = cut_off_length;
961
962    (pos_x, pos_y, pos_z)
963}
964
965fn compute_energy_3d(
966    node_ind: usize,
967    positions: &[[f32; 3]],
968    neighbors: &[Vec<(usize, f32)>],
969    density: &DensityGrid3d,
970    attraction_factor: f32,
971    fine_density: bool,
972    stage: usize,
973) -> f32 {
974    let mut energy: f32 = 0.0;
975
976    for &(neighbor, weight) in &neighbors[node_ind] {
977        let dx = positions[node_ind][0] - positions[neighbor][0];
978        let dy = positions[node_ind][1] - positions[neighbor][1];
979        let dz = positions[node_ind][2] - positions[neighbor][2];
980        let mut dist = dx * dx + dy * dy + dz * dz;
981        if stage < 2 {
982            dist *= dist;
983        }
984        if stage == 0 {
985            dist *= dist;
986        }
987        energy += weight * attraction_factor * dist;
988    }
989
990    energy += density.get_density(
991        positions[node_ind][0],
992        positions[node_ind][1],
993        positions[node_ind][2],
994        fine_density,
995    );
996    energy
997}
998
999// ═══════════════════════════════════════════════════════════════════
1000// Density Grid (2D)
1001// ═══════════════════════════════════════════════════════════════════
1002
1003struct DensityGrid {
1004    density: Vec<f32>,
1005    fall_off: Vec<f32>,
1006    bins: Vec<Vec<[f32; 2]>>,
1007}
1008
1009impl DensityGrid {
1010    fn new() -> Self {
1011        let mut fall_off = vec![0.0_f32; (RADIUS * 2 + 1) as usize * (RADIUS * 2 + 1) as usize];
1012        let diam = (RADIUS * 2 + 1) as usize;
1013        for i in 0..diam {
1014            for j in 0..diam {
1015                let fi = (RADIUS - (i as i32 - RADIUS).abs()) as f32 / RADIUS as f32;
1016                let fj = (RADIUS - (j as i32 - RADIUS).abs()) as f32 / RADIUS as f32;
1017                fall_off[i * diam + j] = fi * fj;
1018            }
1019        }
1020        Self {
1021            density: vec![0.0_f32; GRID_SIZE * GRID_SIZE],
1022            fall_off,
1023            bins: vec![Vec::new(); GRID_SIZE * GRID_SIZE],
1024        }
1025    }
1026
1027    fn pos_to_grid(x: f32, y: f32) -> (i32, i32) {
1028        let gx = ((x + HALF_VIEW + 0.5) * VIEW_TO_GRID) as i32;
1029        let gy = ((y + HALF_VIEW + 0.5) * VIEW_TO_GRID) as i32;
1030        (gx, gy)
1031    }
1032
1033    fn add(&mut self, pos: [f32; 2], fine_density: bool) {
1034        let (gx, gy) = Self::pos_to_grid(pos[0], pos[1]);
1035        if fine_density {
1036            if gx >= 0 && gx < GRID_SIZE as i32 && gy >= 0 && gy < GRID_SIZE as i32 {
1037                self.bins[gy as usize * GRID_SIZE + gx as usize].push(pos);
1038            }
1039            return;
1040        }
1041        let sx = gx - RADIUS;
1042        let sy = gy - RADIUS;
1043        if sx < 0 || sy < 0 || sx >= GRID_SIZE as i32 || sy >= GRID_SIZE as i32 {
1044            return;
1045        }
1046        let diam = (RADIUS * 2 + 1) as usize;
1047        for i in 0..diam {
1048            for j in 0..diam {
1049                let dy = sy as usize + i;
1050                let dx = sx as usize + j;
1051                if dy < GRID_SIZE && dx < GRID_SIZE {
1052                    self.density[dy * GRID_SIZE + dx] += self.fall_off[i * diam + j];
1053                }
1054            }
1055        }
1056    }
1057
1058    fn subtract(
1059        &mut self,
1060        pos: [f32; 2],
1061        first_add: bool,
1062        fine_first_add: bool,
1063        fine_density: bool,
1064    ) {
1065        if fine_density && !fine_first_add {
1066            let (gx, gy) = Self::pos_to_grid(pos[0], pos[1]);
1067            if gx >= 0 && gx < GRID_SIZE as i32 && gy >= 0 && gy < GRID_SIZE as i32 {
1068                let idx = gy as usize * GRID_SIZE + gx as usize;
1069                if !self.bins[idx].is_empty() {
1070                    self.bins[idx].remove(0);
1071                }
1072            }
1073        } else if !first_add {
1074            let (gx, gy) = Self::pos_to_grid(pos[0], pos[1]);
1075            let sx = gx - RADIUS;
1076            let sy = gy - RADIUS;
1077            if sx < 0 || sy < 0 || sx >= GRID_SIZE as i32 || sy >= GRID_SIZE as i32 {
1078                return;
1079            }
1080            let diam = (RADIUS * 2 + 1) as usize;
1081            for i in 0..diam {
1082                for j in 0..diam {
1083                    let dy = sy as usize + i;
1084                    let dx = sx as usize + j;
1085                    if dy < GRID_SIZE && dx < GRID_SIZE {
1086                        self.density[dy * GRID_SIZE + dx] -= self.fall_off[i * diam + j];
1087                    }
1088                }
1089            }
1090        }
1091    }
1092
1093    fn get_density(&self, x: f32, y: f32, fine_density: bool) -> f32 {
1094        let (gx, gy) = Self::pos_to_grid(x, y);
1095        let boundary = 10_i32;
1096
1097        if gx >= GRID_SIZE as i32 - boundary
1098            || gx < boundary
1099            || gy >= GRID_SIZE as i32 - boundary
1100            || gy < boundary
1101        {
1102            return 10000.0;
1103        }
1104
1105        if fine_density {
1106            let mut d: f32 = 0.0;
1107            for i in (gy - 1)..=(gy + 1) {
1108                for j in (gx - 1)..=(gx + 1) {
1109                    if i >= 0 && i < GRID_SIZE as i32 && j >= 0 && j < GRID_SIZE as i32 {
1110                        let idx = i as usize * GRID_SIZE + j as usize;
1111                        for p in &self.bins[idx] {
1112                            let dx = x - p[0];
1113                            let dy_val = y - p[1];
1114                            let dist_sq = dx * dx + dy_val * dy_val;
1115                            d += 1e-4 / (dist_sq + 1e-50);
1116                        }
1117                    }
1118                }
1119            }
1120            d
1121        } else {
1122            let val = self.density[gy as usize * GRID_SIZE + gx as usize];
1123            val * val
1124        }
1125    }
1126}
1127
1128// ═══════════════════════════════════════════════════════════════════
1129// Density Grid (3D)
1130// ═══════════════════════════════════════════════════════════════════
1131
1132const GRID_SIZE_3D: usize = 100;
1133const RADIUS_3D: i32 = 10;
1134const HALF_VIEW_3D: f32 = 125.0;
1135const VIEW_TO_GRID_3D: f32 = 0.4;
1136
1137struct DensityGrid3d {
1138    density: Vec<f32>,
1139    fall_off: Vec<f32>,
1140    bins: Vec<Vec<[f32; 3]>>,
1141}
1142
1143impl DensityGrid3d {
1144    fn new() -> Self {
1145        let diam = (RADIUS_3D * 2 + 1) as usize;
1146        let mut fall_off = vec![0.0_f32; diam * diam * diam];
1147        for i in 0..diam {
1148            for j in 0..diam {
1149                for k in 0..diam {
1150                    let fi = (RADIUS_3D - (i as i32 - RADIUS_3D).abs()) as f32 / RADIUS_3D as f32;
1151                    let fj = (RADIUS_3D - (j as i32 - RADIUS_3D).abs()) as f32 / RADIUS_3D as f32;
1152                    let fk = (RADIUS_3D - (k as i32 - RADIUS_3D).abs()) as f32 / RADIUS_3D as f32;
1153                    fall_off[(i * diam + j) * diam + k] = fi * fj * fk;
1154                }
1155            }
1156        }
1157        let total = GRID_SIZE_3D * GRID_SIZE_3D * GRID_SIZE_3D;
1158        Self {
1159            density: vec![0.0_f32; total],
1160            fall_off,
1161            bins: vec![Vec::new(); total],
1162        }
1163    }
1164
1165    fn pos_to_grid(x: f32, y: f32, z: f32) -> (i32, i32, i32) {
1166        let gx = ((x + HALF_VIEW_3D + 0.5) * VIEW_TO_GRID_3D) as i32;
1167        let gy = ((y + HALF_VIEW_3D + 0.5) * VIEW_TO_GRID_3D) as i32;
1168        let gz = ((z + HALF_VIEW_3D + 0.5) * VIEW_TO_GRID_3D) as i32;
1169        (gx, gy, gz)
1170    }
1171
1172    fn add(&mut self, pos: [f32; 3], fine_density: bool) {
1173        let (gx, gy, gz) = Self::pos_to_grid(pos[0], pos[1], pos[2]);
1174        if fine_density {
1175            if gx >= 0
1176                && gx < GRID_SIZE_3D as i32
1177                && gy >= 0
1178                && gy < GRID_SIZE_3D as i32
1179                && gz >= 0
1180                && gz < GRID_SIZE_3D as i32
1181            {
1182                let idx = (gz as usize * GRID_SIZE_3D + gy as usize) * GRID_SIZE_3D + gx as usize;
1183                self.bins[idx].push(pos);
1184            }
1185            return;
1186        }
1187        let sx = gx - RADIUS_3D;
1188        let sy = gy - RADIUS_3D;
1189        let sz = gz - RADIUS_3D;
1190        if sx < 0
1191            || sy < 0
1192            || sz < 0
1193            || sx >= GRID_SIZE_3D as i32
1194            || sy >= GRID_SIZE_3D as i32
1195            || sz >= GRID_SIZE_3D as i32
1196        {
1197            return;
1198        }
1199        let diam = (RADIUS_3D * 2 + 1) as usize;
1200        for i in 0..diam {
1201            for j in 0..diam {
1202                for k in 0..diam {
1203                    let dz = sz as usize + i;
1204                    let dy = sy as usize + j;
1205                    let dx = sx as usize + k;
1206                    if dz < GRID_SIZE_3D && dy < GRID_SIZE_3D && dx < GRID_SIZE_3D {
1207                        let grid_idx = (dz * GRID_SIZE_3D + dy) * GRID_SIZE_3D + dx;
1208                        self.density[grid_idx] += self.fall_off[(i * diam + j) * diam + k];
1209                    }
1210                }
1211            }
1212        }
1213    }
1214
1215    fn subtract(
1216        &mut self,
1217        pos: [f32; 3],
1218        first_add: bool,
1219        fine_first_add: bool,
1220        fine_density: bool,
1221    ) {
1222        if fine_density && !fine_first_add {
1223            let (gx, gy, gz) = Self::pos_to_grid(pos[0], pos[1], pos[2]);
1224            if gx >= 0
1225                && gx < GRID_SIZE_3D as i32
1226                && gy >= 0
1227                && gy < GRID_SIZE_3D as i32
1228                && gz >= 0
1229                && gz < GRID_SIZE_3D as i32
1230            {
1231                let idx = (gz as usize * GRID_SIZE_3D + gy as usize) * GRID_SIZE_3D + gx as usize;
1232                if !self.bins[idx].is_empty() {
1233                    self.bins[idx].remove(0);
1234                }
1235            }
1236        } else if !first_add {
1237            let (gx, gy, gz) = Self::pos_to_grid(pos[0], pos[1], pos[2]);
1238            let sx = gx - RADIUS_3D;
1239            let sy = gy - RADIUS_3D;
1240            let sz = gz - RADIUS_3D;
1241            if sx < 0
1242                || sy < 0
1243                || sz < 0
1244                || sx >= GRID_SIZE_3D as i32
1245                || sy >= GRID_SIZE_3D as i32
1246                || sz >= GRID_SIZE_3D as i32
1247            {
1248                return;
1249            }
1250            let diam = (RADIUS_3D * 2 + 1) as usize;
1251            for i in 0..diam {
1252                for j in 0..diam {
1253                    for k in 0..diam {
1254                        let dz = sz as usize + i;
1255                        let dy = sy as usize + j;
1256                        let dx = sx as usize + k;
1257                        if dz < GRID_SIZE_3D && dy < GRID_SIZE_3D && dx < GRID_SIZE_3D {
1258                            let grid_idx = (dz * GRID_SIZE_3D + dy) * GRID_SIZE_3D + dx;
1259                            self.density[grid_idx] -= self.fall_off[(i * diam + j) * diam + k];
1260                        }
1261                    }
1262                }
1263            }
1264        }
1265    }
1266
1267    fn get_density(&self, x: f32, y: f32, z: f32, fine_density: bool) -> f32 {
1268        let (gx, gy, gz) = Self::pos_to_grid(x, y, z);
1269        let boundary = 10_i32;
1270
1271        if gx >= GRID_SIZE_3D as i32 - boundary
1272            || gx < boundary
1273            || gy >= GRID_SIZE_3D as i32 - boundary
1274            || gy < boundary
1275            || gz >= GRID_SIZE_3D as i32 - boundary
1276            || gz < boundary
1277        {
1278            return 10000.0;
1279        }
1280
1281        if fine_density {
1282            let mut d: f32 = 0.0;
1283            for iz in (gz - 1)..=(gz + 1) {
1284                for iy in (gy - 1)..=(gy + 1) {
1285                    for ix in (gx - 1)..=(gx + 1) {
1286                        if iz >= 0
1287                            && iz < GRID_SIZE_3D as i32
1288                            && iy >= 0
1289                            && iy < GRID_SIZE_3D as i32
1290                            && ix >= 0
1291                            && ix < GRID_SIZE_3D as i32
1292                        {
1293                            let idx = (iz as usize * GRID_SIZE_3D + iy as usize) * GRID_SIZE_3D
1294                                + ix as usize;
1295                            for p in &self.bins[idx] {
1296                                let dx = x - p[0];
1297                                let dy_val = y - p[1];
1298                                let dz_val = z - p[2];
1299                                let dist_sq = dx * dx + dy_val * dy_val + dz_val * dz_val;
1300                                d += 1e-4 / (dist_sq + 1e-50);
1301                            }
1302                        }
1303                    }
1304                }
1305            }
1306            d
1307        } else {
1308            let idx = (gz as usize * GRID_SIZE_3D + gy as usize) * GRID_SIZE_3D + gx as usize;
1309            let val = self.density[idx];
1310            val * val
1311        }
1312    }
1313}
1314
1315// ═══════════════════════════════════════════════════════════════════
1316// Internal RNG
1317// ═══════════════════════════════════════════════════════════════════
1318
1319struct SplitMix64 {
1320    state: u64,
1321}
1322
1323impl SplitMix64 {
1324    fn new(seed: u64) -> Self {
1325        Self { state: seed }
1326    }
1327
1328    fn next_u64(&mut self) -> u64 {
1329        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
1330        let mut z = self.state;
1331        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1332        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1333        z ^ (z >> 31)
1334    }
1335
1336    fn next_uniform(&mut self) -> f64 {
1337        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
1338    }
1339}
1340
1341// ═══════════════════════════════════════════════════════════════════
1342// Tests
1343// ═══════════════════════════════════════════════════════════════════
1344
1345#[cfg(test)]
1346mod tests {
1347    use super::*;
1348
1349    #[test]
1350    fn test_drl_empty() {
1351        let g = Graph::with_vertices(0);
1352        let opts = DrlOptions::default();
1353        let pos = layout_drl(&g, None, &opts, None).unwrap();
1354        assert!(pos.is_empty());
1355    }
1356
1357    #[test]
1358    fn test_drl_single() {
1359        let g = Graph::with_vertices(1);
1360        let opts = DrlOptions::default();
1361        let pos = layout_drl(&g, None, &opts, None).unwrap();
1362        assert_eq!(pos.len(), 1);
1363        assert!(pos[0][0].abs() < 1e-10 && pos[0][1].abs() < 1e-10);
1364    }
1365
1366    #[test]
1367    fn test_drl_path() {
1368        let mut g = Graph::with_vertices(5);
1369        for i in 0..4 {
1370            g.add_edge(i, i + 1).unwrap();
1371        }
1372        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1373        let pos = layout_drl(&g, None, &opts, None).unwrap();
1374        assert_eq!(pos.len(), 5);
1375        for p in &pos {
1376            assert!(p[0].is_finite() && p[1].is_finite());
1377        }
1378    }
1379
1380    #[test]
1381    fn test_drl_cycle() {
1382        let mut g = Graph::with_vertices(6);
1383        for i in 0..6 {
1384            g.add_edge(i, (i + 1) % 6).unwrap();
1385        }
1386        let opts = DrlOptions::from_template(DrlTemplate::Final);
1387        let pos = layout_drl(&g, None, &opts, None).unwrap();
1388        assert_eq!(pos.len(), 6);
1389        for p in &pos {
1390            assert!(p[0].is_finite() && p[1].is_finite());
1391        }
1392    }
1393
1394    #[test]
1395    fn test_drl_complete() {
1396        let mut g = Graph::with_vertices(4);
1397        for i in 0..4u32 {
1398            for j in (i + 1)..4 {
1399                g.add_edge(i, j).unwrap();
1400            }
1401        }
1402        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1403        let pos = layout_drl(&g, None, &opts, None).unwrap();
1404        assert_eq!(pos.len(), 4);
1405        for p in &pos {
1406            assert!(p[0].is_finite() && p[1].is_finite());
1407        }
1408    }
1409
1410    #[test]
1411    fn test_drl_with_seed() {
1412        let mut g = Graph::with_vertices(3);
1413        g.add_edge(0, 1).unwrap();
1414        g.add_edge(1, 2).unwrap();
1415        let seed = vec![[0.0, 0.0], [100.0, 0.0], [50.0, 86.6]];
1416        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1417        let pos = layout_drl(&g, Some(&seed), &opts, None).unwrap();
1418        assert_eq!(pos.len(), 3);
1419        for p in &pos {
1420            assert!(p[0].is_finite() && p[1].is_finite());
1421        }
1422    }
1423
1424    #[test]
1425    fn test_drl_seed_wrong_length() {
1426        let g = Graph::with_vertices(3);
1427        let seed = vec![[0.0, 0.0]];
1428        let opts = DrlOptions::default();
1429        assert!(layout_drl(&g, Some(&seed), &opts, None).is_err());
1430    }
1431
1432    #[test]
1433    fn test_drl_weighted() {
1434        let mut g = Graph::with_vertices(4);
1435        g.add_edge(0, 1).unwrap();
1436        g.add_edge(1, 2).unwrap();
1437        g.add_edge(2, 3).unwrap();
1438        let weights = vec![1.0, 2.0, 0.5];
1439        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1440        let pos = layout_drl(&g, None, &opts, Some(&weights)).unwrap();
1441        assert_eq!(pos.len(), 4);
1442        for p in &pos {
1443            assert!(p[0].is_finite() && p[1].is_finite());
1444        }
1445    }
1446
1447    #[test]
1448    fn test_drl_bad_weights() {
1449        let mut g = Graph::with_vertices(3);
1450        g.add_edge(0, 1).unwrap();
1451        g.add_edge(1, 2).unwrap();
1452        let opts = DrlOptions::default();
1453        let bad_w = vec![1.0, -1.0];
1454        assert!(layout_drl(&g, None, &opts, Some(&bad_w)).is_err());
1455    }
1456
1457    #[test]
1458    fn test_drl_deterministic() {
1459        let mut g = Graph::with_vertices(5);
1460        g.add_edge(0, 1).unwrap();
1461        g.add_edge(1, 2).unwrap();
1462        g.add_edge(2, 3).unwrap();
1463        g.add_edge(3, 4).unwrap();
1464        g.add_edge(4, 0).unwrap();
1465        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1466        let pos1 = layout_drl(&g, None, &opts, None).unwrap();
1467        let pos2 = layout_drl(&g, None, &opts, None).unwrap();
1468        for i in 0..5 {
1469            assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-6);
1470            assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-6);
1471        }
1472    }
1473
1474    #[test]
1475    fn test_drl_all_templates() {
1476        let mut g = Graph::with_vertices(4);
1477        g.add_edge(0, 1).unwrap();
1478        g.add_edge(1, 2).unwrap();
1479        g.add_edge(2, 3).unwrap();
1480        g.add_edge(3, 0).unwrap();
1481
1482        for templ in [
1483            DrlTemplate::Default,
1484            DrlTemplate::Coarsen,
1485            DrlTemplate::Coarsest,
1486            DrlTemplate::Refine,
1487            DrlTemplate::Final,
1488        ] {
1489            let opts = DrlOptions::from_template(templ);
1490            let pos = layout_drl(&g, None, &opts, None).unwrap();
1491            assert_eq!(pos.len(), 4);
1492            for p in &pos {
1493                assert!(
1494                    p[0].is_finite() && p[1].is_finite(),
1495                    "Non-finite position with template {templ:?}"
1496                );
1497            }
1498        }
1499    }
1500
1501    #[test]
1502    fn test_drl_disconnected() {
1503        let mut g = Graph::with_vertices(6);
1504        g.add_edge(0, 1).unwrap();
1505        g.add_edge(1, 2).unwrap();
1506        g.add_edge(3, 4).unwrap();
1507        g.add_edge(4, 5).unwrap();
1508        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1509        let pos = layout_drl(&g, None, &opts, None).unwrap();
1510        assert_eq!(pos.len(), 6);
1511        for p in &pos {
1512            assert!(p[0].is_finite() && p[1].is_finite());
1513        }
1514    }
1515
1516    // ── 3D DrL tests ─────────────────────────────────────────────
1517
1518    #[test]
1519    fn test_drl_3d_empty() {
1520        let g = Graph::with_vertices(0);
1521        let opts = DrlOptions::default();
1522        let pos = layout_drl_3d(&g, None, &opts, None).unwrap();
1523        assert!(pos.is_empty());
1524    }
1525
1526    #[test]
1527    fn test_drl_3d_single() {
1528        let g = Graph::with_vertices(1);
1529        let opts = DrlOptions::default();
1530        let pos = layout_drl_3d(&g, None, &opts, None).unwrap();
1531        assert_eq!(pos.len(), 1);
1532        assert!(pos[0][0].abs() < 1e-10 && pos[0][1].abs() < 1e-10 && pos[0][2].abs() < 1e-10);
1533    }
1534
1535    #[test]
1536    fn test_drl_3d_path() {
1537        let mut g = Graph::with_vertices(5);
1538        for i in 0..4 {
1539            g.add_edge(i, i + 1).unwrap();
1540        }
1541        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1542        let pos = layout_drl_3d(&g, None, &opts, None).unwrap();
1543        assert_eq!(pos.len(), 5);
1544        for p in &pos {
1545            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1546        }
1547    }
1548
1549    #[test]
1550    fn test_drl_3d_cycle() {
1551        let mut g = Graph::with_vertices(6);
1552        for i in 0..6 {
1553            g.add_edge(i, (i + 1) % 6).unwrap();
1554        }
1555        let opts = DrlOptions::from_template(DrlTemplate::Final);
1556        let pos = layout_drl_3d(&g, None, &opts, None).unwrap();
1557        assert_eq!(pos.len(), 6);
1558        for p in &pos {
1559            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1560        }
1561    }
1562
1563    #[test]
1564    fn test_drl_3d_with_seed() {
1565        let mut g = Graph::with_vertices(3);
1566        g.add_edge(0, 1).unwrap();
1567        g.add_edge(1, 2).unwrap();
1568        let seed = vec![[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [50.0, 86.6, 10.0]];
1569        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1570        let pos = layout_drl_3d(&g, Some(&seed), &opts, None).unwrap();
1571        assert_eq!(pos.len(), 3);
1572        for p in &pos {
1573            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1574        }
1575    }
1576
1577    #[test]
1578    fn test_drl_3d_seed_wrong_length() {
1579        let g = Graph::with_vertices(3);
1580        let seed = vec![[0.0, 0.0, 0.0]];
1581        let opts = DrlOptions::default();
1582        assert!(layout_drl_3d(&g, Some(&seed), &opts, None).is_err());
1583    }
1584
1585    #[test]
1586    fn test_drl_3d_weighted() {
1587        let mut g = Graph::with_vertices(4);
1588        g.add_edge(0, 1).unwrap();
1589        g.add_edge(1, 2).unwrap();
1590        g.add_edge(2, 3).unwrap();
1591        let weights = vec![1.0, 2.0, 0.5];
1592        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1593        let pos = layout_drl_3d(&g, None, &opts, Some(&weights)).unwrap();
1594        assert_eq!(pos.len(), 4);
1595        for p in &pos {
1596            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1597        }
1598    }
1599
1600    #[test]
1601    fn test_drl_3d_bad_weights() {
1602        let mut g = Graph::with_vertices(3);
1603        g.add_edge(0, 1).unwrap();
1604        g.add_edge(1, 2).unwrap();
1605        let opts = DrlOptions::default();
1606        let bad_w = vec![1.0, -1.0];
1607        assert!(layout_drl_3d(&g, None, &opts, Some(&bad_w)).is_err());
1608    }
1609
1610    #[test]
1611    fn test_drl_3d_deterministic() {
1612        let mut g = Graph::with_vertices(5);
1613        g.add_edge(0, 1).unwrap();
1614        g.add_edge(1, 2).unwrap();
1615        g.add_edge(2, 3).unwrap();
1616        g.add_edge(3, 4).unwrap();
1617        g.add_edge(4, 0).unwrap();
1618        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1619        let pos1 = layout_drl_3d(&g, None, &opts, None).unwrap();
1620        let pos2 = layout_drl_3d(&g, None, &opts, None).unwrap();
1621        for i in 0..5 {
1622            assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-6);
1623            assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-6);
1624            assert!((pos1[i][2] - pos2[i][2]).abs() < 1e-6);
1625        }
1626    }
1627
1628    #[test]
1629    fn test_drl_3d_all_templates() {
1630        let mut g = Graph::with_vertices(4);
1631        g.add_edge(0, 1).unwrap();
1632        g.add_edge(1, 2).unwrap();
1633        g.add_edge(2, 3).unwrap();
1634        g.add_edge(3, 0).unwrap();
1635
1636        for templ in [
1637            DrlTemplate::Default,
1638            DrlTemplate::Coarsen,
1639            DrlTemplate::Coarsest,
1640            DrlTemplate::Refine,
1641            DrlTemplate::Final,
1642        ] {
1643            let opts = DrlOptions::from_template(templ);
1644            let pos = layout_drl_3d(&g, None, &opts, None).unwrap();
1645            assert_eq!(pos.len(), 4);
1646            for p in &pos {
1647                assert!(
1648                    p[0].is_finite() && p[1].is_finite() && p[2].is_finite(),
1649                    "Non-finite position with template {templ:?}"
1650                );
1651            }
1652        }
1653    }
1654
1655    #[test]
1656    fn test_drl_3d_disconnected() {
1657        let mut g = Graph::with_vertices(6);
1658        g.add_edge(0, 1).unwrap();
1659        g.add_edge(1, 2).unwrap();
1660        g.add_edge(3, 4).unwrap();
1661        g.add_edge(4, 5).unwrap();
1662        let opts = DrlOptions::from_template(DrlTemplate::Refine);
1663        let pos = layout_drl_3d(&g, None, &opts, None).unwrap();
1664        assert_eq!(pos.len(), 6);
1665        for p in &pos {
1666            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
1667        }
1668    }
1669}