Route 45-degree traces on the exact track (opt-in diagonal mode)
Add diagonal=True to the exact router: after orthogonal routing, each 2-pin net is retried as the shortest exactly-clean octilinear (0/45/90/135-degree) trace. A dropped net is recovered; an orthogonal route is replaced only when the diagonal is strictly shorter. Candidates are the direct 45-degree segment or the two diagonal-plus-axis two-benders; each segment's diagonal copper is covered by the exact octagon and its clearance checked with clearance_conflict_shape, so a trace fits a diagonal corridor the bounding-box cover would reject. Endpoints stay on the pads; diagonal copper is stored with its exact octagon so later nets clear it precisely; same-net copper is ignored during the check so the candidate is verified against every other net before the orthogonal copper is ripped. With diagonal=False the pass is skipped and output is byte-for-byte the orthogonal router. Threaded through route_dsn_board_exact / build_exact_routing_result / route(engine="exact").
This commit is contained in:
parent
20dc5fd96a
commit
700ed06843
@ -28,11 +28,20 @@ win needs a continuous expansion-room router (the next gap). Also deferred:
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import math
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import Pin
|
||||
from freeroute.board.search_tree import ShapeSearchTree, TreeShape
|
||||
from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape, shove_segment
|
||||
from freeroute.geometry import (
|
||||
IntBox,
|
||||
IntPoint,
|
||||
Polyline,
|
||||
PolylineShape,
|
||||
segment_box,
|
||||
segment_octagon,
|
||||
shove_segment,
|
||||
)
|
||||
|
||||
from .grid_router import RouteResult, route_board
|
||||
|
||||
@ -78,10 +87,17 @@ def route_board_exact(
|
||||
max_passes: int = 10,
|
||||
rip_up: bool = True,
|
||||
shove: bool = False,
|
||||
diagonal: bool = False,
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry."""
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
``diagonal`` (opt-in) adds a 45-degree recovery pass: a still-dropped 2-pin net
|
||||
is retried as an octilinear (0/45/90/135-degree) trace whose diagonal copper is
|
||||
covered by an exact integer octagon, so it fits diagonal corridors the
|
||||
orthogonal L cannot. With ``diagonal=False`` the pass is skipped and the output
|
||||
is byte-for-byte the orthogonal router."""
|
||||
signal_layers = layers or [
|
||||
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
||||
]
|
||||
@ -165,6 +181,13 @@ def route_board_exact(
|
||||
if done:
|
||||
shoves += done
|
||||
|
||||
if diagonal:
|
||||
outline = board.bounding_box
|
||||
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||
_diagonalize(
|
||||
net_no, board, tree, accepted, owner, signal_layers, half_width, clearance, outline
|
||||
)
|
||||
|
||||
clean = RouteResult(half_width)
|
||||
for net_no, acc in accepted.items():
|
||||
clean.wires[net_no] = acc.wires
|
||||
@ -174,6 +197,139 @@ def route_board_exact(
|
||||
return ExactRouteResult(result=clean, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
||||
|
||||
|
||||
# --- 45-degree (diagonal) recovery -------------------------------------------
|
||||
|
||||
|
||||
def _octilinear_candidates(a: IntPoint, b: IntPoint) -> list[list[IntPoint]]:
|
||||
"""Octilinear (0/45/90/135-degree) polylines from ``a`` to ``b``.
|
||||
|
||||
A direct diagonal when the offset is exactly 45-degree, otherwise the two
|
||||
two-segment paths that spend ``min(|dx|, |dy|)`` on a 45-degree run and the
|
||||
rest on an axis run (diagonal-first and diagonal-last)."""
|
||||
dx, dy = b.x - a.x, b.y - a.y
|
||||
sx = (dx > 0) - (dx < 0)
|
||||
sy = (dy > 0) - (dy < 0)
|
||||
adx, ady = abs(dx), abs(dy)
|
||||
if adx == ady:
|
||||
raw = [[a, b]]
|
||||
else:
|
||||
m = min(adx, ady)
|
||||
raw = [
|
||||
[a, IntPoint(a.x + sx * m, a.y + sy * m), b], # diagonal first
|
||||
[a, IntPoint(b.x - sx * m, b.y - sy * m), b], # diagonal last
|
||||
]
|
||||
out: list[list[IntPoint]] = []
|
||||
for corners in raw:
|
||||
deduped: list[IntPoint] = []
|
||||
for p in corners:
|
||||
if deduped and deduped[-1].x == p.x and deduped[-1].y == p.y:
|
||||
continue
|
||||
deduped.append(p)
|
||||
if len(deduped) >= 2:
|
||||
out.append(deduped)
|
||||
return out
|
||||
|
||||
|
||||
def _candidate_tiles(corners, net_no, layer, tree, half_width, clearance, outline):
|
||||
"""``(length, tiles)`` if this octilinear candidate is exactly clean, else
|
||||
``None``. ``tiles`` are ``(box, exact, seg)`` triples ready to insert: a plain
|
||||
box for an orthogonal segment or corner, a bounding box + octagon copper + seg
|
||||
for a diagonal segment."""
|
||||
tiles: list[tuple[IntBox, object, object]] = []
|
||||
length = 0.0
|
||||
for p, q in zip(corners, corners[1:], strict=False):
|
||||
dx, dy = q.x - p.x, q.y - p.y
|
||||
length += math.hypot(dx, dy)
|
||||
if dx == 0 or dy == 0: # orthogonal
|
||||
box = segment_box(p, q, half_width)
|
||||
if not outline.contains_box(box):
|
||||
return None
|
||||
if tree.clearance_conflict(box, net_no, layer, clearance):
|
||||
return None
|
||||
tiles.append((box, None, None))
|
||||
elif abs(dx) == abs(dy): # 45-degree diagonal
|
||||
copper = segment_octagon(p, q, half_width)
|
||||
if not outline.contains_box(copper.bounding_box()):
|
||||
return None
|
||||
grown = segment_octagon(p, q, half_width + clearance)
|
||||
if tree.clearance_conflict_shape(grown, net_no, layer):
|
||||
return None
|
||||
tiles.append((copper.bounding_box(), copper, (p, q, half_width)))
|
||||
else: # not octilinear -- reject
|
||||
return None
|
||||
for corner in corners[1:-1]:
|
||||
square = IntBox(
|
||||
corner.x - half_width,
|
||||
corner.y - half_width,
|
||||
corner.x + half_width,
|
||||
corner.y + half_width,
|
||||
)
|
||||
if not outline.contains_box(square):
|
||||
return None
|
||||
if tree.clearance_conflict(square, net_no, layer, clearance):
|
||||
return None
|
||||
tiles.append((square, None, None))
|
||||
return length, tiles
|
||||
|
||||
|
||||
def _wire_length(wires) -> float:
|
||||
total = 0.0
|
||||
for _layer, corners in wires:
|
||||
for p, q in zip(corners, corners[1:], strict=False):
|
||||
total += math.hypot(q.x - p.x, q.y - p.y)
|
||||
return total
|
||||
|
||||
|
||||
def _diagonalize(
|
||||
net_no, board, tree, accepted, owner, signal_layers, half_width, clearance, outline
|
||||
) -> bool:
|
||||
"""Route (or shorten) a 2-pin net with the shortest exactly-clean octilinear
|
||||
trace. Recovers a dropped net; replaces an orthogonal route only when the
|
||||
diagonal is strictly shorter. Endpoints stay on the pads; diagonal copper is
|
||||
stored as an exact octagon so later nets clear it precisely. Same-net copper is
|
||||
ignored by the clearance check, so the candidate is verified against every
|
||||
*other* net before the old copper is removed. Returns ``True`` on a change."""
|
||||
pins = _net_pins(board, net_no, signal_layers)
|
||||
if len(pins) != 2:
|
||||
return False
|
||||
a, b = pins
|
||||
layer = next(iter(set(a.layers) & set(b.layers) & set(signal_layers)), None)
|
||||
if layer is None:
|
||||
return False
|
||||
|
||||
acc = accepted.get(net_no)
|
||||
if acc is not None and acc.vias:
|
||||
return False # keep a multi-layer orthogonal route; diagonal MVP is single-layer
|
||||
|
||||
best = None
|
||||
for corners in _octilinear_candidates(a.location, b.location):
|
||||
found = _candidate_tiles(corners, net_no, layer, tree, half_width, clearance, outline)
|
||||
if found is None:
|
||||
continue
|
||||
length, tiles = found
|
||||
if best is None or length < best[0]:
|
||||
best = (length, corners, tiles)
|
||||
if best is None:
|
||||
return False
|
||||
length, corners, tiles = best
|
||||
|
||||
if acc is not None:
|
||||
if length >= _wire_length(acc.wires):
|
||||
return False # diagonal is not shorter -- keep the orthogonal route
|
||||
for oid in acc.owners:
|
||||
tree.remove_owner(oid) # rip the orthogonal copper; diagonal already clean of others
|
||||
|
||||
new = _Accepted(wires=[(layer, corners)], vias=[])
|
||||
for box, exact, seg in tiles:
|
||||
oid = next(owner)
|
||||
tree.insert(
|
||||
TreeShape(oid, net_no, frozenset({layer}), box, routed=True, exact=exact, seg=seg)
|
||||
)
|
||||
new.owners.append(oid)
|
||||
accepted[net_no] = new
|
||||
return True
|
||||
|
||||
|
||||
# --- shove recovery ----------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@ -111,11 +111,13 @@ def route_dsn_board_exact(
|
||||
max_passes: int = 10,
|
||||
rip_up: bool = True,
|
||||
shove: bool = False,
|
||||
diagonal: bool = False,
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||
``shove`` enables moving existing traces aside to recover dropped nets;
|
||||
``rip_up`` toggles the underlying grid's rip-up-and-retry."""
|
||||
``rip_up`` toggles the underlying grid's rip-up-and-retry; ``diagonal`` enables
|
||||
the 45-degree recovery pass for still-dropped 2-pin nets."""
|
||||
board = build_board(dsn)
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||
@ -128,15 +130,18 @@ def route_dsn_board_exact(
|
||||
max_passes=max_passes,
|
||||
rip_up=rip_up,
|
||||
shove=shove,
|
||||
diagonal=diagonal,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
|
||||
def build_exact_routing_result(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False, diagonal: bool = False
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(dsn, layers=layers, shove=shove)
|
||||
exact, scale, layer_names = route_dsn_board_exact(
|
||||
dsn, layers=layers, shove=shove, diagonal=diagonal
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
@ -177,6 +182,7 @@ def route(
|
||||
engine: str = "grid",
|
||||
shove: bool = False,
|
||||
pack: bool = False,
|
||||
diagonal: bool = False,
|
||||
) -> str:
|
||||
"""Route a Specctra DSN string and return the routed SES string.
|
||||
|
||||
@ -190,7 +196,7 @@ def route(
|
||||
"""
|
||||
dsn = parse_dsn(dsn_text)
|
||||
if engine == "exact":
|
||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove, diagonal=diagonal)
|
||||
elif engine == "room":
|
||||
result = build_rooms_routing_result(dsn, layers=layers, shove=shove, pack=pack)
|
||||
else:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user