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