forked from drandyhaas/KiCadRoutingTools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplane_blocker_detection.py
More file actions
679 lines (596 loc) · 30.6 KB
/
Copy pathplane_blocker_detection.py
File metadata and controls
679 lines (596 loc) · 30.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
"""
Blocker detection for copper plane via placement.
Identifies which nets are blocking via placement or routing,
and provides rip-up functionality to remove blockers.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Dict, Tuple, Optional, Set
import numpy as np
import os
from kicad_parser import PCBData, Pad
from routing_config import GridRouteConfig, GridCoord
from routing_utils import iter_pad_blocked_cells
from bresenham_utils import walk_line
from pcb_modification import remove_net_from_pcb_data, restore_net_to_pcb_data
from obstacle_cache import ViaPlacementObstacleData, precompute_via_placement_obstacles
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'rust_router'))
from grid_router import GridObstacleMap
def _re_add_pad_obstacles_for_net(
pcb_data: PCBData,
net_id: int,
config: GridRouteConfig,
routing_obstacles_cache: Dict[str, GridObstacleMap]
):
"""
Re-add pad obstacles for a net after its segments were removed.
When segments are ripped, the segment obstacles are removed from the cache.
But pad obstacles that overlap with segment obstacles are also removed.
This function re-adds the pad obstacles since pads still exist after rip-up.
Uses the same rectangular-with-rounded-corners pattern as build_routing_obstacle_map.
"""
coord = GridCoord(config.grid_step)
pads = pcb_data.pads_by_net.get(net_id, [])
for pad in pads:
for layer, obstacles in routing_obstacles_cache.items():
# Check if pad is on this layer
if layer not in pad.layers and "*.Cu" not in pad.layers:
continue
gx, gy = coord.to_grid(pad.global_x, pad.global_y)
half_width = pad.size_x / 2
half_height = pad.size_y / 2
margin = config.track_width / 2 + config.clearance
# Corner radius based on pad shape
if pad.shape in ('circle', 'oval'):
corner_radius = min(half_width, half_height)
elif pad.shape == 'roundrect':
corner_radius = pad.roundrect_rratio * min(pad.size_x, pad.size_y)
else:
corner_radius = 0
for cell_gx, cell_gy in iter_pad_blocked_cells(gx, gy, half_width, half_height, margin, config.grid_step, corner_radius,
off_x=pad.global_x - gx * coord.grid_step,
off_y=pad.global_y - gy * coord.grid_step,
rotation_deg=pad.rect_rotation):
obstacles.add_blocked_cell(cell_gx, cell_gy, 0) # layer_idx=0 for single-layer maps
def _point_to_segment_dist_sq(px: float, py: float, x1: float, y1: float, x2: float, y2: float) -> float:
"""Calculate squared distance from point (px, py) to line segment (x1,y1)-(x2,y2)."""
dx = x2 - x1
dy = y2 - y1
length_sq = dx * dx + dy * dy
if length_sq < 1e-10:
# Degenerate segment (point)
return (px - x1) ** 2 + (py - y1) ** 2
# Project point onto line, clamped to segment
t = max(0, min(1, ((px - x1) * dx + (py - y1) * dy) / length_sq))
proj_x = x1 + t * dx
proj_y = y1 + t * dy
return (px - proj_x) ** 2 + (py - proj_y) ** 2
def find_via_position_blocker(
via_x: float,
via_y: float,
pcb_data: PCBData,
config: GridRouteConfig,
exclude_net_id: int,
protected_net_ids: Optional[Set[int]] = None,
quiet: bool = False
) -> Optional[int]:
"""
Find the net that is blocking via placement at a specific position.
Checks segments and vias from other nets to find what's blocking
the given via position.
Args:
via_x, via_y: Position where via placement is blocked
pcb_data: PCB data with all segments/vias
config: Routing configuration
exclude_net_id: Net ID to exclude (the target net)
protected_net_ids: Set of net IDs that should never be identified as blockers
Returns:
Net ID of the closest non-protected blocker, or None if no blocker found
"""
best_blocker = None
best_dist_sq = float('inf')
best_protected_blocker = None
best_protected_dist_sq = float('inf')
protected = protected_net_ids or set()
# Check segments
for seg in pcb_data.segments:
if seg.net_id == exclude_net_id:
continue
dist_sq = _point_to_segment_dist_sq(via_x, via_y, seg.start_x, seg.start_y, seg.end_x, seg.end_y)
clearance_needed = config.via_size / 2 + seg.width / 2 + config.clearance
if dist_sq < clearance_needed ** 2:
if seg.net_id in protected:
if dist_sq < best_protected_dist_sq:
best_protected_dist_sq = dist_sq
best_protected_blocker = seg.net_id
elif dist_sq < best_dist_sq:
best_dist_sq = dist_sq
best_blocker = seg.net_id
# Check vias
for via in pcb_data.vias:
if via.net_id == exclude_net_id:
continue
dx = via.x - via_x
dy = via.y - via_y
dist_sq = dx * dx + dy * dy
clearance_needed = config.via_size / 2 + via.size / 2 + config.clearance
if dist_sq < clearance_needed ** 2:
if via.net_id in protected:
if dist_sq < best_protected_dist_sq:
best_protected_dist_sq = dist_sq
best_protected_blocker = via.net_id
elif dist_sq < best_dist_sq:
best_dist_sq = dist_sq
best_blocker = via.net_id
# Report protected blocker if it's closer than any non-protected blocker
if (not quiet and best_protected_blocker is not None
and best_protected_dist_sq < best_dist_sq):
net = pcb_data.nets.get(best_protected_blocker)
blocker_name = net.name if net else f"net_{best_protected_blocker}"
print(f"blocked by {blocker_name} (protected, cannot rip)...", end=" ")
return best_blocker
def find_route_blocker_from_frontier(
blocked_cells: List[Tuple[int, int, int]],
pcb_data: PCBData,
config: GridRouteConfig,
exclude_net_id: int,
protected_net_ids: Optional[Set[int]] = None
) -> Optional[int]:
"""
Find the net most responsible for blocking a route based on frontier data.
Uses the blocked_cells from route_with_frontier to identify which net's
segments/vias are blocking the most cells on the search frontier.
Args:
blocked_cells: List of (gx, gy, layer) cells from route_with_frontier
pcb_data: PCB data with all segments/vias
config: Routing configuration
exclude_net_id: Net ID to exclude (the target net)
protected_net_ids: Set of net IDs that should never be identified as blockers
Returns:
Net ID of the top non-protected blocker, or None if no blocker found
"""
if not blocked_cells:
return None
coord = GridCoord(config.grid_step)
blocked_set = set(blocked_cells)
protected = protected_net_ids or set()
# Frontier bbox prefilter (#225): every membership test below is on layer 0,
# so only layer-0 frontier cells can ever match, and a segment/via contributes
# only if its clearance-expanded window overlaps that frontier. The router's
# blocked frontier is local, but this function used to walk EVERY board track
# and via (with a per-cell window scan), costing ~27s of rip-up handling on
# daisho. Skipping copper whose expanded bbox can't reach the frontier is an
# exact necessary-condition cull -- the counts (and the chosen blocker) are
# unchanged. No layer-0 frontier cell -> nothing can match (was: empty tally).
_l0x = [gx for (gx, gy, l) in blocked_set if l == 0]
if not _l0x:
return None
_l0y = [gy for (gx, gy, l) in blocked_set if l == 0]
bb_min_x, bb_max_x = min(_l0x), max(_l0x)
bb_min_y, bb_max_y = min(_l0y), max(_l0y)
# Count how many blocked cells each net is responsible for (including protected)
net_block_count: Dict[int, int] = {}
# Check segments
# expansion = existing_track_half + clearance + routing_track_half. Size the
# existing-track half from the segment's ACTUAL width (a wide/diff-pair trace
# is responsible for more blocked cells than the default width implies), so
# the rip-up heuristic attributes blockage to the right net. Mirrors #172.
routing_half = config.track_width / 2
for seg in pcb_data.segments:
if seg.net_id == exclude_net_id:
continue
# Get layer index (assume single layer routing, layer 0)
layer_idx = 0
seg_half = (seg.width if getattr(seg, 'width', 0) and seg.width > 0
else config.track_width) / 2
expansion_grid = max(1, coord.to_grid_dist(seg_half + config.clearance + routing_half))
# Trace along segment and check for blocked cells
gx1, gy1 = coord.to_grid(seg.start_x, seg.start_y)
gx2, gy2 = coord.to_grid(seg.end_x, seg.end_y)
# Skip segments whose expanded window can't reach the frontier bbox.
if (max(gx1, gx2) + expansion_grid < bb_min_x or
min(gx1, gx2) - expansion_grid > bb_max_x or
max(gy1, gy2) + expansion_grid < bb_min_y or
min(gy1, gy2) - expansion_grid > bb_max_y):
continue
count = 0
for gx, gy in walk_line(gx1, gy1, gx2, gy2):
# Check expansion around this point
for ex in range(-expansion_grid, expansion_grid + 1):
for ey in range(-expansion_grid, expansion_grid + 1):
cell = (gx + ex, gy + ey, layer_idx)
if cell in blocked_set:
count += 1
if count > 0:
net_block_count[seg.net_id] = net_block_count.get(seg.net_id, 0) + count
# Check vias - size the keep-out from each via's ACTUAL size (a fanout
# via-in-pad is larger than config.via_size), same rationale as the segments.
for via in pcb_data.vias:
if via.net_id == exclude_net_id:
continue
via_r = (via.size if getattr(via, 'size', 0) and via.size > 0
else config.via_size) / 2
via_expansion_grid = max(1, coord.to_grid_dist(via_r + config.track_width / 2 + config.clearance))
gx, gy = coord.to_grid(via.x, via.y)
# Skip vias whose expanded window can't reach the frontier bbox.
if (gx + via_expansion_grid < bb_min_x or gx - via_expansion_grid > bb_max_x or
gy + via_expansion_grid < bb_min_y or gy - via_expansion_grid > bb_max_y):
continue
count = 0
for ex in range(-via_expansion_grid, via_expansion_grid + 1):
for ey in range(-via_expansion_grid, via_expansion_grid + 1):
if ex * ex + ey * ey <= via_expansion_grid * via_expansion_grid:
# Vias block all layers, but for single-layer routing check layer 0
cell = (gx + ex, gy + ey, 0)
if cell in blocked_set:
count += 1
if count > 0:
net_block_count[via.net_id] = net_block_count.get(via.net_id, 0) + count
if not net_block_count:
return None
# Find top blocker overall (for diagnostics) and top non-protected blocker (for ripping)
top_blocker = max(net_block_count.keys(), key=lambda k: net_block_count[k])
# Check if top blocker is protected
if top_blocker in protected:
net = pcb_data.nets.get(top_blocker)
blocker_name = net.name if net else f"net_{top_blocker}"
print(f"blocked by {blocker_name} (protected, cannot rip)...", end=" ")
# Find top non-protected blocker
non_protected = {k: v for k, v in net_block_count.items() if k not in protected}
if non_protected:
return max(non_protected.keys(), key=lambda k: non_protected[k])
return None
return top_blocker
@dataclass
class ViaPlacementResult:
"""Result of via placement attempt with potential rip-up."""
success: bool
via_pos: Optional[Tuple[float, float]]
segments: List[Dict]
ripped_net_ids: List[int] # Nets that were ripped up to achieve success
via_at_pad_center: bool
# Ripped nets restored (fully or partially) by the collision-checked
# settle: (net_id, kept_segments, kept_vias, dropped_piece_count).
# The caller must strip the net's INPUT copper from the output and emit
# the kept pieces as new copper -- the writer works from the input file,
# so a pcb_data-only restore would resurrect the dropped pieces (#319
# board==file contract).
restored_nets: List = None
def _point_to_segment_dist_sq(px: float, py: float,
ax: float, ay: float,
bx: float, by: float) -> float:
"""Squared distance from point (px,py) to segment (ax,ay)-(bx,by)."""
dx, dy = bx - ax, by - ay
seg_len_sq = dx * dx + dy * dy
if seg_len_sq <= 1e-12:
return (px - ax) ** 2 + (py - ay) ** 2
t = ((px - ax) * dx + (py - ay) * dy) / seg_len_sq
t = max(0.0, min(1.0, t))
cx, cy = ax + t * dx, ay + t * dy
return (px - cx) ** 2 + (py - cy) ** 2
def _restored_piece_collides(seg: Optional[Dict], via: Optional[Dict],
plane_vias: List[Dict], plane_segments: List[Dict],
via_size: float, clearance: float) -> bool:
"""Issue #88.1: return True if a to-be-restored segment or via would
overlap newly-placed plane copper (plane vias/segments placed this run).
Plane stitching vias span all layers, so a restored via or any restored
segment (on any layer) that comes within (via_size/2 + own_radius +
clearance) of a plane via center is a short. Restored segments are also
tested against plane segments on the same layer. Collision-free pieces are
restored verbatim; colliding pieces are left ripped (so the net falls into
the ripped-nets set to be re-routed rather than shorted onto plane copper).
"""
via_r = via_size / 2.0
if via is not None:
# Restored via vs plane vias (via-via, all layers).
vr = via.get('size', via_size) / 2.0
thresh = via_r + vr + clearance
thresh_sq = thresh * thresh
for pv in plane_vias:
if (via['x'] - pv['x']) ** 2 + (via['y'] - pv['y']) ** 2 < thresh_sq:
return True
# Restored via vs plane SEGMENTS (the barrel spans all layers, so a
# segment on any layer counts). The original #88.1 call sites only
# restored against stitching VIAS so this was never needed; the #329
# tap restore also checks against the tap's new TRACE copper --
# without this, a restored via sat on 13 fresh +3V3 trace segments
# (glasgow /IO_Banks/DA2, 0707b wave set1).
for ps in plane_segments:
ps_half_w = ps.get('width', 0.2) / 2.0
v_thresh = vr + ps_half_w + clearance
if _point_to_segment_dist_sq(via['x'], via['y'],
ps['start'][0], ps['start'][1],
ps['end'][0], ps['end'][1]) < v_thresh * v_thresh:
return True
return False
if seg is not None:
sx0, sy0 = seg['start'][0], seg['start'][1]
sx1, sy1 = seg['end'][0], seg['end'][1]
half_w = seg.get('width', 0.2) / 2.0
# Restored segment vs plane vias (via copper, all layers).
thresh = via_r + half_w + clearance
thresh_sq = thresh * thresh
for pv in plane_vias:
if _point_to_segment_dist_sq(pv['x'], pv['y'], sx0, sy0, sx1, sy1) < thresh_sq:
return True
# Restored segment vs plane segments on the same layer (seg-seg).
seg_layer = seg.get('layer')
for ps in plane_segments:
if ps.get('layer') != seg_layer:
continue
ps_half_w = ps.get('width', 0.2) / 2.0
s_thresh = half_w + ps_half_w + clearance
s_thresh_sq = s_thresh * s_thresh
# Endpoint sampling covers the short axis-overlap case; an X
# CROSSING has all four endpoints far apart, so also test true
# intersection (#329 restore checks restored signal traces
# against the tap's new trace copper, where crossings happen).
px0, py0 = ps['start'][0], ps['start'][1]
px1, py1 = ps['end'][0], ps['end'][1]
if (_point_to_segment_dist_sq(px0, py0, sx0, sy0, sx1, sy1) < s_thresh_sq or
_point_to_segment_dist_sq(px1, py1, sx0, sy0, sx1, sy1) < s_thresh_sq or
_point_to_segment_dist_sq(sx0, sy0, px0, py0, px1, py1) < s_thresh_sq or
_point_to_segment_dist_sq(sx1, sy1, px0, py0, px1, py1) < s_thresh_sq):
return True
from geometry_utils import segments_intersect_2d
if segments_intersect_2d((sx0, sy0), (sx1, sy1), (px0, py0), (px1, py1)):
return True
return False
return False
def _settle_ripped_nets(ripped_data, pcb_data, via_obstacle_cache, obstacles,
routing_obstacles_cache, plane_vias, plane_segments,
via_size, clearance, new_via_pos=None, new_segments=None,
config=None, all_copper_layers=None):
"""Collision-checked restore of ripped nets (#88.1, extended to the tap
SUCCESS path -- the route_planes side of the #329 fix): restore every
ripped net whose copper overlaps neither the plane copper placed this run
nor the NEW tap copper (via + trace); leave colliding nets ripped and
return them for the caller's honest reroute handoff. Shipping every
successful-tap rip destroyed 11 routed signal nets on ottercast (the
entire remaining zero-copper cluster: AP_CK32KO, +1V1, EPHY_TX_N/P, ...).
"""
check_vias = list(plane_vias or [])
check_segs = list(plane_segments or [])
if new_via_pos is not None:
check_vias.append({'x': new_via_pos[0], 'y': new_via_pos[1], 'size': via_size})
# A net ripped a SECOND time is checked against lists that still hold its
# own previously-emitted copper at identical coordinates -- without
# filtering, every piece "collides with itself" and the whole net is
# wrongly left ripped (+1V1 on ottercast: C98.2 restore, R82.2 re-rip).
def _universe(nid):
return ([v for v in check_vias if v.get('net_id') != nid],
[c for c in check_segs if c.get('net_id') != nid])
for ns in (new_segments or []):
if isinstance(ns, dict):
check_segs.append({'start': tuple(ns['start']), 'end': tuple(ns['end']),
'width': ns.get('width', 0.2), 'layer': ns.get('layer')})
else:
check_segs.append({'start': (ns.start_x, ns.start_y),
'end': (ns.end_x, ns.end_y),
'width': ns.width, 'layer': ns.layer})
still_ripped: List[int] = []
restored: List[Tuple[int, list, list, int]] = []
any_restored = False
partial: List[Tuple[int, int]] = [] # (net_id, dropped_piece_count)
for blocker_id, removed_segs, removed_vias in ripped_data:
u_vias, u_segs = _universe(blocker_id)
keep_segs, keep_vias, dropped = [], [], 0
for rs in removed_segs:
seg_dict = {'start': (rs.start_x, rs.start_y), 'end': (rs.end_x, rs.end_y),
'width': rs.width, 'layer': rs.layer}
if _restored_piece_collides(seg_dict, None, u_vias, u_segs,
via_size, clearance):
dropped += 1
else:
keep_segs.append(rs)
for rv in removed_vias:
via_dict = {'x': rv.x, 'y': rv.y, 'size': rv.size}
if _restored_piece_collides(None, via_dict, u_vias, u_segs,
via_size, clearance):
dropped += 1
else:
keep_vias.append(rv)
if not keep_segs and not keep_vias:
# Nothing restorable: leave fully ripped for the caller's honest
# reroute handoff (net excluded from output + reported).
still_ripped.append(blocker_id)
continue
restore_net_to_pcb_data(pcb_data, keep_segs, keep_vias)
any_restored = True
restored.append((blocker_id, keep_segs, keep_vias, dropped))
if dropped:
partial.append((blocker_id, dropped))
# Re-add the net's obstacle footprint. The cache covers the FULL pre-rip
# net, which is exact for a FULL restore. For a PARTIAL restore, re-adding
# the full cache leaked the DROPPED pieces' keep-out cells -- blocked in
# the maintained map but open in a fresh rebuild (#342: 242 leaked cells
# on the balance test). Recompute the keep-out from the net's now-KEPT
# pcb_data copper (restore_net_to_pcb_data above already put the kept
# pieces back) so the map == fresh rebuild, and REPLACE the cache so a
# later re-rip removes exactly what was added.
if blocker_id in via_obstacle_cache:
cache = via_obstacle_cache[blocker_id]
if dropped and config is not None:
cache = precompute_via_placement_obstacles(
pcb_data, blocker_id, config, all_copper_layers or [])
via_obstacle_cache[blocker_id] = cache
if len(cache.blocked_vias) > 0:
obstacles.add_blocked_vias_batch(cache.blocked_vias)
for layer, cells in cache.blocked_cells_by_layer.items():
if layer in routing_obstacles_cache and len(cells) > 0:
cells_3d = np.column_stack([cells, np.zeros(len(cells), dtype=np.int32)])
routing_obstacles_cache[layer].add_blocked_cells_batch(cells_3d)
if any_restored:
print("(restored) ", end="")
if partial:
names = ', '.join(f"net_{nid}(-{n})" for nid, n in partial)
print(f"(partial restore, dropped colliding pieces: {names}) ", end="")
if still_ripped:
print(f"(left {len(still_ripped)} colliding net(s) ripped) ", end="")
return still_ripped, restored
def try_place_via_with_ripup(
pad: Pad,
pad_layer: str,
net_id: int,
pcb_data: PCBData,
config: GridRouteConfig,
coord: GridCoord,
max_search_radius: float,
max_rip_nets: int,
obstacles: GridObstacleMap,
routing_obstacles: Optional[GridObstacleMap],
via_obstacle_cache: Dict[int, ViaPlacementObstacleData],
routing_obstacles_cache: Dict[str, GridObstacleMap],
all_copper_layers: List[str],
via_blocked: bool, # True if via placement failed, False if routing failed
blocked_cells: Optional[List[Tuple[int, int, int]]] = None, # Frontier from failed route
new_vias: List[Dict] = None, # Previously placed vias to re-block after rebuild
hole_to_hole_clearance: float = 0.2,
via_drill: float = 0.4,
protected_net_ids: Optional[Set[int]] = None, # Nets that should never be ripped up
verbose: bool = False,
find_via_position_fn=None, # Function to find via position
route_via_to_pad_fn=None, # Function to route via to pad
pending_pads: Optional[List[Dict]] = None, # Pads that still need vias (for exclusion zones)
# Issue #88.1: collision-aware restoration. Plane vias/segments placed so
# far this run, plus geometry, so restoration can skip pieces that would
# overlap newly-placed plane copper instead of restoring shorts.
plane_vias: Optional[List[Dict]] = None,
plane_segments: Optional[List[Dict]] = None,
via_size: float = 0.5,
clearance: float = 0.25
) -> ViaPlacementResult:
"""
Try to place a via and route to pad, ripping up blockers as needed.
Called AFTER the fast path already failed. On first iteration, just finds and
rips up the blocker without re-searching (since we already know search failed).
Uses incremental obstacle updates for performance - instead of rebuilding the
entire obstacle map after ripping a net, we just remove that net's cached obstacles.
Args:
find_via_position_fn: Function to find via position (injected to avoid circular import)
route_via_to_pad_fn: Function to route via to pad (injected to avoid circular import)
"""
ripped_net_ids = []
ripped_data = [] # Store (blocker_id, removed_segs, removed_vias) for restoration on failure
failed_route_positions: Set[Tuple[int, int]] = set() # Track positions where routing failed
for attempt in range(max_rip_nets):
if attempt == 0:
# First iteration: skip search, we already know it failed
# Just find the blocker directly
if via_blocked:
blocker = find_via_position_blocker(
pad.global_x, pad.global_y, pcb_data, config, net_id, protected_net_ids
)
else:
# Route was blocked - use frontier data
blocker = find_route_blocker_from_frontier(
blocked_cells or [], pcb_data, config, net_id, protected_net_ids
)
else:
# After rip-up, try again (skip positions near previously failed ones)
if find_via_position_fn is None:
break # Can't continue without find_via_position
via_pos = find_via_position_fn(
pad, obstacles, coord, max_search_radius,
routing_obstacles=routing_obstacles,
config=config,
pad_layer=pad_layer,
net_id=net_id,
verbose=False,
failed_route_positions=failed_route_positions,
pending_pads=pending_pads
)
if via_pos:
via_at_pad_center = (abs(via_pos[0] - pad.global_x) < 0.001 and
abs(via_pos[1] - pad.global_y) < 0.001)
if via_at_pad_center or not pad_layer:
still, restored = _settle_ripped_nets(
ripped_data, pcb_data, via_obstacle_cache, obstacles,
routing_obstacles_cache, plane_vias, plane_segments,
via_size, clearance, new_via_pos=via_pos,
config=config, all_copper_layers=all_copper_layers)
return ViaPlacementResult(
success=True, via_pos=via_pos, segments=[],
ripped_net_ids=still, via_at_pad_center=via_at_pad_center,
restored_nets=restored
)
# Try routing
if route_via_to_pad_fn is None:
break # Can't continue without route_via_to_pad
route_result = route_via_to_pad_fn(
via_pos, pad, pad_layer, net_id,
routing_obstacles, config,
verbose=False, return_blocked_cells=True
)
if route_result.success:
still, restored = _settle_ripped_nets(
ripped_data, pcb_data, via_obstacle_cache, obstacles,
routing_obstacles_cache, plane_vias, plane_segments,
via_size, clearance, new_via_pos=via_pos,
new_segments=route_result.segments,
config=config, all_copper_layers=all_copper_layers)
return ViaPlacementResult(
success=True, via_pos=via_pos, segments=route_result.segments,
ripped_net_ids=still, via_at_pad_center=False,
restored_nets=restored
)
# Routing failed - find blocker from frontier
blocker = find_route_blocker_from_frontier(
route_result.blocked_cells, pcb_data, config, net_id, protected_net_ids
)
else:
# Via placement blocked
blocker = find_via_position_blocker(
pad.global_x, pad.global_y, pcb_data, config, net_id, protected_net_ids
)
if blocker is None:
break
# Compute cache BEFORE ripping (while segments/vias still exist in pcb_data)
if blocker not in via_obstacle_cache:
via_obstacle_cache[blocker] = precompute_via_placement_obstacles(
pcb_data, blocker, config, all_copper_layers
)
# Rip up blocker
blocker_net = pcb_data.nets.get(blocker)
blocker_name = blocker_net.name if blocker_net else f"net_{blocker}"
print(f"ripping {blocker_name}...", end=" ")
removed_segs, removed_vias = remove_net_from_pcb_data(pcb_data, blocker)
ripped_net_ids.append(blocker)
ripped_data.append((blocker, removed_segs, removed_vias))
# Incremental update: remove ripped net's obstacles from existing maps
cache = via_obstacle_cache[blocker]
# Remove from via obstacle map
if len(cache.blocked_vias) > 0:
obstacles.remove_blocked_vias_batch(cache.blocked_vias)
# Remove from routing obstacle maps
for layer, cells in cache.blocked_cells_by_layer.items():
if layer in routing_obstacles_cache and len(cells) > 0:
cells_3d = np.column_stack([cells, np.zeros(len(cells), dtype=np.int32)])
routing_obstacles_cache[layer].remove_blocked_cells_batch(cells_3d)
# Re-add pad obstacles for the ripped net (pads still exist, only segments removed)
_re_add_pad_obstacles_for_net(pcb_data, blocker, config, routing_obstacles_cache)
# Update routing_obstacles reference if it's for the current layer
if pad_layer and pad_layer in routing_obstacles_cache:
routing_obstacles = routing_obstacles_cache[pad_layer]
# Add ripped net's pads to pending_pads for exclusion zones in subsequent attempts
if pending_pads is not None:
ripped_pads = pcb_data.pads_by_net.get(blocker, [])
for rp in ripped_pads:
pending_pads.append({'pad': rp, 'needs_via': True})
# Failed - restore ripped nets, collision-aware (issue #88.1); shared
# with the success paths via _settle_ripped_nets.
still_ripped, _restored = _settle_ripped_nets(
ripped_data, pcb_data, via_obstacle_cache, obstacles,
routing_obstacles_cache, plane_vias, plane_segments,
via_size, clearance, config=config, all_copper_layers=all_copper_layers)
return ViaPlacementResult(
success=False, via_pos=None, segments=[],
# Nets we restored are gone from this list; nets we left ripped (because
# restoring them would short onto plane copper) are returned so the
# caller marks them ripped and re-routes them.
ripped_net_ids=still_ripped,
via_at_pad_center=False,
restored_nets=_restored
)