Skip to main content

pedalkernel_layout/
optimize.rs

1//! Phase 6: Aesthetic optimization pass.
2//!
3//! After initial placement and routing, run optimization passes to improve
4//! readability: grid snapping, overlap removal, crossing minimization, and
5//! symmetry balancing.
6
7use crate::types::*;
8
9/// Grid size for snapping (in layout units).
10const GRID_SIZE: f32 = 10.0;
11
12/// Minimum clearance between component bounding boxes.
13const MIN_CLEARANCE: f32 = 30.0;
14
15/// Component bounding box size estimate (half-width/height).
16const COMPONENT_HALF_SIZE: f32 = 15.0;
17
18/// Run all optimization passes on the layout.
19pub fn optimize_layout(layout: &mut Layout) {
20    snap_to_grid(layout);
21    resolve_overlaps(layout);
22    minimize_crossings(layout);
23    resolve_wire_component_overlaps(layout);
24    center_groups(layout);
25}
26
27/// Snap all component positions to the nearest grid point.
28fn snap_to_grid(layout: &mut Layout) {
29    for comp in &mut layout.components {
30        comp.x = (comp.x / GRID_SIZE).round() * GRID_SIZE;
31        comp.y = (comp.y / GRID_SIZE).round() * GRID_SIZE;
32    }
33}
34
35/// Push apart overlapping components.
36fn resolve_overlaps(layout: &mut Layout) {
37    // Simple iterative repulsion: for each pair of components, if they
38    // overlap, push them apart along the axis of least overlap.
39    for _ in 0..10 {
40        let mut any_overlap = false;
41
42        for i in 0..layout.components.len() {
43            for j in (i + 1)..layout.components.len() {
44                let dx = layout.components[j].x - layout.components[i].x;
45                let dy = layout.components[j].y - layout.components[i].y;
46
47                let overlap_x = MIN_CLEARANCE - dx.abs();
48                let overlap_y = MIN_CLEARANCE - dy.abs();
49
50                if overlap_x > 0.0 && overlap_y > 0.0 {
51                    any_overlap = true;
52
53                    if overlap_x < overlap_y {
54                        // Push apart horizontally
55                        let push = overlap_x / 2.0 + 1.0;
56                        if dx >= 0.0 {
57                            layout.components[i].x -= push;
58                            layout.components[j].x += push;
59                        } else {
60                            layout.components[i].x += push;
61                            layout.components[j].x -= push;
62                        }
63                    } else {
64                        // Push apart vertically
65                        let push = overlap_y / 2.0 + 1.0;
66                        if dy >= 0.0 {
67                            layout.components[i].y -= push;
68                            layout.components[j].y += push;
69                        } else {
70                            layout.components[i].y += push;
71                            layout.components[j].y -= push;
72                        }
73                    }
74                }
75            }
76        }
77
78        if !any_overlap {
79            break;
80        }
81    }
82
83    // Re-snap after overlap resolution
84    snap_to_grid(layout);
85}
86
87/// Minimize wire crossings using barycenter heuristic.
88///
89/// For each column, reorder components to minimize crossings with
90/// adjacent columns. This is a simplified version of the Sugiyama
91/// crossing minimization step.
92fn minimize_crossings(layout: &mut Layout) {
93    // Group components by their group name
94    let mut group_members: std::collections::BTreeMap<String, Vec<usize>> =
95        std::collections::BTreeMap::new();
96    for (i, comp) in layout.components.iter().enumerate() {
97        group_members.entry(comp.group.clone()).or_default().push(i);
98    }
99
100    // For each group, sort members by their connected wire positions
101    // (barycenter of connected component positions)
102    for (_group_name, members) in &group_members {
103        if members.len() <= 1 {
104            continue;
105        }
106
107        // Compute barycenter for each member based on connected wires
108        let mut barycenters: Vec<(usize, f32)> = members
109            .iter()
110            .map(|&idx| {
111                let comp_name = &layout.components[idx].name;
112                let connected_y: Vec<f32> = layout
113                    .wires
114                    .iter()
115                    .filter(|w| {
116                        w.points
117                            .first()
118                            .map(|p| {
119                                (p[0] - layout.components[idx].x).abs() < COMPONENT_HALF_SIZE
120                                    && (p[1] - layout.components[idx].y).abs() < COMPONENT_HALF_SIZE
121                            })
122                            .unwrap_or(false)
123                            || w.points
124                                .last()
125                                .map(|p| {
126                                    (p[0] - layout.components[idx].x).abs() < COMPONENT_HALF_SIZE
127                                        && (p[1] - layout.components[idx].y).abs()
128                                            < COMPONENT_HALF_SIZE
129                                })
130                                .unwrap_or(false)
131                    })
132                    .flat_map(|w| w.points.iter().map(|p| p[1]))
133                    .collect();
134
135                let bc = if connected_y.is_empty() {
136                    layout.components[idx].y
137                } else {
138                    connected_y.iter().sum::<f32>() / connected_y.len() as f32
139                };
140
141                let _ = comp_name; // used for filtering above
142                (idx, bc)
143            })
144            .collect();
145
146        barycenters.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
147
148        // Reorder y-positions to match barycenter order
149        let mut y_positions: Vec<f32> = barycenters
150            .iter()
151            .map(|&(idx, _)| layout.components[idx].y)
152            .collect();
153        y_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
154
155        for (rank, &(idx, _)) in barycenters.iter().enumerate() {
156            if rank < y_positions.len() {
157                layout.components[idx].y = y_positions[rank];
158            }
159        }
160    }
161}
162
163/// Resolve wire segments that pass through component bounding boxes by
164/// inserting jog waypoints to route around them.
165fn resolve_wire_component_overlaps(layout: &mut Layout) {
166    // Build component bounding boxes
167    let boxes: Vec<Rect> = layout
168        .components
169        .iter()
170        .map(|c| Rect {
171            x: c.x - COMPONENT_HALF_SIZE,
172            y: c.y - COMPONENT_HALF_SIZE,
173            w: COMPONENT_HALF_SIZE * 2.0,
174            h: COMPONENT_HALF_SIZE * 2.0,
175        })
176        .collect();
177
178    // Collect component centers for endpoint matching
179    let centers: Vec<(f32, f32)> = layout.components.iter().map(|c| (c.x, c.y)).collect();
180
181    for wire in &mut layout.wires {
182        let mut new_points: Vec<[f32; 2]> = Vec::new();
183
184        for seg in wire.points.windows(2) {
185            new_points.push(seg[0]);
186
187            let p0 = Point::new(seg[0][0], seg[0][1]);
188            let p1 = Point::new(seg[1][0], seg[1][1]);
189
190            // Check each component box for overlap with this segment
191            for (ci, bbox) in boxes.iter().enumerate() {
192                // Skip if this segment starts or ends at this component
193                let (cx, cy) = centers[ci];
194                let is_endpoint = ((p0.x - cx).abs() < 1.0 && (p0.y - cy).abs() < 1.0)
195                    || ((p1.x - cx).abs() < 1.0 && (p1.y - cy).abs() < 1.0);
196                if is_endpoint {
197                    continue;
198                }
199
200                // Check if the segment passes through the bounding box
201                if !segment_intersects_rect(p0, p1, bbox) {
202                    continue;
203                }
204
205                // Insert jog waypoints to route around the component
206                let jog_offset = COMPONENT_HALF_SIZE + 5.0;
207                let is_horizontal = (p0.y - p1.y).abs() < 0.5;
208
209                if is_horizontal {
210                    // Horizontal segment: jog vertically around component
211                    let jog_y = if p0.y < cy {
212                        bbox.y - 5.0
213                    } else {
214                        bbox.y + bbox.h + 5.0
215                    };
216                    new_points.push([cx - jog_offset, p0.y]);
217                    new_points.push([cx - jog_offset, jog_y]);
218                    new_points.push([cx + jog_offset, jog_y]);
219                    new_points.push([cx + jog_offset, p1.y]);
220                } else {
221                    // Vertical segment: jog horizontally around component
222                    let jog_x = if p0.x < cx {
223                        bbox.x - 5.0
224                    } else {
225                        bbox.x + bbox.w + 5.0
226                    };
227                    new_points.push([p0.x, cy - jog_offset]);
228                    new_points.push([jog_x, cy - jog_offset]);
229                    new_points.push([jog_x, cy + jog_offset]);
230                    new_points.push([p1.x, cy + jog_offset]);
231                }
232                break; // Only jog around the first overlapping component per segment
233            }
234        }
235
236        // Push the final point
237        if let Some(&last) = wire.points.last() {
238            new_points.push(last);
239        }
240
241        // Deduplicate consecutive identical points
242        new_points.dedup();
243        wire.points = new_points;
244    }
245}
246
247/// Check if a line segment from p0 to p1 intersects a rectangle.
248fn segment_intersects_rect(p0: Point, p1: Point, rect: &Rect) -> bool {
249    let is_horizontal = (p0.y - p1.y).abs() < 0.5;
250    let is_vertical = (p0.x - p1.x).abs() < 0.5;
251
252    if is_horizontal {
253        // Horizontal segment: check if it crosses through the rect vertically
254        let min_x = p0.x.min(p1.x);
255        let max_x = p0.x.max(p1.x);
256        p0.y > rect.y && p0.y < rect.y + rect.h && max_x > rect.x && min_x < rect.x + rect.w
257    } else if is_vertical {
258        // Vertical segment: check if it crosses through the rect horizontally
259        let min_y = p0.y.min(p1.y);
260        let max_y = p0.y.max(p1.y);
261        p0.x > rect.x && p0.x < rect.x + rect.w && max_y > rect.y && min_y < rect.y + rect.h
262    } else {
263        false // Non-orthogonal segments shouldn't exist in Manhattan routing
264    }
265}
266
267/// Center each group's components within the group bounding box.
268fn center_groups(layout: &mut Layout) {
269    // Re-compute group bounds from current component positions
270    for group in &mut layout.groups {
271        let members: Vec<usize> = layout
272            .components
273            .iter()
274            .enumerate()
275            .filter(|(_, c)| c.group == group.name)
276            .map(|(i, _)| i)
277            .collect();
278
279        if members.is_empty() {
280            continue;
281        }
282
283        let min_x = members
284            .iter()
285            .map(|&i| layout.components[i].x)
286            .fold(f32::INFINITY, f32::min);
287        let max_x = members
288            .iter()
289            .map(|&i| layout.components[i].x)
290            .fold(f32::NEG_INFINITY, f32::max);
291        let min_y = members
292            .iter()
293            .map(|&i| layout.components[i].y)
294            .fold(f32::INFINITY, f32::min);
295        let max_y = members
296            .iter()
297            .map(|&i| layout.components[i].y)
298            .fold(f32::NEG_INFINITY, f32::max);
299
300        let padding = COMPONENT_HALF_SIZE;
301        group.bounds = GroupBounds {
302            x: min_x - padding,
303            y: min_y - padding,
304            w: (max_x - min_x) + 2.0 * padding,
305            h: (max_y - min_y) + 2.0 * padding,
306        };
307    }
308}
309
310/// Compute a layout quality score (higher is better).
311///
312/// Used for hill-climbing optimization and testing.
313pub fn layout_score(layout: &Layout) -> f32 {
314    let crossing_penalty = count_wire_crossings(layout) as f32 * -10.0;
315    let alignment_bonus = count_grid_aligned(layout) as f32 * 1.0;
316    let overlap_penalty = count_overlaps(layout) as f32 * -100.0;
317    let symmetry_bonus = measure_vertical_symmetry(layout) * 5.0;
318    let wire_length_penalty = total_wire_length(layout) * -0.01;
319    let signal_flow_bonus = if signal_flows_left_to_right(layout) {
320        50.0
321    } else {
322        0.0
323    };
324
325    crossing_penalty
326        + alignment_bonus
327        + overlap_penalty
328        + symmetry_bonus
329        + wire_length_penalty
330        + signal_flow_bonus
331}
332
333fn count_wire_crossings(layout: &Layout) -> usize {
334    let mut crossings = 0;
335    for i in 0..layout.wires.len() {
336        for j in (i + 1)..layout.wires.len() {
337            // Check each segment pair for crossings
338            let wire_a = &layout.wires[i];
339            let wire_b = &layout.wires[j];
340            for seg_a in wire_a.points.windows(2) {
341                for seg_b in wire_b.points.windows(2) {
342                    if segments_cross(seg_a[0], seg_a[1], seg_b[0], seg_b[1]) {
343                        crossings += 1;
344                    }
345                }
346            }
347        }
348    }
349    crossings
350}
351
352fn segments_cross(a1: [f32; 2], a2: [f32; 2], b1: [f32; 2], b2: [f32; 2]) -> bool {
353    // Simple check for orthogonal segment crossing:
354    // One segment is horizontal and one is vertical
355    let a_horiz = (a1[1] - a2[1]).abs() < 0.5;
356    let b_horiz = (b1[1] - b2[1]).abs() < 0.5;
357
358    if a_horiz == b_horiz {
359        return false; // Parallel segments don't cross (for Manhattan routing)
360    }
361
362    let (h, v) = if a_horiz {
363        ((a1, a2), (b1, b2))
364    } else {
365        ((b1, b2), (a1, a2))
366    };
367
368    let (h_min_x, h_max_x) = minmax(h.0[0], h.1[0]);
369    let h_y = h.0[1];
370    let (v_min_y, v_max_y) = minmax(v.0[1], v.1[1]);
371    let v_x = v.0[0];
372
373    v_x > h_min_x && v_x < h_max_x && h_y > v_min_y && h_y < v_max_y
374}
375
376fn minmax(a: f32, b: f32) -> (f32, f32) {
377    if a <= b {
378        (a, b)
379    } else {
380        (b, a)
381    }
382}
383
384fn count_grid_aligned(layout: &Layout) -> usize {
385    layout
386        .components
387        .iter()
388        .filter(|c| (c.x % GRID_SIZE).abs() < 0.5 && (c.y % GRID_SIZE).abs() < 0.5)
389        .count()
390}
391
392fn count_overlaps(layout: &Layout) -> usize {
393    let mut overlaps = 0;
394    for i in 0..layout.components.len() {
395        for j in (i + 1)..layout.components.len() {
396            let dx = (layout.components[i].x - layout.components[j].x).abs();
397            let dy = (layout.components[i].y - layout.components[j].y).abs();
398            if dx < MIN_CLEARANCE && dy < MIN_CLEARANCE {
399                overlaps += 1;
400            }
401        }
402    }
403    overlaps
404}
405
406fn measure_vertical_symmetry(layout: &Layout) -> f32 {
407    if layout.components.is_empty() {
408        return 0.0;
409    }
410
411    let center_y = layout.bounds.height / 2.0;
412    let deviations: f32 = layout
413        .components
414        .iter()
415        .map(|c| (c.y - center_y).abs())
416        .sum();
417    let max_deviation = layout.components.len() as f32 * center_y;
418
419    if max_deviation > 0.0 {
420        1.0 - (deviations / max_deviation)
421    } else {
422        1.0
423    }
424}
425
426fn total_wire_length(layout: &Layout) -> f32 {
427    layout
428        .wires
429        .iter()
430        .flat_map(|w| {
431            w.points.windows(2).map(|seg| {
432                let dx = seg[1][0] - seg[0][0];
433                let dy = seg[1][1] - seg[0][1];
434                (dx * dx + dy * dy).sqrt()
435            })
436        })
437        .sum()
438}
439
440fn signal_flows_left_to_right(layout: &Layout) -> bool {
441    // Check that signal-path wires generally go left to right
442    let signal_wires: Vec<_> = layout.wires.iter().filter(|w| w.signal_path).collect();
443    if signal_wires.is_empty() {
444        return true;
445    }
446
447    let left_to_right_count = signal_wires
448        .iter()
449        .filter(|w| {
450            if let (Some(first), Some(last)) = (w.points.first(), w.points.last()) {
451                first[0] <= last[0]
452            } else {
453                true
454            }
455        })
456        .count();
457
458    left_to_right_count * 2 >= signal_wires.len()
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn segments_cross_perpendicular() {
467        // Horizontal (0,5)-(10,5) crossing vertical (5,0)-(5,10)
468        assert!(segments_cross(
469            [0.0, 5.0],
470            [10.0, 5.0],
471            [5.0, 0.0],
472            [5.0, 10.0]
473        ));
474    }
475
476    #[test]
477    fn segments_no_cross_parallel() {
478        // Two horizontal segments
479        assert!(!segments_cross(
480            [0.0, 5.0],
481            [10.0, 5.0],
482            [0.0, 7.0],
483            [10.0, 7.0]
484        ));
485    }
486
487    #[test]
488    fn segments_no_cross_miss() {
489        // Horizontal and vertical that don't intersect
490        assert!(!segments_cross(
491            [0.0, 5.0],
492            [3.0, 5.0],
493            [5.0, 0.0],
494            [5.0, 10.0]
495        ));
496    }
497}