Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 71 additions & 40 deletions board.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ def _add_modules_from_loader(self) -> None:
# Get module properties
name = module_data.get('name')
version = module_data.get('version')
module_id = module_data.get('id')
position = (module_data['position']['x'], module_data['position']['y'])
rotation = module_data.get('rotation')

# Create Module object
module = Module(name=name, version=version, position=position, rotation=rotation)
module = Module(name=name, version=version, position=position, rotation=rotation, module_id=module_id)
self.modules.append(module)

def _add_layers_from_loader(self) -> None:
Expand Down Expand Up @@ -295,9 +296,12 @@ def _validate_zones_and_modules(self) -> None:
if not self.zones:
print("🟠 No zones to validate")
return

# Recompute warnings fresh for this validation pass
self.position_warnings = []

# 1. Check if any module zones overlap
print("🔵 Validating module zone overlaps")
# 1. Check if any module zones overlap or are too close
print("🔵 Validating module zone spacing")
modules_with_zones = [module for module in self.modules if hasattr(module, 'zone') and module.zone]

for i, module1 in enumerate(modules_with_zones):
Expand All @@ -306,12 +310,30 @@ def _validate_zones_and_modules(self) -> None:
if i >= j:
continue

if self._do_zones_overlap(module1.zone, module2.zone):
warning = f"🔴 WARNING: Zone overlap detected between modules '{module1.name}' and '{module2.name}'"
print(warning)
overlap_without_margin = self._do_zones_overlap(module1.zone, module2.zone, margin=0.0)
if overlap_without_margin:
warning = (
"MODULE_OVERLAPPING_OTHER_MODULE "
f"moduleIds=[{module1.module_id},{module2.module_id}] "
f"moduleIdShort=[{module1.module_id[:4] if module1.module_id else 'unkn'},{module2.module_id[:4] if module2.module_id else 'unkn'}] "
f"moduleNames=[{module1.name},{module2.name}]"
)
print(f"🔴 {warning}")
self.position_warnings.append(warning)
continue

overlap_with_margin = self._do_zones_overlap(module1.zone, module2.zone)
if overlap_with_margin:
warning = (
"MODULE_TOO_CLOSE_TO_OTHER_MODULE "
f"moduleIds=[{module1.module_id},{module2.module_id}] "
f"moduleIdShort=[{module1.module_id[:4] if module1.module_id else 'unkn'},{module2.module_id[:4] if module2.module_id else 'unkn'}] "
f"moduleNames=[{module1.name},{module2.name}]"
)
print(f"🔴 {warning}")
self.position_warnings.append(warning)

# 2. Check if any module zone extends beyond the board size
# 2. Check if any module zone extends beyond board or is too close to board edge
print("🔵 Validating module zones within board boundaries")
# Calculate the board boundaries
xmin = self.origin_x - self.width / 2
Expand All @@ -323,45 +345,46 @@ def _validate_zones_and_modules(self) -> None:
# Extract corner points from module's zone
bl, tl, tr, br = module.zone

# Check if any corner is outside the board boundaries
if (bl[0] < xmin or bl[1] < ymin or
tl[0] < xmin or tl[1] > ymax or
tr[0] > xmax or tr[1] > ymax or
br[0] > xmax or br[1] < ymin):

warning = f"🔴 Module '{module.name}' zone extends beyond board boundaries"
print(warning)
zone_x_min = min(bl[0], tl[0], tr[0], br[0])
zone_x_max = max(bl[0], tl[0], tr[0], br[0])
zone_y_min = min(bl[1], tl[1], tr[1], br[1])
zone_y_max = max(bl[1], tl[1], tr[1], br[1])

# Overhanging board edge
if (zone_x_min < xmin or zone_y_min < ymin or zone_x_max > xmax or zone_y_max > ymax):
warning = (
"MODULE_OVERHANGING_BOARD_EDGE "
f"moduleId={module.module_id} "
f"moduleIdShort={module.module_id[:4] if module.module_id else 'unkn'} "
f"moduleName={module.name}"
)
print(f"🔴 {warning}")
self.position_warnings.append(warning)
continue

edge_clearance = min(
zone_x_min - xmin,
xmax - zone_x_max,
zone_y_min - ymin,
ymax - zone_y_max,
)

if self.module_margin > 0 and edge_clearance < self.module_margin:
warning = (
"MODULE_TOO_CLOSE_TO_BOARD_EDGE "
f"moduleId={module.module_id} "
f"moduleIdShort={module.module_id[:4] if module.module_id else 'unkn'} "
f"moduleName={module.name}"
)
print(f"🔴 {warning}")
self.position_warnings.append(warning)

