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