diff --git a/.gitignore b/.gitignore index b5a6ce5..89d2522 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ wheels/ .venv .hypothesis +*.png + diff --git a/examples/07_iteration_00.png b/examples/07_iteration_00.png deleted file mode 100644 index 428b32e..0000000 Binary files a/examples/07_iteration_00.png and /dev/null differ diff --git a/examples/07_iteration_01.png b/examples/07_iteration_01.png deleted file mode 100644 index f2e2c23..0000000 Binary files a/examples/07_iteration_01.png and /dev/null differ diff --git a/examples/07_iteration_02.png b/examples/07_iteration_02.png deleted file mode 100644 index c6c0b0a..0000000 Binary files a/examples/07_iteration_02.png and /dev/null differ diff --git a/examples/07_iteration_03.png b/examples/07_iteration_03.png deleted file mode 100644 index 55c8ff9..0000000 Binary files a/examples/07_iteration_03.png and /dev/null differ diff --git a/examples/07_iteration_04.png b/examples/07_iteration_04.png deleted file mode 100644 index de8fe6a..0000000 Binary files a/examples/07_iteration_04.png and /dev/null differ diff --git a/examples/07_iteration_05.png b/examples/07_iteration_05.png deleted file mode 100644 index de8fe6a..0000000 Binary files a/examples/07_iteration_05.png and /dev/null differ diff --git a/examples/07_iteration_06.png b/examples/07_iteration_06.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_06.png and /dev/null differ diff --git a/examples/07_iteration_07.png b/examples/07_iteration_07.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_07.png and /dev/null differ diff --git a/examples/07_iteration_08.png b/examples/07_iteration_08.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_08.png and /dev/null differ diff --git a/examples/07_iteration_09.png b/examples/07_iteration_09.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_09.png and /dev/null differ diff --git a/examples/07_iteration_10.png b/examples/07_iteration_10.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_10.png and /dev/null differ diff --git a/examples/07_iteration_11.png b/examples/07_iteration_11.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_11.png and /dev/null differ diff --git a/examples/07_iteration_12.png b/examples/07_iteration_12.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_12.png and /dev/null differ diff --git a/examples/07_iteration_13.png b/examples/07_iteration_13.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_13.png and /dev/null differ diff --git a/examples/07_iteration_14.png b/examples/07_iteration_14.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_14.png and /dev/null differ diff --git a/examples/07_iteration_15.png b/examples/07_iteration_15.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_15.png and /dev/null differ diff --git a/examples/07_iteration_16.png b/examples/07_iteration_16.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_16.png and /dev/null differ diff --git a/examples/07_iteration_17.png b/examples/07_iteration_17.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_17.png and /dev/null differ diff --git a/examples/07_iteration_18.png b/examples/07_iteration_18.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_18.png and /dev/null differ diff --git a/examples/07_iteration_19.png b/examples/07_iteration_19.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_19.png and /dev/null differ diff --git a/examples/07_iteration_20.png b/examples/07_iteration_20.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_20.png and /dev/null differ diff --git a/examples/07_iteration_21.png b/examples/07_iteration_21.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_21.png and /dev/null differ diff --git a/examples/07_iteration_22.png b/examples/07_iteration_22.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_22.png and /dev/null differ diff --git a/examples/07_iteration_23.png b/examples/07_iteration_23.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_23.png and /dev/null differ diff --git a/examples/07_iteration_24.png b/examples/07_iteration_24.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_24.png and /dev/null differ diff --git a/examples/07_iteration_25.png b/examples/07_iteration_25.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_25.png and /dev/null differ diff --git a/examples/07_iteration_26.png b/examples/07_iteration_26.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_26.png and /dev/null differ diff --git a/examples/07_iteration_27.png b/examples/07_iteration_27.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_27.png and /dev/null differ diff --git a/examples/07_iteration_28.png b/examples/07_iteration_28.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_28.png and /dev/null differ diff --git a/examples/07_iteration_29.png b/examples/07_iteration_29.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_29.png and /dev/null differ diff --git a/examples/07_iteration_30.png b/examples/07_iteration_30.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_30.png and /dev/null differ diff --git a/examples/07_iteration_31.png b/examples/07_iteration_31.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_31.png and /dev/null differ diff --git a/examples/07_iteration_32.png b/examples/07_iteration_32.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_32.png and /dev/null differ diff --git a/examples/07_iteration_33.png b/examples/07_iteration_33.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_33.png and /dev/null differ diff --git a/examples/07_iteration_34.png b/examples/07_iteration_34.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_34.png and /dev/null differ diff --git a/examples/07_iteration_35.png b/examples/07_iteration_35.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_35.png and /dev/null differ diff --git a/examples/07_iteration_36.png b/examples/07_iteration_36.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_36.png and /dev/null differ diff --git a/examples/07_iteration_37.png b/examples/07_iteration_37.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_37.png and /dev/null differ diff --git a/examples/07_iteration_38.png b/examples/07_iteration_38.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_38.png and /dev/null differ diff --git a/examples/07_iteration_39.png b/examples/07_iteration_39.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_39.png and /dev/null differ diff --git a/examples/07_iteration_40.png b/examples/07_iteration_40.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_40.png and /dev/null differ diff --git a/examples/07_iteration_41.png b/examples/07_iteration_41.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_41.png and /dev/null differ diff --git a/examples/07_iteration_42.png b/examples/07_iteration_42.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_42.png and /dev/null differ diff --git a/examples/07_iteration_43.png b/examples/07_iteration_43.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_43.png and /dev/null differ diff --git a/examples/07_iteration_44.png b/examples/07_iteration_44.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_44.png and /dev/null differ diff --git a/examples/07_iteration_45.png b/examples/07_iteration_45.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_45.png and /dev/null differ diff --git a/examples/07_iteration_46.png b/examples/07_iteration_46.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_46.png and /dev/null differ diff --git a/examples/07_iteration_47.png b/examples/07_iteration_47.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_47.png and /dev/null differ diff --git a/examples/07_iteration_48.png b/examples/07_iteration_48.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_48.png and /dev/null differ diff --git a/examples/07_iteration_49.png b/examples/07_iteration_49.png deleted file mode 100644 index 2c1e5df..0000000 Binary files a/examples/07_iteration_49.png and /dev/null differ diff --git a/examples/07_large_scale_routing.png b/examples/07_large_scale_routing.png deleted file mode 100644 index 32b048f..0000000 Binary files a/examples/07_large_scale_routing.png and /dev/null differ diff --git a/examples/07_large_scale_routing.py b/examples/07_large_scale_routing.py index 1ff3188..b97cd37 100644 --- a/examples/07_large_scale_routing.py +++ b/examples/07_large_scale_routing.py @@ -6,7 +6,7 @@ from inire.router.astar import AStarRouter from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder -from inire.utils.visualization import plot_routing_results, plot_danger_map, plot_expanded_nodes +from inire.utils.visualization import plot_routing_results, plot_danger_map, plot_expanded_nodes, plot_expansion_density from shapely.geometry import box def main() -> None: @@ -27,10 +27,10 @@ def main() -> None: danger_map = DangerMap(bounds=bounds) danger_map.precompute(obstacles) - evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, unit_length_cost=0.5, bend_penalty=100.0, sbend_penalty=200.0, congestion_penalty=100.0) + evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=2.0, unit_length_cost=0.1, bend_penalty=100.0, sbend_penalty=200.0, congestion_penalty=20.0) - router = AStarRouter(evaluator, node_limit=1000000, snap_size=5.0) - pf = PathFinder(router, evaluator, max_iterations=10, base_congestion_penalty=100.0) + router = AStarRouter(evaluator, node_limit=2000000, snap_size=5.0, bend_radii=[50.0], sbend_radii=[50.0], use_analytical_sbends=False) + pf = PathFinder(router, evaluator, max_iterations=50, base_congestion_penalty=20.0, congestion_multiplier=1.2) # 2. Define Netlist netlist = {} @@ -49,36 +49,115 @@ def main() -> None: net_widths = {nid: 2.0 for nid in netlist} - def iteration_callback(idx, current_results): - print(f" Iteration {idx} finished. Successes: {sum(1 for r in current_results.values() if r.is_valid)}/{len(netlist)}") - print(pf.router.get_metrics_summary()) - pf.router.reset_metrics() - # fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist) - # plot_danger_map(danger_map, ax=ax) - # fig.savefig(f"examples/07_iteration_{idx:02d}.png") - # import matplotlib.pyplot as plt - # plt.close(fig) - # 3. Route print(f"Routing {len(netlist)} nets through 200um bottleneck...") + + iteration_stats = [] + + def iteration_callback(idx, current_results): + successes = sum(1 for r in current_results.values() if r.is_valid) + total_collisions = sum(r.collisions for r in current_results.values()) + total_nodes = pf.router.metrics['nodes_expanded'] + + # Identify Hotspots + hotspots = {} + overlap_matrix = {} # (net_a, net_b) -> count + + for nid, res in current_results.items(): + if res.path: + for comp in res.path: + for poly in comp.geometry: + # Check what it overlaps with + overlaps = engine.dynamic_index.intersection(poly.bounds) + for other_obj_id in overlaps: + other_nid, other_poly = engine.dynamic_geometries[other_obj_id] + if other_nid != nid: + if poly.intersects(other_poly): + # Record hotspot + cx, cy = poly.centroid.x, poly.centroid.y + grid_key = (int(cx/20)*20, int(cy/20)*20) + hotspots[grid_key] = hotspots.get(grid_key, 0) + 1 + + # Record pair + pair = tuple(sorted((nid, other_nid))) + overlap_matrix[pair] = overlap_matrix.get(pair, 0) + 1 + + print(f" Iteration {idx} finished. Successes: {successes}/{len(netlist)}, Collisions: {total_collisions}") + if overlap_matrix: + top_pairs = sorted(overlap_matrix.items(), key=lambda x: x[1], reverse=True)[:3] + print(f" Top Conflicts: {top_pairs}") + if hotspots: + top_hotspots = sorted(hotspots.items(), key=lambda x: x[1], reverse=True)[:3] + print(f" Top Hotspots: {top_hotspots}") + + # Adaptive Greediness: Decay from 2.0 to 1.1 over 25 iterations + new_greedy = max(1.1, 2.0 - ((idx + 1) / 25.0)) + evaluator.greedy_h_weight = new_greedy + print(f" Adaptive Greedy Weight for Next Iteration: {new_greedy:.3f}") + + iteration_stats.append({ + 'Iteration': idx, + 'Success': successes, + 'Congestion': total_collisions, + 'Nodes': total_nodes + }) + + # Save plots only for certain iterations to save time + if idx % 20 == 0 or idx == pf.max_iterations - 1: + # Save a plot of this iteration's result + fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist) + plot_danger_map(danger_map, ax=ax) + + # Overlay failures: show where they stopped + for nid, res in current_results.items(): + if not res.is_valid and res.path: + last_p = res.path[-1].end_port + target_p = netlist[nid][1] + dist = abs(last_p.x - target_p.x) + abs(last_p.y - target_p.y) + ax.scatter(last_p.x, last_p.y, color='red', marker='x', s=100) + ax.text(last_p.x, last_p.y, f" {nid} (rem: {dist:.0f}um)", color='red', fontsize=8) + + fig.savefig(f"examples/07_iteration_{idx:02d}.png") + import matplotlib.pyplot as plt + plt.close(fig) + + # Plot Expansion Density if data is available + if pf.accumulated_expanded_nodes: + fig_d, ax_d = plot_expansion_density(pf.accumulated_expanded_nodes, bounds) + fig_d.savefig(f"examples/07_iteration_{idx:02d}_density.png") + plt.close(fig_d) + + pf.router.reset_metrics() + import cProfile, pstats profiler = cProfile.Profile() profiler.enable() t0 = time.perf_counter() - results = pf.route_all(netlist, net_widths, store_expanded=True, iteration_callback=iteration_callback) + results = pf.route_all(netlist, net_widths, store_expanded=True, iteration_callback=iteration_callback, shuffle_nets=True, seed=42) t1 = time.perf_counter() profiler.disable() + + # ... (rest of the code) stats = pstats.Stats(profiler).sort_stats('tottime') stats.print_stats(20) print(f"Routing took {t1-t0:.4f}s") # 4. Check Results + print("\n--- Iteration Summary ---") + print(f"{'Iter':<5} | {'Success':<8} | {'Congest':<8} | {'Nodes':<10}") + print("-" * 40) + for s in iteration_stats: + print(f"{s['Iteration']:<5} | {s['Success']:<8} | {s['Congestion']:<8} | {s['Nodes']:<10}") + success_count = sum(1 for res in results.values() if res.is_valid) - print(f"Routed {success_count}/{len(netlist)} nets successfully.") + print(f"\nFinal: Routed {success_count}/{len(netlist)} nets successfully.") for nid, res in results.items(): + target_p = netlist[nid][1] if not res.is_valid: - print(f" FAILED: {nid}") + last_p = res.path[-1].end_port if res.path else netlist[nid][0] + dist = abs(last_p.x - target_p.x) + abs(last_p.y - target_p.y) + print(f" FAILED: {nid} (Stopped {dist:.1f}um from target)") else: types = [move.move_type for move in res.path] from collections import Counter diff --git a/inire/geometry/components.py b/inire/geometry/components.py index 0bda4ea..617b067 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -242,88 +242,65 @@ def _get_arc_polygons( def _clip_bbox( - bbox: Polygon, cx: float, cy: float, radius: float, width: float, - clip_margin: float, - arc_poly: Polygon, - t_start: float | None = None, - t_end: float | None = None, + t_start: float, + t_end: float, ) -> Polygon: """ - Clips corners of a bounding box for better collision modeling. + Generates a rotationally invariant bounding polygon for an arc. """ - r_out_cut = radius + width / 2.0 + clip_margin - r_in_cut = radius - width / 2.0 - clip_margin - - # Angular range of the arc - if t_start is not None and t_end is not None: - ts, te = t_start, t_end - if ts > te: - ts, te = te, ts - # Sweep could cross 2pi boundary - sweep = (te - ts) % (2 * numpy.pi) - ts_norm = ts % (2 * numpy.pi) + sweep = abs(t_end - t_start) + if sweep > 2 * numpy.pi: + sweep = sweep % (2 * numpy.pi) + + mid_angle = (t_start + t_end) / 2.0 + # Handle wrap-around for mid_angle + if abs(t_end - t_start) > numpy.pi: + mid_angle += numpy.pi + + r_out = radius + width / 2.0 + r_in = max(0.0, radius - width / 2.0) + + half_sweep = sweep / 2.0 + + # Define vertices in local space (center at 0,0, symmetry axis along +X) + # 1. Start Inner + # 2. Start Outer + # 3. Peak Outer (intersection of tangents at start/end) + # 4. End Outer + # 5. End Inner + # 6. Peak Inner (ensures convexity and inner clipping) + + # Outer tangent intersection point + # Tangent at -hs: x*cos(hs) - y*sin(hs) = r_out + # Tangent at +hs: x*cos(hs) + y*sin(hs) = r_out + # Intersection: y=0, x = r_out / cos(hs) + cos_hs = numpy.cos(half_sweep) + if cos_hs < 1e-3: # Sweep near 180 deg + peak_out_x = r_out * 2.0 # Fallback else: - # Fallback: assume 90 deg based on centroid quadrant - ac = arc_poly.centroid - mid_angle = numpy.arctan2(ac.y - cy, ac.x - cx) - ts_norm = (mid_angle - numpy.pi/4) % (2 * numpy.pi) - sweep = numpy.pi/2 + peak_out_x = r_out / cos_hs - minx, miny, maxx, maxy = bbox.bounds - verts = [ - numpy.array([minx, miny]), - numpy.array([maxx, miny]), - numpy.array([maxx, maxy]), - numpy.array([minx, maxy]) + local_verts = [ + [r_in * numpy.cos(-half_sweep), r_in * numpy.sin(-half_sweep)], + [r_out * numpy.cos(-half_sweep), r_out * numpy.sin(-half_sweep)], + [peak_out_x, 0.0], + [r_out * numpy.cos(half_sweep), r_out * numpy.sin(half_sweep)], + [r_in * numpy.cos(half_sweep), r_in * numpy.sin(half_sweep)], + [r_in, 0.0] ] - new_verts = [] - for p in verts: - dx, dy = p[0] - cx, p[1] - cy - dist = numpy.sqrt(dx**2 + dy**2) - angle = numpy.arctan2(dy, dx) - angle_rel = (angle - ts_norm) % (2 * numpy.pi) - is_in_sweep = angle_rel <= sweep + 1e-6 - - d_line = -1.0 - if is_in_sweep: - # We can clip if outside R_out or inside R_in - if dist > radius + width/2.0 - 1e-6: - d_line = r_out_cut * numpy.sqrt(2) - elif r_in_cut > 1e-3 and dist < radius - width/2.0 + 1e-6: - d_line = r_in_cut - else: - # Corner is outside angular sweep. - if dist > radius + width/2.0 - 1e-6: - d_line = r_out_cut * numpy.sqrt(2) - elif r_in_cut > 1e-3 and dist < radius - width/2.0 + 1e-6: - d_line = r_in_cut - - if d_line > 0: - sx = 1.0 if dx > 0 else -1.0 - sy = 1.0 if dy > 0 else -1.0 - try: - # Intersection of line sx*(x-cx) + sy*(y-cy) = d_line with box edges - p_edge_x = numpy.array([p[0], cy + (d_line - sx * (p[0] - cx)) / sy]) - p_edge_y = numpy.array([cx + (d_line - sy * (p[1] - cy)) / sx, p[1]]) - - # Check if intersection points are on the box boundary - if (minx - 1e-6 <= p_edge_y[0] <= maxx + 1e-6 and - miny - 1e-6 <= p_edge_x[1] <= maxy + 1e-6): - new_verts.append(p_edge_x) - new_verts.append(p_edge_y) - else: - new_verts.append(p) - except ZeroDivisionError: - new_verts.append(p) - else: - new_verts.append(p) - - return Polygon(new_verts).convex_hull + # Rotate and translate to world space + cos_m = numpy.cos(mid_angle) + sin_m = numpy.sin(mid_angle) + rot = numpy.array([[cos_m, -sin_m], [sin_m, cos_m]]) + + world_verts = (numpy.array(local_verts) @ rot.T) + [cx, cy] + + return Polygon(world_verts) def _apply_collision_model( @@ -346,16 +323,16 @@ def _apply_collision_model( if collision_type == "arc": return [arc_poly] - # Bounding box of the high-fidelity arc + if collision_type == "clipped_bbox" and t_start is not None and t_end is not None: + return [_clip_bbox(cx, cy, radius, width, t_start, t_end)] + + # Bounding box of the high-fidelity arc (fallback for bbox or missing angles) minx, miny, maxx, maxy = arc_poly.bounds bbox_poly = box(minx, miny, maxx, maxy) if collision_type == "bbox": return [bbox_poly] - if collision_type == "clipped_bbox": - return [_clip_bbox(bbox_poly, cx, cy, radius, width, clip_margin, arc_poly, t_start, t_end)] - return [arc_poly] @@ -405,8 +382,23 @@ class Bend90: else: ex, ey = ex_raw, ey_raw - # Slightly adjust radius to hit snapped point exactly + # Slightly adjust radius and t_end to hit snapped point exactly actual_radius = numpy.sqrt((ex - cx)**2 + (ey - cy)**2) + + t_end_snapped = numpy.arctan2(ey - cy, ex - cx) + # Ensure directionality and approx 90 degree sweep + if direction == "CCW": + while t_end_snapped <= t_start: + t_end_snapped += 2 * numpy.pi + while t_end_snapped > t_start + numpy.pi: + t_end_snapped -= 2 * numpy.pi + else: + while t_end_snapped >= t_start: + t_end_snapped -= 2 * numpy.pi + while t_end_snapped < t_start - numpy.pi: + t_end_snapped += 2 * numpy.pi + t_end = t_end_snapped + end_port = Port(ex, ey, new_ori) arc_polys = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta) diff --git a/inire/router/astar.py b/inire/router/astar.py index d68f259..8f580bd 100644 --- a/inire/router/astar.py +++ b/inire/router/astar.py @@ -204,13 +204,20 @@ class AStarRouter: if max_reach >= proj_t - 0.01: self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{proj_t}', 'S', (proj_t,), skip_congestion, skip_static=True, snap_to_grid=False) - # B. SBend Jump (if oriented correctly but offset) - if proj_t > 0 and abs(cp.orientation - target.orientation) < 0.1 and abs(perp_t) > 1e-3: - if proj_t < 200.0: # Only lookahead when close - for radius in self.config.sbend_radii: - if abs(perp_t) < 2 * radius: - # Try to generate it - self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{perp_t}R{radius}', 'SB', (perp_t, radius), skip_congestion, snap_to_grid=False) + # B. SBend Jump (Direct to Target) + if self.config.use_analytical_sbends and proj_t > 0 and abs(cp.orientation - target.orientation) < 0.1 and abs(perp_t) > 1e-3: + # Calculate required radius to hit target exactly: R = (dx^2 + dy^2) / (4*|dy|) + req_radius = (proj_t**2 + perp_t**2) / (4.0 * abs(perp_t)) + + min_radius = min(self.config.sbend_radii) if self.config.sbend_radii else 50.0 + + if req_radius >= min_radius: + # We can hit it exactly! + self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB_Direct_R{req_radius:.1f}', 'SB', (perp_t, req_radius), skip_congestion, snap_to_grid=False) + else: + # Required radius is too small. We must use a larger radius and some straight segments. + # A* will handle this through Priority 3 SBends + Priority 2 Straights. + pass # In super sparse mode, we can return here, but A* needs other options for optimality. # return @@ -406,6 +413,10 @@ class AStarRouter: if 'SB' in move_type: penalty = self.config.sbend_penalty elif 'B' in move_type: penalty = self.config.bend_penalty + # Scale penalty by radius (larger radius = smoother = lower penalty) + if move_radius is not None and move_radius > 1e-6: + penalty *= (10.0 / move_radius)**0.5 + move_cost = self.cost_evaluator.evaluate_move( result.geometry, result.end_port, net_width, net_id, start_port=parent_p, length=result.length, @@ -418,9 +429,6 @@ class AStarRouter: self.metrics['pruned_cost'] += 1 return - if 'B' in move_type and move_radius is not None and move_radius > 1e-6: - move_cost *= (10.0 / move_radius)**0.5 - g_cost = parent.g_cost + move_cost if state in closed_set and closed_set[state] <= g_cost + 1e-6: self.metrics['pruned_closed_set'] += 1 diff --git a/inire/router/config.py b/inire/router/config.py index ab37f56..00cc6ad 100644 --- a/inire/router/config.py +++ b/inire/router/config.py @@ -25,6 +25,7 @@ class RouterConfig: bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0]) sbend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0, 500.0]) snap_to_target_dist: float = 1000.0 + use_analytical_sbends: bool = True bend_penalty: float = 250.0 sbend_penalty: float = 500.0 bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc" diff --git a/inire/router/pathfinder.py b/inire/router/pathfinder.py index 5f2f500..6cb1410 100644 --- a/inire/router/pathfinder.py +++ b/inire/router/pathfinder.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import time +import random from dataclasses import dataclass from typing import TYPE_CHECKING, Callable @@ -39,7 +40,7 @@ class PathFinder: """ Multi-net router using Negotiated Congestion. """ - __slots__ = ('router', 'cost_evaluator', 'max_iterations', 'base_congestion_penalty', 'use_tiered_strategy') + __slots__ = ('router', 'cost_evaluator', 'max_iterations', 'base_congestion_penalty', 'use_tiered_strategy', 'congestion_multiplier', 'accumulated_expanded_nodes') router: AStarRouter """ The A* search engine """ @@ -53,6 +54,9 @@ class PathFinder: base_congestion_penalty: float """ Starting penalty for overlaps """ + congestion_multiplier: float + """ Multiplier for congestion penalty per iteration """ + use_tiered_strategy: bool """ If True, use simpler collision models in early iterations for speed """ @@ -62,6 +66,7 @@ class PathFinder: cost_evaluator: CostEvaluator, max_iterations: int = 10, base_congestion_penalty: float = 100.0, + congestion_multiplier: float = 1.5, use_tiered_strategy: bool = True, ) -> None: """ @@ -72,13 +77,16 @@ class PathFinder: cost_evaluator: The evaluator for path costs. max_iterations: Maximum number of rip-up and reroute iterations. base_congestion_penalty: Starting penalty for overlaps. + congestion_multiplier: Multiplier for congestion penalty per iteration. use_tiered_strategy: Whether to use simplified collision models in early iterations. """ self.router = router self.cost_evaluator = cost_evaluator self.max_iterations = max_iterations self.base_congestion_penalty = base_congestion_penalty + self.congestion_multiplier = congestion_multiplier self.use_tiered_strategy = use_tiered_strategy + self.accumulated_expanded_nodes: list[tuple[float, float, float]] = [] def route_all( self, @@ -86,6 +94,8 @@ class PathFinder: net_widths: dict[str, float], store_expanded: bool = False, iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None, + shuffle_nets: bool = False, + seed: int | None = None, ) -> dict[str, RoutingResult]: """ Route all nets in the netlist using Negotiated Congestion. @@ -93,25 +103,40 @@ class PathFinder: Args: netlist: Mapping of net_id to (start_port, target_port). net_widths: Mapping of net_id to waveguide width. - store_expanded: Whether to store expanded nodes for the last iteration. + store_expanded: Whether to store expanded nodes for ALL iterations and nets. iteration_callback: Optional callback(iteration_idx, current_results). + shuffle_nets: Whether to randomize the order of nets each iteration. + seed: Optional seed for randomization (enables reproducibility). Returns: Mapping of net_id to RoutingResult. """ results: dict[str, RoutingResult] = {} self.cost_evaluator.congestion_penalty = self.base_congestion_penalty + self.accumulated_expanded_nodes = [] start_time = time.monotonic() num_nets = len(netlist) session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations) + all_net_ids = list(netlist.keys()) + for iteration in range(self.max_iterations): any_congestion = False + # Clear accumulation for this iteration so callback gets fresh data + self.accumulated_expanded_nodes = [] + logger.info(f'PathFinder Iteration {iteration}...') + # 0. Shuffle nets if requested + if shuffle_nets: + # Use a new seed based on iteration for deterministic different orders + it_seed = (seed + iteration) if seed is not None else None + random.Random(it_seed).shuffle(all_net_ids) + # Sequence through nets - for net_id, (start, target) in netlist.items(): + for net_id in all_net_ids: + start, target = netlist[net_id] # Timeout check elapsed = time.monotonic() - start_time if elapsed > session_timeout: @@ -139,15 +164,16 @@ class PathFinder: current_node_limit = base_node_limit * (iteration + 1) net_start = time.monotonic() - # Store expanded only in the last potential iteration or if specifically requested - do_store = store_expanded and (iteration == self.max_iterations - 1) # Temporarily override node_limit original_limit = self.router.node_limit self.router.node_limit = current_node_limit - path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True, store_expanded=do_store, skip_congestion=skip_cong) + path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True, store_expanded=store_expanded, skip_congestion=skip_cong) + if store_expanded and self.router.last_expanded_nodes: + self.accumulated_expanded_nodes.extend(self.router.last_expanded_nodes) + # Restore self.router.node_limit = original_limit @@ -205,7 +231,7 @@ class PathFinder: break # 4. Inflate congestion penalty - self.cost_evaluator.congestion_penalty *= 1.5 + self.cost_evaluator.congestion_penalty *= self.congestion_multiplier return self._finalize_results(results, netlist) diff --git a/inire/utils/visualization.py b/inire/utils/visualization.py index cc679d4..f314aa9 100644 --- a/inire/utils/visualization.py +++ b/inire/utils/visualization.py @@ -142,6 +142,7 @@ def plot_danger_map( plt.colorbar(im, ax=ax, label='Danger Cost') ax.set_title("Danger Map (Proximity Costs)") return fig, ax + def plot_expanded_nodes( nodes: list[tuple[float, float, float]], ax: Axes | None = None, @@ -162,3 +163,56 @@ def plot_expanded_nodes( x, y, _ = zip(*nodes) ax.scatter(x, y, s=1, c=color, alpha=alpha, zorder=0) return fig, ax + +def plot_expansion_density( + nodes: list[tuple[float, float, float]], + bounds: tuple[float, float, float, float], + ax: Axes | None = None, + bins: int | tuple[int, int] = 50, +) -> tuple[Figure, Axes]: + """ + Plot a density heatmap (2D histogram) of expanded nodes. + + Args: + nodes: List of (x, y, orientation) tuples. + bounds: (minx, miny, maxx, maxy) for the plot range. + ax: Optional existing axes to plot on. + bins: Number of bins for the histogram (int or (nx, ny)). + + Returns: + Figure and Axes objects. + """ + if ax is None: + fig, ax = plt.subplots(figsize=(12, 12)) + else: + fig = ax.get_figure() + + if not nodes: + ax.text(0.5, 0.5, "No Expansion Data", ha='center', va='center', transform=ax.transAxes) + return fig, ax + + x, y, _ = zip(*nodes) + + # Create 2D histogram + h, xedges, yedges = numpy.histogram2d( + x, y, + bins=bins, + range=[[bounds[0], bounds[2]], [bounds[1], bounds[3]]] + ) + + # Plot as image + im = ax.imshow( + h.T, + origin='lower', + extent=[bounds[0], bounds[2], bounds[1], bounds[3]], + cmap='plasma', + interpolation='nearest', + alpha=0.7 + ) + + plt.colorbar(im, ax=ax, label='Expansion Count') + ax.set_title("Search Expansion Density") + ax.set_xlim(bounds[0], bounds[2]) + ax.set_ylim(bounds[1], bounds[3]) + + return fig, ax