Skip to main content

rust_igraph/algorithms/layout/
merge_dla.rs

1//! Layout merging via Diffusion Limited Aggregation (ALGO-LO-014).
2//!
3//! Merges multiple 2D component layouts into a single combined layout
4//! by covering each with a bounding circle and placing them using DLA
5//! random walks so they do not overlap.
6//!
7//! Reference: igraph C `src/layout/merge_dla.c` + `merge_grid.c`.
8
9use crate::core::IgraphResult;
10
11/// Merge multiple 2D layouts into a single combined layout using DLA placement.
12///
13/// Each component layout is covered by a bounding circle. The largest
14/// component is placed at the origin, then subsequent components are
15/// placed using a DLA (Diffusion Limited Aggregation) random walk that
16/// finds a position touching an already-placed component.
17///
18/// # Arguments
19///
20/// * `layouts` — slice of component layouts, each a `Vec<[f64; 2]>`.
21///
22/// Returns a single merged layout containing all vertices from all
23/// components in order.
24///
25/// # Examples
26///
27/// ```
28/// use rust_igraph::layout_merge_dla;
29///
30/// let layout1 = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 0.5]];
31/// let layout2 = vec![[0.0, 0.0], [1.0, 1.0]];
32/// let merged = layout_merge_dla(&[&layout1, &layout2]).unwrap();
33/// assert_eq!(merged.len(), 5);
34/// assert!(merged.iter().all(|p| p[0].is_finite() && p[1].is_finite()));
35/// ```
36pub fn layout_merge_dla(layouts: &[&[[f64; 2]]]) -> IgraphResult<Vec<[f64; 2]>> {
37    let n_components = layouts.len();
38
39    if n_components == 0 {
40        return Ok(Vec::new());
41    }
42    if n_components == 1 {
43        return Ok(layouts[0].to_vec());
44    }
45
46    // Compute bounding spheres for each component
47    let mut centers_x = vec![0.0_f64; n_components];
48    let mut centers_y = vec![0.0_f64; n_components];
49    let mut radii = vec![0.0_f64; n_components];
50    let mut placement_radii = vec![0.0_f64; n_components];
51    let mut total_nodes = 0usize;
52
53    let mut area = 0.0_f64;
54    for (i, layout) in layouts.iter().enumerate() {
55        let size = layout.len();
56        total_nodes += size;
57
58        if size == 0 {
59            continue;
60        }
61
62        // Compute bounding sphere
63        let (cx, cy, r) = bounding_circle(layout);
64        centers_x[i] = cx;
65        centers_y[i] = cy;
66        radii[i] = r;
67
68        // Placement radius scales with component size
69        placement_radii[i] = (size as f64).powf(0.75);
70        area += placement_radii[i] * placement_radii[i];
71    }
72
73    // Sort components by size (largest first)
74    let mut order: Vec<usize> = (0..n_components).collect();
75    order.sort_unstable_by(|&a, &b| layouts[b].len().cmp(&layouts[a].len()));
76
77    // Create merge grid
78    let grid_extent = (5.0 * area).sqrt();
79    let mut grid = MergeGrid::new(
80        -grid_extent,
81        grid_extent,
82        200,
83        -grid_extent,
84        grid_extent,
85        200,
86    );
87
88    // Place components by DLA
89    let mut placed_x = vec![0.0_f64; n_components];
90    let mut placed_y = vec![0.0_f64; n_components];
91
92    // Place the largest at origin
93    let first = order[0];
94    placed_x[first] = 0.0;
95    placed_y[first] = 0.0;
96    grid.place_sphere(0.0, 0.0, placement_radii[first], first);
97
98    let mut rng = SplitMix64::new(42);
99    let startr = grid_extent;
100    let killr = grid_extent + 5.0;
101
102    // Place remaining via DLA random walk
103    for &comp_idx in order.iter().skip(1) {
104        let r = placement_radii[comp_idx];
105        if r <= 0.0 {
106            placed_x[comp_idx] = 0.0;
107            placed_y[comp_idx] = 0.0;
108            continue;
109        }
110
111        let (px, py) = dla_walk(&grid, r, 0.0, 0.0, startr, killr, &mut rng);
112        placed_x[comp_idx] = px;
113        placed_y[comp_idx] = py;
114        grid.place_sphere(px, py, r, comp_idx);
115    }
116
117    // Build the result: transform each component's vertices
118    let mut result = Vec::with_capacity(total_nodes);
119    for (i, layout) in layouts.iter().enumerate() {
120        let ox = placed_x[i];
121        let oy = placed_y[i];
122        let nr = radii[i];
123        let scale = if nr > 0.0 {
124            placement_radii[i] / nr
125        } else {
126            1.0
127        };
128        let cx = centers_x[i];
129        let cy = centers_y[i];
130
131        for p in *layout {
132            result.push([scale * (p[0] - cx) + ox, scale * (p[1] - cy) + oy]);
133        }
134    }
135
136    Ok(result)
137}
138
139fn bounding_circle(points: &[[f64; 2]]) -> (f64, f64, f64) {
140    if points.is_empty() {
141        return (0.0, 0.0, 0.0);
142    }
143    if points.len() == 1 {
144        return (points[0][0], points[0][1], 0.0);
145    }
146
147    let mut xmin = points[0][0];
148    let mut xmax = points[0][0];
149    let mut ymin = points[0][1];
150    let mut ymax = points[0][1];
151
152    for p in points.iter().skip(1) {
153        if p[0] < xmin {
154            xmin = p[0];
155        }
156        if p[0] > xmax {
157            xmax = p[0];
158        }
159        if p[1] < ymin {
160            ymin = p[1];
161        }
162        if p[1] > ymax {
163            ymax = p[1];
164        }
165    }
166
167    let cx = (xmin + xmax) / 2.0;
168    let cy = (ymin + ymax) / 2.0;
169    let dx = xmax - xmin;
170    let dy = ymax - ymin;
171    let r = (dx * dx + dy * dy).sqrt() / 2.0;
172
173    (cx, cy, r)
174}
175
176fn dla_walk(
177    grid: &MergeGrid,
178    r: f64,
179    cx: f64,
180    cy: f64,
181    startr: f64,
182    killr: f64,
183    rng: &mut SplitMix64,
184) -> (f64, f64) {
185    let max_attempts = 100_000;
186
187    for _ in 0..max_attempts {
188        // Start particle at random position on circle of radius startr
189        let angle = rng.next_uniform() * std::f64::consts::TAU;
190        let len = rng.next_uniform() * 0.5 * startr + 0.5 * startr;
191        let mut x = cx + len * angle.cos();
192        let mut y = cy + len * angle.sin();
193
194        // Check if starting position already collides
195        if grid.get_sphere(x, y, r) >= 0 {
196            continue;
197        }
198
199        // Random walk until collision or kill
200        let step_size = startr / 100.0;
201        loop {
202            let dist = ((x - cx) * (x - cx) + (y - cy) * (y - cy)).sqrt();
203            if dist >= killr {
204                break;
205            }
206
207            let a = rng.next_uniform() * std::f64::consts::TAU;
208            let l = rng.next_uniform() * step_size;
209            let nx = x + l * a.cos();
210            let ny = y + l * a.sin();
211
212            if grid.get_sphere(nx, ny, r) >= 0 {
213                return (x, y);
214            }
215            x = nx;
216            y = ny;
217        }
218    }
219
220    // Fallback: place at startr away from center
221    let angle = rng.next_uniform() * std::f64::consts::TAU;
222    (cx + startr * angle.cos(), cy + startr * angle.sin())
223}
224
225// ═══════════════════════════════════════════════════════════════════
226// Merge Grid
227// ═══════════════════════════════════════════════════════════════════
228
229struct MergeGrid {
230    minx: f64,
231    maxx: f64,
232    miny: f64,
233    maxy: f64,
234    stepsx: usize,
235    stepsy: usize,
236    deltax: f64,
237    deltay: f64,
238    data: Vec<i32>, // 0 = empty, id+1 = occupied by component id
239}
240
241impl MergeGrid {
242    fn new(minx: f64, maxx: f64, stepsx: usize, miny: f64, maxy: f64, stepsy: usize) -> Self {
243        Self {
244            minx,
245            maxx,
246            miny,
247            maxy,
248            stepsx,
249            stepsy,
250            deltax: (maxx - minx) / stepsx as f64,
251            deltay: (maxy - miny) / stepsy as f64,
252            data: vec![0; stepsx * stepsy],
253        }
254    }
255
256    fn which(&self, xc: f64, yc: f64) -> (usize, usize) {
257        let cx = if xc <= self.minx {
258            0
259        } else if xc >= self.maxx {
260            self.stepsx - 1
261        } else {
262            ((xc - self.minx) / self.deltax) as usize
263        };
264
265        let cy = if yc <= self.miny {
266            0
267        } else if yc >= self.maxy {
268            self.stepsy - 1
269        } else {
270            ((yc - self.miny) / self.deltay) as usize
271        };
272
273        (cx.min(self.stepsx - 1), cy.min(self.stepsy - 1))
274    }
275
276    fn place_sphere(&mut self, x: f64, y: f64, r: f64, id: usize) {
277        let (cx, cy) = self.which(x, y);
278        let val = (id as i32) + 1;
279
280        self.data[cy * self.stepsx + cx] = val;
281
282        // Fill all four quadrants within radius
283        // Quadrant 1: +x, +y
284        let mut i = 0i32;
285        while (cx as i32 + i) < self.stepsx as i32 {
286            let gx = self.minx + (cx as f64 + i as f64) * self.deltax;
287            if Self::grid_dist(x, y, gx, y) >= r {
288                break;
289            }
290            let mut j = 0i32;
291            while (cy as i32 + j) < self.stepsy as i32 {
292                let gy = self.miny + (cy as f64 + j as f64) * self.deltay;
293                if Self::grid_dist(x, y, gx, gy) >= r {
294                    break;
295                }
296                let idx = (cy as i32 + j) as usize * self.stepsx + (cx as i32 + i) as usize;
297                self.data[idx] = val;
298                j += 1;
299            }
300            i += 1;
301        }
302
303        // Quadrant 2: +x, -y
304        i = 0;
305        while (cx as i32 + i) < self.stepsx as i32 {
306            let gx = self.minx + (cx as f64 + i as f64) * self.deltax;
307            if Self::grid_dist(x, y, gx, y) >= r {
308                break;
309            }
310            let mut j = 1i32;
311            while (cy as i32 - j) > 0 {
312                let gy = self.miny + (cy as f64 - j as f64 + 1.0) * self.deltay;
313                if Self::grid_dist(x, y, gx, gy) >= r {
314                    break;
315                }
316                let idx = (cy as i32 - j) as usize * self.stepsx + (cx as i32 + i) as usize;
317                self.data[idx] = val;
318                j += 1;
319            }
320            i += 1;
321        }
322
323        // Quadrant 3: -x, +y
324        i = 1;
325        while (cx as i32 - i) > 0 {
326            let gx = self.minx + (cx as f64 - i as f64 + 1.0) * self.deltax;
327            if Self::grid_dist(x, y, gx, y) >= r {
328                break;
329            }
330            let mut j = 0i32;
331            while (cy as i32 + j) < self.stepsy as i32 {
332                let gy = self.miny + (cy as f64 + j as f64) * self.deltay;
333                if Self::grid_dist(x, y, gx, gy) >= r {
334                    break;
335                }
336                let idx = (cy as i32 + j) as usize * self.stepsx + (cx as i32 - i) as usize;
337                self.data[idx] = val;
338                j += 1;
339            }
340            i += 1;
341        }
342
343        // Quadrant 4: -x, -y
344        i = 1;
345        while (cx as i32 - i) > 0 {
346            let gx = self.minx + (cx as f64 - i as f64 + 1.0) * self.deltax;
347            if Self::grid_dist(x, y, gx, y) >= r {
348                break;
349            }
350            let mut j = 1i32;
351            while (cy as i32 - j) > 0 {
352                let gy = self.miny + (cy as f64 - j as f64 + 1.0) * self.deltay;
353                if Self::grid_dist(x, y, gx, gy) >= r {
354                    break;
355                }
356                let idx = (cy as i32 - j) as usize * self.stepsx + (cx as i32 - i) as usize;
357                self.data[idx] = val;
358                j += 1;
359            }
360            i += 1;
361        }
362    }
363
364    fn get_sphere(&self, x: f64, y: f64, r: f64) -> i32 {
365        if x - r <= self.minx || x + r >= self.maxx || y - r <= self.miny || y + r >= self.maxy {
366            return -1;
367        }
368
369        let (cx, cy) = self.which(x, y);
370        let ret = self.data[cy * self.stepsx + cx] - 1;
371        if ret >= 0 {
372            return ret;
373        }
374
375        // Check four quadrants
376        if let Some(id) = self.check_quadrant(x, y, r, cx, cy, 1, 1) {
377            return id;
378        }
379        if let Some(id) = self.check_quadrant(x, y, r, cx, cy, 1, -1) {
380            return id;
381        }
382        if let Some(id) = self.check_quadrant(x, y, r, cx, cy, -1, 1) {
383            return id;
384        }
385        if let Some(id) = self.check_quadrant(x, y, r, cx, cy, -1, -1) {
386            return id;
387        }
388
389        -1
390    }
391
392    fn check_quadrant(
393        &self,
394        x: f64,
395        y: f64,
396        r: f64,
397        cx: usize,
398        cy: usize,
399        dx: i32,
400        dy: i32,
401    ) -> Option<i32> {
402        let i_start: i32 = i32::from(dx <= 0);
403        let j_start: i32 = i32::from(dy <= 0);
404
405        let mut i = i_start;
406        loop {
407            let gxi = cx as i32 + i * dx;
408            if gxi < 0 || gxi >= self.stepsx as i32 {
409                break;
410            }
411            let gx = self.minx + gxi as f64 * self.deltax;
412            if Self::grid_dist(x, y, gx, y) >= r {
413                break;
414            }
415
416            let mut j = j_start;
417            loop {
418                let gyj = cy as i32 + j * dy;
419                if gyj < 0 || gyj >= self.stepsy as i32 {
420                    break;
421                }
422                let gy = self.miny + gyj as f64 * self.deltay;
423                if Self::grid_dist(x, y, gx, gy) >= r {
424                    break;
425                }
426
427                let idx = gyj as usize * self.stepsx + gxi as usize;
428                let val = self.data[idx] - 1;
429                if val >= 0 {
430                    return Some(val);
431                }
432                j += 1;
433            }
434            i += 1;
435        }
436        None
437    }
438
439    fn grid_dist(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
440        let dx = x1 - x2;
441        let dy = y1 - y2;
442        (dx * dx + dy * dy).sqrt()
443    }
444}
445
446// ═══════════════════════════════════════════════════════════════════
447// Internal RNG
448// ═══════════════════════════════════════════════════════════════════
449
450struct SplitMix64 {
451    state: u64,
452}
453
454impl SplitMix64 {
455    fn new(seed: u64) -> Self {
456        Self { state: seed }
457    }
458
459    fn next_u64(&mut self) -> u64 {
460        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
461        let mut z = self.state;
462        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
463        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
464        z ^ (z >> 31)
465    }
466
467    fn next_uniform(&mut self) -> f64 {
468        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
469    }
470}
471
472// ═══════════════════════════════════════════════════════════════════
473// Tests
474// ═══════════════════════════════════════════════════════════════════
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn test_merge_empty() {
482        let result = layout_merge_dla(&[]).unwrap();
483        assert!(result.is_empty());
484    }
485
486    #[test]
487    fn test_merge_single() {
488        let layout = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
489        let result = layout_merge_dla(&[&layout]).unwrap();
490        assert_eq!(result.len(), 3);
491    }
492
493    #[test]
494    fn test_merge_two_components() {
495        let l1 = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
496        let l2 = vec![[0.0, 0.0], [1.0, 1.0]];
497        let result = layout_merge_dla(&[&l1, &l2]).unwrap();
498        assert_eq!(result.len(), 5);
499        for p in &result {
500            assert!(p[0].is_finite() && p[1].is_finite());
501        }
502    }
503
504    #[test]
505    fn test_merge_three_components() {
506        let l1 = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]];
507        let l2 = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
508        let l3 = vec![[0.0, 0.0], [0.5, 0.5]];
509        let result = layout_merge_dla(&[&l1, &l2, &l3]).unwrap();
510        assert_eq!(result.len(), 9);
511        for p in &result {
512            assert!(p[0].is_finite() && p[1].is_finite());
513        }
514    }
515
516    #[test]
517    fn test_merge_single_vertex_components() {
518        let l1 = vec![[0.0, 0.0]];
519        let l2 = vec![[1.0, 1.0]];
520        let l3 = vec![[2.0, 2.0]];
521        let result = layout_merge_dla(&[&l1, &l2, &l3]).unwrap();
522        assert_eq!(result.len(), 3);
523    }
524
525    #[test]
526    fn test_merge_empty_component() {
527        let l1: Vec<[f64; 2]> = vec![];
528        let l2 = vec![[0.0, 0.0], [1.0, 0.0]];
529        let result = layout_merge_dla(&[&l1[..], &l2]).unwrap();
530        assert_eq!(result.len(), 2);
531    }
532
533    #[test]
534    fn test_merge_deterministic() {
535        let l1 = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
536        let l2 = vec![[0.0, 0.0], [1.0, 1.0]];
537        let r1 = layout_merge_dla(&[&l1, &l2]).unwrap();
538        let r2 = layout_merge_dla(&[&l1, &l2]).unwrap();
539        for i in 0..5 {
540            assert!((r1[i][0] - r2[i][0]).abs() < 1e-10);
541            assert!((r1[i][1] - r2[i][1]).abs() < 1e-10);
542        }
543    }
544
545    #[test]
546    fn test_merge_no_overlap() {
547        let l1 = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0], [0.5, -1.0]];
548        let l2 = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]];
549        let result = layout_merge_dla(&[&l1, &l2]).unwrap();
550        // Component centers should be separated
551        let cx1 = (result[0][0] + result[1][0] + result[2][0] + result[3][0]) / 4.0;
552        let cy1 = (result[0][1] + result[1][1] + result[2][1] + result[3][1]) / 4.0;
553        let cx2 = (result[4][0] + result[5][0] + result[6][0]) / 3.0;
554        let cy2 = (result[4][1] + result[5][1] + result[6][1]) / 3.0;
555        let dist = ((cx1 - cx2) * (cx1 - cx2) + (cy1 - cy2) * (cy1 - cy2)).sqrt();
556        assert!(dist > 0.1);
557    }
558}