# 3. Check if any module zones overlap with remaining zones in self.zones
print("🔵 Validating module zones against other zones")
all_zones = self.zones.get_data()

for module in modules_with_zones:
module_zone = module.zone

for zone in all_zones:
# Skip if this is the module's own zone
# Calculate zone center for comparison
bl_z, _, tr_z, _ = zone
center_x = (bl_z[0] + tr_z[0]) / 2
center_y = (bl_z[1] + tr_z[1]) / 2

# Skip if this is the module's own zone
if module.position.x == center_x and module.position.y == center_y:
continue

if self._do_zones_overlap(module_zone, zone):
warning = f"🔴 Module '{module.name}' zone overlaps with a zone at position {(zone[0], zone[2])}"
print(warning)
self.position_warnings.append(warning)

if not self.position_warnings:
print("🟢 All zones and modules validated successfully!")
else:
print(f"🔴 Found {len(self.position_warnings)} validation issues")

def _do_zones_overlap(self, zone1: List[Tuple[float, float]], zone2: List[Tuple[float, float]]) -> bool:
def _do_zones_overlap(self, zone1: List[Tuple[float, float]], zone2: List[Tuple[float, float]], margin: Optional[float] = None) -> bool:
"""
Check if two zones overlap, considering a module margin.

Expand All @@ -377,7 +400,8 @@ def _do_zones_overlap(self, zone1: List[Tuple[float, float]], zone2: List[Tuple[
bl2, _, tr2, _ = zone2

# Add margin to the bounding boxes
margin = self.module_margin
if margin is None:
margin = self.module_margin

# Create bounding boxes with margin [min_x, min_y, max_x, max_y]
box1 = [bl1[0] - margin, bl1[1] - margin, tr1[0] + margin, tr1[1] + margin]
Expand Down Expand Up @@ -600,6 +624,13 @@ def get_module_name_from_position(self, position: Tuple[float, float]) -> Option
if module.position.x == position[0] and module.position.y == position[1]:
return module.name
return None

def get_module_from_position(self, position: Tuple[float, float], epsilon: float = 1e-6) -> Optional[Module]:
"""Get the module object at a specific position."""
for module in self.modules:
if abs(module.position.x - position[0]) <= epsilon and abs(module.position.y - position[1]) <= epsilon:
return module
return None

def add_sockets(self, sockets: Sockets) -> None:
"""Add sockets to the board
Expand Down
97 changes: 92 additions & 5 deletions bus_router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import math
from collections import defaultdict
from typing import Dict, List, Tuple

Expand Down Expand Up @@ -601,6 +602,76 @@ def _group_sockets(self, sockets_data, zones_data):
print(f"Ordered zone center: {zone_center}, Groups: {groups}")

return ordered_socket_groups

def _get_module_id(self, module) -> str:
return module.module_id if module and getattr(module, "module_id", None) else "unknown"

def _get_module_short_id(self, module_id: str) -> str:
return module_id[:4] if module_id != "unknown" else "unkn"

def _zone_bbox(self, zone: Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float], Tuple[float, float]]) -> Tuple[float, float, float, float]:
bl, _, tr, _ = zone
return bl[0], bl[1], tr[0], tr[1]

def _zone_clearance(self, zone_a, zone_b) -> float:
ax_min, ay_min, ax_max, ay_max = self._zone_bbox(zone_a)
bx_min, by_min, bx_max, by_max = self._zone_bbox(zone_b)

margin = self.board.module_margin
ax_min -= margin
ay_min -= margin
ax_max += margin
ay_max += margin

bx_min -= margin
by_min -= margin
bx_max += margin
by_max += margin

dx = max(ax_min - bx_max, bx_min - ax_max, 0.0)
dy = max(ay_min - by_max, by_min - ay_max, 0.0)
return math.hypot(dx, dy)

def _find_too_close_module_pair(self, module) -> tuple[str, str, str, float]:
if module is None:
return "unknown", "unkn", "unknown", -1.0

best_other = None
best_clearance = float("inf")
best_center_distance = float("inf")

for other in self.board.modules:
if other is module:
continue

if getattr(module, "zone", None) and getattr(other, "zone", None):
clearance = self._zone_clearance(module.zone, other.zone)
else:
clearance = float("inf")

center_distance = math.dist((module.position.x, module.position.y), (other.position.x, other.position.y))

if clearance < best_clearance or (clearance == best_clearance and center_distance < best_center_distance):
best_other = other
best_clearance = clearance
best_center_distance = center_distance

if best_other is None:
return "unknown", "unkn", "unknown", -1.0

