-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathreroute_loop.py
More file actions
933 lines (856 loc) · 60.4 KB
/
Copy pathreroute_loop.py
File metadata and controls
933 lines (856 loc) · 60.4 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
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
"""
Reroute loop for ripped-up nets.
This module contains the unified reroute loop that handles all nets ripped
during diff pair or single-ended routing, extracted from route.py.
"""
from __future__ import annotations
import time
from typing import List, Tuple
from routing_state import (RoutingState, record_net_event, record_rip_ancestry,
record_pair_rip_ancestry, rip_exclude_set,
diff_pair_rip_exclude)
from obstacle_costs import compute_track_proximity_for_net
from connectivity import get_net_endpoints, calculate_stub_length, get_multipoint_net_pads
from net_queries import calculate_route_length
from pcb_modification import add_route_to_pcb_data
from single_ended_routing import route_net_with_obstacles, route_multipoint_main, route_multipoint_taps
from diff_pair_routing import (route_diff_pair_with_obstacles, get_diff_pair_endpoints,
_route_direct_coupled_middle)
from blocking_analysis import analyze_frontier_blocking, print_blocking_analysis, filter_rippable_blockers, invalidate_obstacle_cache
from rip_up_reroute import rip_up_net, restore_net
from polarity_swap import apply_polarity_swap, get_canonical_net_id
from layer_swap_fallback import try_fallback_layer_swap, add_own_stubs_as_obstacles_for_diff_pair
from routing_context import (
build_single_ended_obstacles, build_diff_pair_obstacles,
build_diff_pair_leg_obstacles,
record_single_ended_success, record_diff_pair_success,
prepare_obstacles_inplace, restore_obstacles_inplace
)
from obstacle_cache import update_net_obstacles_after_routing
from terminal_colors import RED, GREEN, RESET
def run_reroute_loop(
state: RoutingState,
route_index_start: int = 0,
cancel_check=None,
progress_callback=None,
failed_so_far: int = 0,
) -> Tuple[int, int, float, int, int]:
"""
Run the unified reroute loop for all nets ripped during routing.
Args:
state: The routing state object containing all shared state
route_index_start: Starting route index (continues from previous routing phases)
cancel_check: Optional callable returning True if routing should be cancelled
progress_callback: Optional callable(current, total, net_name) for progress updates
failed_so_far: Number of failed routes from previous routing phases
Returns:
Tuple of (successful, failed, total_time, total_iterations, route_index)
"""
# Extract frequently-used state fields as local references
pcb_data = state.pcb_data
config = state.config
routed_net_ids = state.routed_net_ids
routed_net_paths = state.routed_net_paths
routed_results = state.routed_results
diff_pair_by_net_id = state.diff_pair_by_net_id
track_proximity_cache = state.track_proximity_cache
layer_map = state.layer_map
reroute_queue = state.reroute_queue
rip_and_retry_history = state.rip_and_retry_history
rerouted_pairs = state.rerouted_pairs
polarity_swapped_pairs = state.polarity_swapped_pairs
remaining_net_ids = state.remaining_net_ids
results = state.results
pad_swaps = state.pad_swaps
base_obstacles = state.base_obstacles
diff_pair_base_obstacles = state.diff_pair_base_obstacles
diff_pair_extra_clearance = state.diff_pair_extra_clearance
gnd_net_id = state.gnd_net_id
all_unrouted_net_ids = state.all_unrouted_net_ids
total_routes = state.total_routes
enable_layer_switch = state.enable_layer_switch
target_swaps = state.target_swaps
all_swap_vias = state.all_swap_vias
all_segment_modifications = state.all_segment_modifications
# Counters (kept as locals)
successful = 0
failed = 0
total_time = 0.0
total_iterations = 0
route_index = route_index_start
reroute_index = 0
# Cache for obstacle cells - persists across retry iterations for performance
obstacle_cache = {}
# Get the queued_net_ids tracking set from state
queued_net_ids = state.queued_net_ids
# Unified reroute loop - handles all nets ripped during diff pair or single-ended routing
while reroute_index < len(reroute_queue):
# Check for cancellation at start of each reroute iteration
if cancel_check and cancel_check():
print("\nReroute cancelled by user")
break
reroute_item = reroute_queue[reroute_index]
reroute_index += 1
if reroute_item[0] == 'single':
_, ripped_net_name, ripped_net_id = reroute_item
# Skip if already routed
if ripped_net_id in routed_results:
continue
route_index += 1
current_total = total_routes + len(reroute_queue)
total_failed = failed_so_far + failed
failed_str = f" ({total_failed} failed)" if total_failed > 0 else ""
print(f"\n[REROUTE {route_index}/{current_total}{failed_str}] Re-routing ripped net {ripped_net_name}")
print("-" * 40)
# Update progress for reroute
if progress_callback:
msg = f"Reroute: {ripped_net_name}"
if total_failed > 0:
msg += f" ({total_failed} failed)"
progress_callback(route_index, current_total, msg)
# Calculate stub length before routing
stub_length = calculate_stub_length(pcb_data, ripped_net_id)
start_time = time.time()
# Use in-place approach if working obstacles available (saves memory)
reroute_via_cells = None
if state.working_obstacles is not None and state.net_obstacles_cache:
unrouted_stubs, reroute_via_cells = prepare_obstacles_inplace(
state.working_obstacles, pcb_data, config, ripped_net_id,
all_unrouted_net_ids, routed_net_ids, track_proximity_cache, layer_map,
state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions
)
obstacles = state.working_obstacles
else:
obstacles, unrouted_stubs = build_single_ended_obstacles(
base_obstacles, pcb_data, config, routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, ripped_net_id, gnd_net_id, track_proximity_cache, layer_map,
ripped_route_layer_costs=state.ripped_route_layer_costs,
ripped_route_via_positions=state.ripped_route_via_positions
)
# Check for multi-point net (3+ pads, no existing segments since they were ripped)
multipoint_pads = get_multipoint_net_pads(pcb_data, ripped_net_id, config)
if multipoint_pads:
print(f" Multi-point net with {len(multipoint_pads)} pads - routing main + taps")
result = route_multipoint_main(pcb_data, ripped_net_id, config, obstacles, multipoint_pads)
# If Phase 1 succeeded, immediately do Phase 3 (tap routing)
if result and not result.get('failed') and result.get('is_multipoint'):
main_segments_count = len(result['new_segments'])
main_vias_count = len(result.get('new_vias', []))
tap_result = route_multipoint_taps(pcb_data, ripped_net_id, config, obstacles, result)
if tap_result:
result = tap_result # Use combined result
tap_segments = len(result['new_segments']) - main_segments_count
tap_vias = len(result.get('new_vias', [])) - main_vias_count
print(f" Tap routing: {tap_segments} segments, {tap_vias} vias")
# Print red final failure message if there are unconnected pads
final_failed_pads = tap_result.get('failed_pads_info', [])
if final_failed_pads:
net_name = pcb_data.nets[ripped_net_id].name if ripped_net_id in pcb_data.nets else f"Net {ripped_net_id}"
for pad in final_failed_pads:
print(f" {RED}NOT CONNECTED (so far): {net_name} - {pad['component_ref']} pad {pad['pad_number']} at ({pad['x']:.2f}, {pad['y']:.2f}) - may still be recovered; final verdict in the end-of-run summary{RESET}")
# Remove from pending_multipoint_nets since Phase 3 is now complete
if ripped_net_id in state.pending_multipoint_nets:
del state.pending_multipoint_nets[ripped_net_id]
else:
result = route_net_with_obstacles(pcb_data, ripped_net_id, config, obstacles)
elapsed = time.time() - start_time
total_time += elapsed
if result and not result.get('failed'):
routed_length = calculate_route_length(result['new_segments'], result.get('new_vias', []), pcb_data)
route_length = routed_length + stub_length
result['route_length'] = route_length
result['stub_length'] = stub_length
print(f" REROUTE SUCCESS: {len(result['new_segments'])} segments, {len(result.get('new_vias', []))} vias, length={route_length:.2f}mm (stubs={stub_length:.2f}mm) ({elapsed:.2f}s)")
results.append(result)
successful += 1
total_iterations += result['iterations']
record_single_ended_success(
pcb_data, result, ripped_net_id, config,
remaining_net_ids, routed_net_ids, routed_net_paths,
routed_results, track_proximity_cache, layer_map
)
record_net_event(state, ripped_net_id, "reroute_succeeded", {
"context": "reroute_loop",
"segments": len(result['new_segments']),
"vias": len(result.get('new_vias', []))
})
# Allow re-queuing if this net gets ripped again later
queued_net_ids.discard(ripped_net_id)
# Update net obstacles cache with new route, then restore working obstacles
if reroute_via_cells is not None and state.working_obstacles is not None:
update_net_obstacles_after_routing(pcb_data, ripped_net_id, result, config, state.net_obstacles_cache)
restore_obstacles_inplace(state.working_obstacles, ripped_net_id,
state.net_obstacles_cache, reroute_via_cells)
reroute_via_cells = None
else:
# Reroute failed - try rip-up and retry
iterations = result['iterations'] if result else 0
total_iterations += iterations
print(f" REROUTE FAILED: ({elapsed:.2f}s) - attempting rip-up and retry...")
# Restore working obstacles before rip-up/retry (in-place approach)
if reroute_via_cells is not None and state.working_obstacles is not None:
restore_obstacles_inplace(state.working_obstacles, ripped_net_id,
state.net_obstacles_cache, reroute_via_cells)
reroute_via_cells = None
reroute_succeeded = False
ripped_items = []
if routed_net_paths and result:
fwd_cells = result.pop('blocked_cells_forward', [])
bwd_cells = result.pop('blocked_cells_backward', [])
blocked_cells = list(set(fwd_cells + bwd_cells))
del fwd_cells, bwd_cells # Free memory immediately
if blocked_cells:
# Get source/target coordinates for blocking analysis
reroute_sources, reroute_targets, _ = get_net_endpoints(pcb_data, ripped_net_id, config)
reroute_source_xy = None
reroute_target_xy = None
if reroute_sources:
reroute_source_xy = (reroute_sources[0][3], reroute_sources[0][4])
if reroute_targets:
reroute_target_xy = (reroute_targets[0][3], reroute_targets[0][4])
blockers = analyze_frontier_blocking(
blocked_cells, pcb_data, config, routed_net_paths,
exclude_net_ids=rip_exclude_set(state, ripped_net_id),
target_xy=reroute_target_xy,
source_xy=reroute_source_xy,
obstacle_cache=obstacle_cache
)
print_blocking_analysis(blockers)
# Filter to only rippable blockers
rippable_blockers, seen_canonical_ids = filter_rippable_blockers(
blockers, routed_results, diff_pair_by_net_id, get_canonical_net_id
)
ripped_canonical_ids = set()
last_retry_blocked_cells = blocked_cells
for N in range(1, config.max_rip_up_count + 1):
if N > 1 and last_retry_blocked_cells:
print(f" Re-analyzing {len(last_retry_blocked_cells)} blocked cells from N={N-1} retry:")
fresh_blockers = analyze_frontier_blocking(
last_retry_blocked_cells, pcb_data, config, routed_net_paths,
exclude_net_ids=rip_exclude_set(state, ripped_net_id),
target_xy=reroute_target_xy,
source_xy=reroute_source_xy,
obstacle_cache=obstacle_cache
)
print_blocking_analysis(fresh_blockers, prefix=" ")
next_blocker = None
for b in fresh_blockers:
if b.net_id in routed_results:
canonical = get_canonical_net_id(b.net_id, diff_pair_by_net_id)
if canonical not in ripped_canonical_ids:
next_blocker = b
break
if next_blocker is None:
print(f" No additional rippable blockers from retry analysis")
break
next_canonical = get_canonical_net_id(next_blocker.net_id, diff_pair_by_net_id)
if next_canonical not in seen_canonical_ids:
seen_canonical_ids.add(next_canonical)
rippable_blockers.append(next_blocker)
for idx, b in enumerate(rippable_blockers):
if get_canonical_net_id(b.net_id, diff_pair_by_net_id) == next_canonical:
if idx != N - 1 and N - 1 < len(rippable_blockers):
rippable_blockers[idx], rippable_blockers[N-1] = rippable_blockers[N-1], rippable_blockers[idx]
break
if N > len(rippable_blockers):
break
blocker_canonicals = frozenset(
get_canonical_net_id(rippable_blockers[i].net_id, diff_pair_by_net_id)
for i in range(N)
)
if (ripped_net_id, blocker_canonicals) in rip_and_retry_history:
continue
rip_successful = True
new_ripped_this_level = []
if N == 1:
blocker = rippable_blockers[0]
if blocker.net_id in diff_pair_by_net_id:
ripped_pair_name_tmp, _ = diff_pair_by_net_id[blocker.net_id]
print(f" Ripping up diff pair {ripped_pair_name_tmp} to retry reroute...")
else:
print(f" Ripping up {blocker.net_name} to retry reroute...")
else:
blocker = rippable_blockers[N-1]
if blocker.net_id in diff_pair_by_net_id:
ripped_pair_name_tmp, _ = diff_pair_by_net_id[blocker.net_id]
print(f" Extending to N={N}: ripping diff pair {ripped_pair_name_tmp}...")
else:
print(f" Extending to N={N}: ripping {blocker.net_name}...")
for i in range(len(ripped_items), N):
blocker = rippable_blockers[i]
if blocker.net_id not in routed_results:
continue
saved_result_tmp, ripped_ids, was_in_results = rip_up_net(
blocker.net_id, pcb_data, routed_net_ids, routed_net_paths,
routed_results, diff_pair_by_net_id, remaining_net_ids,
results, config, track_proximity_cache,
state.working_obstacles, state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions,
layer_map
)
if saved_result_tmp is None:
rip_successful = False
break
ripped_items.append((blocker.net_id, saved_result_tmp, ripped_ids, was_in_results))
new_ripped_this_level.append((blocker.net_id, saved_result_tmp, ripped_ids, was_in_results))
ripped_canonical_ids.add(get_canonical_net_id(blocker.net_id, diff_pair_by_net_id))
# Invalidate obstacle cache for ripped nets and record rip events
for rid in ripped_ids:
invalidate_obstacle_cache(obstacle_cache, rid)
record_rip_ancestry(state, ripped_net_id, rid)
record_net_event(state, rid, "ripped_by", {
"ripping_net_id": ripped_net_id,
"ripping_net_name": ripped_net_name,
"reason": f"reroute_loop rip-up retry N={N}",
"N": N
})
if was_in_results:
successful -= 1
if not rip_successful:
for net_id_tmp, saved_result_tmp, ripped_ids, was_in_results in reversed(new_ripped_this_level):
restore_net(net_id_tmp, saved_result_tmp, ripped_ids, was_in_results,
pcb_data, routed_net_ids, routed_net_paths,
routed_results, diff_pair_by_net_id, remaining_net_ids,
results, config, track_proximity_cache, layer_map,
state.working_obstacles, state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions, refused_sink=state.collision_refused_net_ids)
if was_in_results:
successful += 1
ripped_items.pop()
continue
# Prepare obstacles in-place for retry (saves memory)
retry_via_cells = None
if state.working_obstacles is not None and state.net_obstacles_cache:
_, retry_via_cells = prepare_obstacles_inplace(
state.working_obstacles, pcb_data, config, ripped_net_id,
all_unrouted_net_ids, routed_net_ids, track_proximity_cache, layer_map,
state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions
)
retry_obstacles = state.working_obstacles
else:
retry_obstacles, _ = build_single_ended_obstacles(
base_obstacles, pcb_data, config, routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, ripped_net_id, gnd_net_id, track_proximity_cache, layer_map,
ripped_route_layer_costs=state.ripped_route_layer_costs,
ripped_route_via_positions=state.ripped_route_via_positions
)
# Check for multi-point net in retry as well
retry_multipoint_pads = get_multipoint_net_pads(pcb_data, ripped_net_id, config)
if retry_multipoint_pads:
retry_result = route_multipoint_main(pcb_data, ripped_net_id, config, retry_obstacles, retry_multipoint_pads)
if retry_result and not retry_result.get('failed') and retry_result.get('is_multipoint'):
tap_result = route_multipoint_taps(pcb_data, ripped_net_id, config, retry_obstacles, retry_result)
if tap_result:
retry_result = tap_result
# Print red final failure message if there are unconnected pads
final_failed_pads = tap_result.get('failed_pads_info', [])
if final_failed_pads:
net_name = pcb_data.nets[ripped_net_id].name if ripped_net_id in pcb_data.nets else f"Net {ripped_net_id}"
for pad in final_failed_pads:
print(f" {RED}NOT CONNECTED (so far): {net_name} - {pad['component_ref']} pad {pad['pad_number']} at ({pad['x']:.2f}, {pad['y']:.2f}) - may still be recovered; final verdict in the end-of-run summary{RESET}")
# Remove from pending_multipoint_nets since Phase 3 is now complete
if ripped_net_id in state.pending_multipoint_nets:
del state.pending_multipoint_nets[ripped_net_id]
else:
retry_result = route_net_with_obstacles(pcb_data, ripped_net_id, config, retry_obstacles)
if retry_result and not retry_result.get('failed'):
routed_length = calculate_route_length(retry_result['new_segments'], retry_result.get('new_vias', []), pcb_data)
route_length = routed_length + stub_length
retry_result['route_length'] = route_length
retry_result['stub_length'] = stub_length
print(f" REROUTE RETRY SUCCESS (N={N}): {len(retry_result['new_segments'])} segments, {len(retry_result.get('new_vias', []))} vias, length={route_length:.2f}mm (stubs={stub_length:.2f}mm)")
results.append(retry_result)
successful += 1
total_iterations += retry_result['iterations']
add_route_to_pcb_data(pcb_data, retry_result, debug_lines=config.debug_lines)
if ripped_net_id in remaining_net_ids:
remaining_net_ids.remove(ripped_net_id)
routed_net_ids.append(ripped_net_id)
routed_results[ripped_net_id] = retry_result
record_net_event(state, ripped_net_id, "reroute_succeeded", {
"context": "reroute_loop",
"N": N,
"segments": len(retry_result['new_segments']),
"vias": len(retry_result.get('new_vias', []))
})
if retry_result.get('path'):
routed_net_paths[ripped_net_id] = retry_result['path']
track_proximity_cache[ripped_net_id] = compute_track_proximity_for_net(pcb_data, ripped_net_id, config, layer_map)
# Allow re-queuing if this net gets ripped again later
queued_net_ids.discard(ripped_net_id)
# Update net obstacles cache with new route, then restore working obstacles
if retry_via_cells is not None and state.working_obstacles is not None:
update_net_obstacles_after_routing(pcb_data, ripped_net_id, retry_result, config, state.net_obstacles_cache)
restore_obstacles_inplace(state.working_obstacles, ripped_net_id,
state.net_obstacles_cache, retry_via_cells)
retry_via_cells = None
# Invalidate blocking analysis cache since we added segments
invalidate_obstacle_cache(obstacle_cache, ripped_net_id)
# Queue ripped nets and add to history
rip_and_retry_history.add((ripped_net_id, blocker_canonicals))
for net_id_tmp, saved_result_tmp, ripped_ids, was_in_results in ripped_items:
if was_in_results:
successful -= 1
if net_id_tmp in diff_pair_by_net_id:
ripped_pair_name_tmp, ripped_pair_tmp = diff_pair_by_net_id[net_id_tmp]
canonical_id = ripped_pair_tmp.p_net_id
if canonical_id not in queued_net_ids:
reroute_queue.append(('diff_pair', ripped_pair_name_tmp, ripped_pair_tmp))
queued_net_ids.add(canonical_id)
else:
if net_id_tmp not in queued_net_ids:
net = pcb_data.nets.get(net_id_tmp)
net_name_tmp = net.name if net else f"Net {net_id_tmp}"
reroute_queue.append(('single', net_name_tmp, net_id_tmp))
queued_net_ids.add(net_id_tmp)
reroute_succeeded = True
break
else:
print(f" REROUTE RETRY FAILED (N={N})")
# Restore working obstacles after retry failure (in-place)
if retry_via_cells is not None and state.working_obstacles is not None:
restore_obstacles_inplace(state.working_obstacles, ripped_net_id,
state.net_obstacles_cache, retry_via_cells)
retry_via_cells = None
if retry_result:
retry_fwd = retry_result.pop('blocked_cells_forward', [])
retry_bwd = retry_result.pop('blocked_cells_backward', [])
last_retry_blocked_cells = list(set(retry_fwd + retry_bwd))
del retry_fwd, retry_bwd # Free memory immediately
if last_retry_blocked_cells:
print(f" Retry had {len(last_retry_blocked_cells)} blocked cells")
else:
print(f" No blocked cells from retry to analyze")
# If all N levels failed, restore all ripped nets
if not reroute_succeeded and ripped_items:
print(f" {RED}All rip-up attempts failed: Restoring {len(ripped_items)} net(s){RESET}")
for net_id_tmp, saved_result_tmp, ripped_ids, was_in_results in reversed(ripped_items):
restore_net(net_id_tmp, saved_result_tmp, ripped_ids, was_in_results,
pcb_data, routed_net_ids, routed_net_paths,
routed_results, diff_pair_by_net_id, remaining_net_ids,
results, config, track_proximity_cache, layer_map,
state.working_obstacles, state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions, refused_sink=state.collision_refused_net_ids)
if was_in_results:
successful += 1
if not reroute_succeeded:
if not ripped_items:
print(f" {RED}ROUTE FAILED - no rippable blockers found{RESET}")
from routing_diagnostics import static_boxin_hint
hint = static_boxin_hint(result, config, pcb_data)
if hint:
print(f" {hint}")
# Remove from pending_multipoint_nets to prevent Phase 3 from
# trying to route taps for a net with no main route.
if ripped_net_id in state.pending_multipoint_nets:
del state.pending_multipoint_nets[ripped_net_id]
failed += 1
elif reroute_item[0] == 'diff_pair':
# Handle diff pairs that were ripped during single-ended routing
_, ripped_pair_name, ripped_pair = reroute_item
# Skip if already routed
if ripped_pair.p_net_id in routed_results and ripped_pair.n_net_id in routed_results:
continue
route_index += 1
current_total = total_routes + len(reroute_queue)
total_failed = failed_so_far + failed
failed_str = f" ({total_failed} failed)" if total_failed > 0 else ""
print(f"\n[REROUTE {route_index}/{current_total}{failed_str}] Re-routing ripped diff pair {ripped_pair_name}")
print("-" * 40)
# Update progress for diff pair reroute
if progress_callback:
msg = f"Reroute: {ripped_pair_name}"
if total_failed > 0:
msg += f" ({total_failed} failed)"
progress_callback(route_index, current_total, msg)
start_time = time.time()
obstacles, unrouted_stubs = build_diff_pair_obstacles(
diff_pair_base_obstacles, pcb_data, config, routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, ripped_pair.p_net_id, ripped_pair.n_net_id, gnd_net_id,
track_proximity_cache, layer_map, diff_pair_extra_clearance,
add_own_stubs_func=add_own_stubs_as_obstacles_for_diff_pair,
ripped_route_layer_costs=state.ripped_route_layer_costs,
ripped_route_via_positions=state.ripped_route_via_positions
)
# Get source/target coordinates for blocking analysis
reroute_sources, reroute_targets, _ = get_diff_pair_endpoints(pcb_data, ripped_pair.p_net_id, ripped_pair.n_net_id, config)
reroute_source_xy = None
reroute_target_xy = None
if reroute_sources:
src = reroute_sources[0]
reroute_source_xy = ((src[5] + src[7]) / 2, (src[6] + src[8]) / 2)
if reroute_targets:
tgt = reroute_targets[0]
reroute_target_xy = ((tgt[5] + tgt[7]) / 2, (tgt[6] + tgt[8]) / 2)
result = route_diff_pair_with_obstacles(pcb_data, ripped_pair, config, obstacles, base_obstacles, unrouted_stubs)
elapsed = time.time() - start_time
total_time += elapsed
if result and not result.get('failed') and not result.get('probe_blocked'):
print(f" REROUTE SUCCESS: {len(result['new_segments'])} segments, {len(result['new_vias'])} vias ({elapsed:.2f}s)")
results.append(result)
successful += 1
total_iterations += result['iterations']
rerouted_pairs.add(ripped_pair_name)
if result.get('polarity_swap_denied'):
state.polarity_swap_denied_pairs.add(ripped_pair_name)
apply_polarity_swap(pcb_data, result, pad_swaps, ripped_pair_name, polarity_swapped_pairs)
record_diff_pair_success(
pcb_data, result, ripped_pair, ripped_pair_name, config,
remaining_net_ids, routed_net_ids, routed_net_paths, routed_results,
diff_pair_by_net_id, track_proximity_cache, layer_map
)
# Allow re-queuing if this pair gets ripped again later
queued_net_ids.discard(ripped_pair.p_net_id)
queued_net_ids.discard(ripped_pair.n_net_id)
else:
# Reroute failed - try rip-up and retry
iterations = result['iterations'] if result else 0
total_iterations += iterations
print(f" REROUTE FAILED: ({elapsed:.2f}s) - attempting rip-up and retry...")
reroute_succeeded = False
ripped_items = []
if routed_net_paths and result:
# Find blocked cells from the failed route
# Pop to free memory from result dict (cells kept in local vars for fallback)
fwd_cells = result.pop('blocked_cells_forward', [])
bwd_cells = result.pop('blocked_cells_backward', [])
if result.get('probe_blocked'):
blocked_cells = result.pop('blocked_cells', [])
else:
fwd_iters = result.get('iterations_forward', 0)
bwd_iters = result.get('iterations_backward', 0)
if fwd_iters > 0 and (bwd_iters == 0 or fwd_iters <= bwd_iters):
blocked_cells = fwd_cells
else:
blocked_cells = bwd_cells
if not blocked_cells:
print(f" No blocked cells from router - cannot analyze blockers")
if blocked_cells:
blockers = analyze_frontier_blocking(
blocked_cells, pcb_data, config, routed_net_paths,
exclude_net_ids=diff_pair_rip_exclude(
state, ripped_pair.p_net_id, ripped_pair.n_net_id),
extra_clearance=diff_pair_extra_clearance,
target_xy=reroute_target_xy,
source_xy=reroute_source_xy,
obstacle_cache=obstacle_cache
)
print_blocking_analysis(blockers)
# Filter to only rippable blockers and deduplicate by diff pair
rippable_blockers, seen_canonical_ids = filter_rippable_blockers(
blockers, routed_results, diff_pair_by_net_id, get_canonical_net_id
)
current_canonical = ripped_pair.p_net_id
if blockers and not rippable_blockers:
unrippable_names = []
for b in blockers[:3]:
if b.net_id in diff_pair_by_net_id:
unrippable_names.append(diff_pair_by_net_id[b.net_id][0])
else:
unrippable_names.append(b.net_name)
print(f" No rippable blockers (not yet routed): {', '.join(unrippable_names)}")
ripped_canonical_ids = set()
last_retry_blocked_cells = blocked_cells
for N in range(1, config.max_rip_up_count + 1):
if N > 1 and last_retry_blocked_cells:
print(f" Re-analyzing {len(last_retry_blocked_cells)} blocked cells from N={N-1} retry:")
fresh_blockers = analyze_frontier_blocking(
last_retry_blocked_cells, pcb_data, config, routed_net_paths,
exclude_net_ids=diff_pair_rip_exclude(
state, ripped_pair.p_net_id, ripped_pair.n_net_id),
extra_clearance=diff_pair_extra_clearance,
target_xy=reroute_target_xy,
source_xy=reroute_source_xy,
obstacle_cache=obstacle_cache
)
print_blocking_analysis(fresh_blockers, prefix=" ")
next_blocker = None
for b in fresh_blockers:
if b.net_id in routed_results:
canonical = get_canonical_net_id(b.net_id, diff_pair_by_net_id)
if canonical not in ripped_canonical_ids:
next_blocker = b
break
if next_blocker is None:
print(f" No additional rippable blockers from retry analysis")
break
next_canonical = get_canonical_net_id(next_blocker.net_id, diff_pair_by_net_id)
if next_canonical not in seen_canonical_ids:
seen_canonical_ids.add(next_canonical)
rippable_blockers.append(next_blocker)
for idx, b in enumerate(rippable_blockers):
if get_canonical_net_id(b.net_id, diff_pair_by_net_id) == next_canonical:
if idx != N - 1 and N - 1 < len(rippable_blockers):
rippable_blockers[idx], rippable_blockers[N-1] = rippable_blockers[N-1], rippable_blockers[idx]
break
if N > len(rippable_blockers):
break
blocker_canonicals = frozenset(
get_canonical_net_id(rippable_blockers[i].net_id, diff_pair_by_net_id)
for i in range(N)
)
if (current_canonical, blocker_canonicals) in rip_and_retry_history:
blocker_names = []
for i in range(N):
b = rippable_blockers[i]
if b.net_id in diff_pair_by_net_id:
blocker_names.append(diff_pair_by_net_id[b.net_id][0])
else:
blocker_names.append(b.net_name)
print(f" Skipping rip-up (already tried): {', '.join(blocker_names)}")
continue
# Rip up blockers
rip_successful = True
new_ripped_this_level = []
if N == 1:
blocker = rippable_blockers[0]
if blocker.net_id in diff_pair_by_net_id:
ripped_pair_name_tmp, _ = diff_pair_by_net_id[blocker.net_id]
print(f" Ripping up diff pair {ripped_pair_name_tmp} to retry reroute...")
else:
print(f" Ripping up {blocker.net_name} to retry reroute...")
else:
blocker = rippable_blockers[N-1]
if blocker.net_id in diff_pair_by_net_id:
ripped_pair_name_tmp, _ = diff_pair_by_net_id[blocker.net_id]
print(f" Extending to N={N}: ripping diff pair {ripped_pair_name_tmp}...")
else:
print(f" Extending to N={N}: ripping {blocker.net_name}...")
for i in range(len(ripped_items), N):
blocker = rippable_blockers[i]
if blocker.net_id not in routed_results:
continue
saved_result_tmp, ripped_ids, was_in_results = rip_up_net(
blocker.net_id, pcb_data, routed_net_ids, routed_net_paths,
routed_results, diff_pair_by_net_id, remaining_net_ids,
results, config, track_proximity_cache,
state.working_obstacles, state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions,
layer_map
)
if saved_result_tmp is None:
rip_successful = False
break
ripped_items.append((blocker.net_id, saved_result_tmp, ripped_ids, was_in_results))
new_ripped_this_level.append((blocker.net_id, saved_result_tmp, ripped_ids, was_in_results))
ripped_canonical_ids.add(get_canonical_net_id(blocker.net_id, diff_pair_by_net_id))
# Invalidate obstacle cache for ripped nets and record rip events
for rid in ripped_ids:
invalidate_obstacle_cache(obstacle_cache, rid)
# Cycle guard (issue #219): the rerouting pair
# ripped rid, so rid must not rip either half back.
record_pair_rip_ancestry(state, ripped_pair.p_net_id,
ripped_pair.n_net_id, rid)
record_net_event(state, rid, "ripped_by", {
"ripping_net_id": ripped_pair.p_net_id,
"ripping_net_name": ripped_pair_name,
"reason": f"reroute_loop diff-pair rip-up retry N={N}",
"N": N
})
if was_in_results:
successful -= 1
if not rip_successful:
for net_id_tmp, saved_result_tmp, ripped_ids, was_in_results in reversed(new_ripped_this_level):
restore_net(net_id_tmp, saved_result_tmp, ripped_ids, was_in_results,
pcb_data, routed_net_ids, routed_net_paths,
routed_results, diff_pair_by_net_id, remaining_net_ids,
results, config, track_proximity_cache, layer_map,
state.working_obstacles, state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions, refused_sink=state.collision_refused_net_ids)
if was_in_results:
successful += 1
ripped_items.pop()
continue
# Rebuild obstacles and retry
retry_obstacles, unrouted_stubs = build_diff_pair_obstacles(
diff_pair_base_obstacles, pcb_data, config, routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, ripped_pair.p_net_id, ripped_pair.n_net_id, gnd_net_id,
track_proximity_cache, layer_map, diff_pair_extra_clearance,
add_own_stubs_func=add_own_stubs_as_obstacles_for_diff_pair,
ripped_route_layer_costs=state.ripped_route_layer_costs,
ripped_route_via_positions=state.ripped_route_via_positions
)
retry_result = route_diff_pair_with_obstacles(pcb_data, ripped_pair, config, retry_obstacles, base_obstacles, unrouted_stubs)
if retry_result and not retry_result.get('failed') and not retry_result.get('probe_blocked'):
print(f" REROUTE RETRY SUCCESS (N={N}): {len(retry_result['new_segments'])} segments, {len(retry_result['new_vias'])} vias")
results.append(retry_result)
successful += 1
total_iterations += retry_result['iterations']
rerouted_pairs.add(ripped_pair_name)
apply_polarity_swap(pcb_data, retry_result, pad_swaps, ripped_pair_name, polarity_swapped_pairs)
add_route_to_pcb_data(pcb_data, retry_result, debug_lines=config.debug_lines)
if ripped_pair.p_net_id in remaining_net_ids:
remaining_net_ids.remove(ripped_pair.p_net_id)
if ripped_pair.n_net_id in remaining_net_ids:
remaining_net_ids.remove(ripped_pair.n_net_id)
routed_net_ids.append(ripped_pair.p_net_id)
routed_net_ids.append(ripped_pair.n_net_id)
if retry_result.get('p_path'):
routed_net_paths[ripped_pair.p_net_id] = retry_result['p_path']
if retry_result.get('n_path'):
routed_net_paths[ripped_pair.n_net_id] = retry_result['n_path']
routed_results[ripped_pair.p_net_id] = retry_result
routed_results[ripped_pair.n_net_id] = retry_result
diff_pair_by_net_id[ripped_pair.p_net_id] = (ripped_pair_name, ripped_pair)
diff_pair_by_net_id[ripped_pair.n_net_id] = (ripped_pair_name, ripped_pair)
track_proximity_cache[ripped_pair.p_net_id] = compute_track_proximity_for_net(pcb_data, ripped_pair.p_net_id, config, layer_map)
track_proximity_cache[ripped_pair.n_net_id] = compute_track_proximity_for_net(pcb_data, ripped_pair.n_net_id, config, layer_map)
# Allow re-queuing if this pair gets ripped again later
queued_net_ids.discard(ripped_pair.p_net_id)
queued_net_ids.discard(ripped_pair.n_net_id)
# Invalidate blocking analysis cache since we added segments
invalidate_obstacle_cache(obstacle_cache, ripped_pair.p_net_id)
invalidate_obstacle_cache(obstacle_cache, ripped_pair.n_net_id)
# Queue ripped nets and add to history
rip_and_retry_history.add((current_canonical, blocker_canonicals))
for net_id_tmp, saved_result_tmp, ripped_ids, was_in_results in ripped_items:
if was_in_results:
successful -= 1
if net_id_tmp in diff_pair_by_net_id:
ripped_pair_name_tmp, ripped_pair_tmp = diff_pair_by_net_id[net_id_tmp]
canonical_id = ripped_pair_tmp.p_net_id
if canonical_id not in queued_net_ids:
reroute_queue.append(('diff_pair', ripped_pair_name_tmp, ripped_pair_tmp))
queued_net_ids.add(canonical_id)
else:
if net_id_tmp not in queued_net_ids:
net = pcb_data.nets.get(net_id_tmp)
net_name_tmp = net.name if net else f"Net {net_id_tmp}"
reroute_queue.append(('single', net_name_tmp, net_id_tmp))
queued_net_ids.add(net_id_tmp)
reroute_succeeded = True
break
else:
print(f" REROUTE RETRY FAILED (N={N})")
if retry_result:
if retry_result.get('probe_blocked'):
last_retry_blocked_cells = retry_result.pop('blocked_cells', [])
else:
retry_fwd = retry_result.pop('blocked_cells_forward', [])
retry_bwd = retry_result.pop('blocked_cells_backward', [])
last_retry_blocked_cells = list(set(retry_fwd + retry_bwd))
del retry_fwd, retry_bwd # Free memory immediately
if last_retry_blocked_cells:
print(f" Retry had {len(last_retry_blocked_cells)} blocked cells")
else:
print(f" No blocked cells from retry to analyze")
else:
print(f" Retry returned no result (endpoint error?)")
last_retry_blocked_cells = []
# If all N levels failed, restore all ripped nets
if not reroute_succeeded and ripped_items:
print(f" {RED}All rip-up attempts failed: Restoring {len(ripped_items)} net(s){RESET}")
for net_id_tmp, saved_result_tmp, ripped_ids, was_in_results in reversed(ripped_items):
restore_net(net_id_tmp, saved_result_tmp, ripped_ids, was_in_results,
pcb_data, routed_net_ids, routed_net_paths,
routed_results, diff_pair_by_net_id, remaining_net_ids,
results, config, track_proximity_cache, layer_map,
state.working_obstacles, state.net_obstacles_cache,
state.ripped_route_layer_costs, state.ripped_route_via_positions, refused_sink=state.collision_refused_net_ids)
if was_in_results:
successful += 1
# Fallback layer swap for setback failures in reroute (after rip-up fails)
# Note: fwd_cells and bwd_cells already extracted above
if not reroute_succeeded and enable_layer_switch and result:
fwd_iters = result.get('iterations_forward', 0)
bwd_iters = result.get('iterations_backward', 0)
is_setback_failure = (fwd_iters == 0 and bwd_iters == 0 and (fwd_cells or bwd_cells))
if is_setback_failure:
print(f" Trying fallback layer swap for setback failure...")
swap_success, swap_result, _, _ = try_fallback_layer_swap(
pcb_data, ripped_pair, ripped_pair_name, config,
fwd_cells, bwd_cells,
diff_pair_base_obstacles, base_obstacles,
routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, gnd_net_id,
track_proximity_cache, diff_pair_extra_clearance,
all_swap_vias, all_segment_modifications,
None, None, # all_stubs_by_layer, stub_endpoints_by_layer
routed_net_paths, routed_results, diff_pair_by_net_id, layer_map,
target_swaps, results=results, obstacle_cache=obstacle_cache)
if swap_success and swap_result:
print(f" {GREEN}FALLBACK LAYER SWAP SUCCESS{RESET}")
results.append(swap_result)
successful += 1
total_iterations += swap_result['iterations']
rerouted_pairs.add(ripped_pair_name)
apply_polarity_swap(pcb_data, swap_result, pad_swaps, ripped_pair_name, polarity_swapped_pairs)
add_route_to_pcb_data(pcb_data, swap_result, debug_lines=config.debug_lines)
if ripped_pair.p_net_id in remaining_net_ids:
remaining_net_ids.remove(ripped_pair.p_net_id)
if ripped_pair.n_net_id in remaining_net_ids:
remaining_net_ids.remove(ripped_pair.n_net_id)
routed_net_ids.append(ripped_pair.p_net_id)
routed_net_ids.append(ripped_pair.n_net_id)
track_proximity_cache[ripped_pair.p_net_id] = compute_track_proximity_for_net(pcb_data, ripped_pair.p_net_id, config, layer_map)
track_proximity_cache[ripped_pair.n_net_id] = compute_track_proximity_for_net(pcb_data, ripped_pair.n_net_id, config, layer_map)
if swap_result.get('p_path'):
routed_net_paths[ripped_pair.p_net_id] = swap_result['p_path']
if swap_result.get('n_path'):
routed_net_paths[ripped_pair.n_net_id] = swap_result['n_path']
routed_results[ripped_pair.p_net_id] = swap_result
routed_results[ripped_pair.n_net_id] = swap_result
diff_pair_by_net_id[ripped_pair.p_net_id] = (ripped_pair_name, ripped_pair)
diff_pair_by_net_id[ripped_pair.n_net_id] = (ripped_pair_name, ripped_pair)
# Invalidate blocking analysis cache since we added segments
invalidate_obstacle_cache(obstacle_cache, ripped_pair.p_net_id)
invalidate_obstacle_cache(obstacle_cache, ripped_pair.n_net_id)
reroute_succeeded = True
# Last-resort hybrid (issue #244): a rip-reroute casualty whose lane
# is gone and whose standard probe + rip-up both dead-ended can still
# have a coupled-middle + single-ended-leg escape (often to an inner
# layer). The MAIN routing loop already tries this on a first-pass
# failure (diff_pair_loop.py); the reroute path did not, so a pair
# that only fails AFTER being ripped (butterstick /GIGABIT-0/ETH_B,
# the /SYZYGY1.D1 class) failed honestly instead of taking the same
# fallback. Rebuild the obstacle maps fresh -- rip-up/restore mutated
# pcb_data since the reroute's were built. Returns None when it can't
# lay a clean route, so this can't make things worse.
if not reroute_succeeded and config.diff_pair_hybrid_escape:
hyb_obstacles, _ = build_diff_pair_obstacles(
diff_pair_base_obstacles, pcb_data, config, routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, ripped_pair.p_net_id, ripped_pair.n_net_id, gnd_net_id,
track_proximity_cache, layer_map, diff_pair_extra_clearance,
add_own_stubs_func=add_own_stubs_as_obstacles_for_diff_pair,
ripped_route_layer_costs=state.ripped_route_layer_costs,
ripped_route_via_positions=state.ripped_route_via_positions)
# Single-ended leg clearance map. See build_diff_pair_leg_obstacles
# for why this uses base_obstacles + 0 clearance (not the coupled
# diff_pair_base_obstacles / extra clearance the middle map above
# uses) -- previously this site built it inconsistently (#246 review).
leg_obstacles = build_diff_pair_leg_obstacles(
base_obstacles, pcb_data, config, routed_net_ids, remaining_net_ids,
all_unrouted_net_ids, ripped_pair.p_net_id, ripped_pair.n_net_id,
gnd_net_id, track_proximity_cache, layer_map)
hyb = _route_direct_coupled_middle(
pcb_data, ripped_pair, config, hyb_obstacles, config.layers,
leg_obstacles=leg_obstacles)
if hyb and not hyb.get('failed'):
print(f" {GREEN}HYBRID ESCAPE (reroute): direct coupled middle "
f"+ point-to-point terminal legs{RESET}")
results.append(hyb)
successful += 1
total_iterations += hyb.get('iterations', 0)
rerouted_pairs.add(ripped_pair_name)
record_diff_pair_success(
pcb_data, hyb, ripped_pair, ripped_pair_name, config,
remaining_net_ids, routed_net_ids, routed_net_paths, routed_results,
diff_pair_by_net_id, track_proximity_cache, layer_map)
queued_net_ids.discard(ripped_pair.p_net_id)
queued_net_ids.discard(ripped_pair.n_net_id)
invalidate_obstacle_cache(obstacle_cache, ripped_pair.p_net_id)
invalidate_obstacle_cache(obstacle_cache, ripped_pair.n_net_id)
reroute_succeeded = True
if not reroute_succeeded:
if not ripped_items:
print(f" {RED}REROUTE FAILED - could not find route{RESET}")
# Remove from pending_multipoint_nets to prevent Phase 3 from
# trying to route taps for a net with no main route.
if ripped_pair.p_net_id in state.pending_multipoint_nets:
del state.pending_multipoint_nets[ripped_pair.p_net_id]
if ripped_pair.n_net_id in state.pending_multipoint_nets:
del state.pending_multipoint_nets[ripped_pair.n_net_id]
failed += 1
return successful, failed, total_time, total_iterations, route_index