other_id = self._get_module_id(best_other)
return other_id, self._get_module_short_id(other_id), best_other.name if best_other.name else "unknown", best_clearance

def _build_pair_issue(self, module_id: str, module_name: str, too_close_id: str, too_close_name: str, clearance_mm: float) -> str:
module_short = self._get_module_short_id(module_id)
too_close_short = self._get_module_short_id(too_close_id)
issue_code = "MODULE_OVERLAPPING_OTHER_MODULE" if clearance_mm <= 0 else "MODULE_TOO_CLOSE_TO_OTHER_MODULE"
return (
f"{issue_code} "
f"moduleIds=[{module_id},{too_close_id}] "
f"moduleIdShort=[{module_short},{too_close_short}] "
f"moduleNames=[{module_name if module_name else 'unknown'},{too_close_name}]"
)

def route(self) -> None:
try:
Expand Down Expand Up @@ -634,18 +705,32 @@ def route(self) -> None:

for zone_center, socket_groups in grouped_sockets.items():

module_name = self.board.get_module_name_from_position(zone_center)
module = self.board.get_module_from_position(zone_center)
module_name = module.name if module else self.board.get_module_name_from_position(zone_center)
module_id = module.module_id if module and module.module_id else "unknown"

# For each group of sockets
for group_idx, socket_group in enumerate(socket_groups):
i = 0
state_visit_counts = defaultdict(int)
max_state_revisits = 4

if self.debugger:
self.debugger.log_event(f"Starting group {group_idx}: {len(socket_group)} sockets")

# Process all sockets in the group
print(f"Socket group length: {len(socket_group)}")
while i < len(socket_group):
group_signature = tuple(
(entry[0], float(entry[1][0]), float(entry[1][1])) for entry in socket_group
)
state_key = (i, group_signature)
state_visit_counts[state_key] += 1

if state_visit_counts[state_key] > max_state_revisits:
too_close_id, too_close_short, too_close_name, too_close_clearance = self._find_too_close_module_pair(module)
raise RuntimeError(self._build_pair_issue(module_id, module_name, too_close_id, too_close_name, too_close_clearance))

socket = socket_group[i]
net_name, socket_pos = socket

Expand Down Expand Up @@ -694,13 +779,15 @@ def route(self) -> None:
timeout = 7
if current_time - last_write_time > timeout:
print(f"🔴 Abandoned job (ID: {thread_context.job_id}) due to expired keepalive ({timeout} seconds)")
sys.exit() # Exit the thread
too_close_id, too_close_short, too_close_name, too_close_clearance = self._find_too_close_module_pair(module)
raise RuntimeError(self._build_pair_issue(module_id, module_name, too_close_id, too_close_name, too_close_clearance))

# Also abandon the job if there's more than 150 images in the routing_imgs folder
routing_imgs_folder = thread_context.job_folder / "routing_imgs"
if routing_imgs_folder.exists() and len(list(routing_imgs_folder.glob("*.png"))) > 150:
print(f"🔴 Abandoned job (ID: {thread_context.job_id}) due to too many routing attempts (>150)")
sys.exit() # Exit the thread
too_close_id, too_close_short, too_close_name, too_close_clearance = self._find_too_close_module_pair(module)
raise RuntimeError(self._build_pair_issue(module_id, module_name, too_close_id, too_close_name, too_close_clearance))


path = self._route_socket_to_bus(self.base_grid, socket_pos, bus_point, net_name)
Expand Down Expand Up @@ -742,8 +829,8 @@ def route(self) -> None:
if i == 0:
i += 1
socket_count += 1
# continue
raise Exception(f" socket at {socket_pos} in group {group_idx} (first socket, cannot backtrack)")
too_close_id, too_close_short, too_close_name, too_close_clearance = self._find_too_close_module_pair(module)
raise Exception(self._build_pair_issue(module_id, module_name, too_close_id, too_close_name, too_close_clearance))

# Otherwise, we can backtrack
print(f"🟠 Backtracking in group {group_idx} at socket {i}")
Expand Down
3 changes: 2 additions & 1 deletion module.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ def __repr__(self) -> str:
return f"Position(x={self.x}, y={self.y})"

class Module:
def __init__(self, name: str, version: str, position: Tuple[float, float], rotation: float):
def __init__(self, name: str, version: str, position: Tuple[float, float], rotation: float, module_id: Optional[str] = None):
self.name = name
self.version = version
self.module_id = module_id
# HACK: Manually inverting the y-coordinate here
self.position = Position(position[0], -position[1])
self.rotation = rotation
Expand Down
Loading