2025-10-27 16:30:40 -07:00

284 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Board Analysis Module
Analyzes PCB characteristics to derive optimal routing parameters.
Computes capacity, demand, congestion ratio, and layer assignments.
"""
from dataclasses import dataclass
from typing import Dict, List, Set, Tuple, Optional
import numpy as np
import logging
logger = logging.getLogger(__name__)
@dataclass
class BoardCharacteristics:
"""Physical and topological properties of the board"""
# Layer configuration
signal_layers: List[int] # [1, 2, 3, ..., N]
h_layers: Set[int] # Layers designated for horizontal routing
v_layers: Set[int] # Layers designated for vertical routing
layer_count: int # Total signal layers
plane_layers: Set[int] # Power/ground planes (not routed)
# Physical dimensions
board_width_mm: float
board_height_mm: float
usable_area_mm2: float # After subtracting keepouts
grid_pitch_mm: float
# Routing capacity
total_horizontal_channels: int # Available H routing channels
total_vertical_channels: int # Available V routing channels
channels_per_h_layer: int # Avg channels per H layer
channels_per_v_layer: int # Avg channels per V layer
via_capacity_estimate: int # Estimated via locations
# Routing demand
net_count: int
total_hpwl_mm: float # Sum of half-perimeter wirelengths
avg_net_hpwl_mm: float
demand_horizontal_mm: float # Sum of horizontal spans
demand_vertical_mm: float # Sum of vertical spans
demand_h_percentage: float # % of demand that's horizontal
# Congestion analysis
congestion_ratio: float # ρ = demand / capacity (key metric!)
density_nets_per_mm2: float
routing_complexity: str # "SPARSE", "NORMAL", "TIGHT", "DENSE"
# Via analysis
allowed_via_spans: Set[Tuple[int, int]] # Legal via layer pairs
via_flexibility: float # % of possible pairs that are legal
def analyze_board_characteristics(
lattice,
tasks: Dict,
board_data: Optional[Dict] = None
) -> BoardCharacteristics:
"""
Analyze board and compute routing characteristics.
Args:
lattice: Routing lattice with dimensions and layer info
tasks: Dict of nets to route {net_id: (src_node, dst_node)}
board_data: Optional dict with board metadata
Returns:
BoardCharacteristics with all analyzed properties
"""
logger.info("=" * 80)
logger.info("ANALYZING BOARD CHARACTERISTICS")
logger.info("=" * 80)
# Extract basic board properties
layer_count = lattice.layers
grid_pitch_mm = lattice.pitch
# Calculate board dimensions from bounds or lattice steps
if hasattr(lattice, 'bounds'):
bounds = lattice.bounds
board_width_mm = bounds[2] - bounds[0]
board_height_mm = bounds[3] - bounds[1]
else:
# Fallback: calculate from grid steps
board_width_mm = lattice.x_steps * grid_pitch_mm
board_height_mm = lattice.y_steps * grid_pitch_mm
# All layers except F.Cu (0) and B.Cu (Nz-1)
signal_layers = list(range(1, layer_count - 1))
plane_layers = set() # Could be extracted from board_data if available
logger.info(f"Board: {board_width_mm:.1f}mm × {board_height_mm:.1f}mm")
logger.info(f"Layers: {layer_count} total, {len(signal_layers)} signal")
logger.info(f"Grid pitch: {grid_pitch_mm}mm")
# Compute usable area (simplified - could subtract keepouts)
usable_area_mm2 = board_width_mm * board_height_mm
# Assign H/V layers based on demand analysis
h_layers, v_layers, demand_h_pct = _assign_hv_layers_by_demand(
signal_layers, tasks, lattice
)
logger.info(f"H-layers: {sorted(h_layers)} ({len(h_layers)} layers)")
logger.info(f"V-layers: {sorted(v_layers)} ({len(v_layers)} layers)")
logger.info(f"Demand: {demand_h_pct*100:.1f}% horizontal, {(1-demand_h_pct)*100:.1f}% vertical")
# Compute routing capacity (how much space we have)
channels_x = int(board_width_mm / grid_pitch_mm)
channels_y = int(board_height_mm / grid_pitch_mm)
channels_per_h_layer = channels_x
channels_per_v_layer = channels_y
total_h_channels = len(h_layers) * channels_x * channels_y # H layer capacity
total_v_channels = len(v_layers) * channels_y * channels_x # V layer capacity
via_capacity_estimate = channels_x * channels_y * (layer_count - 1) # Rough estimate
# Compute routing demand from nets
net_count = len(tasks)
total_hpwl = 0.0
demand_h = 0.0
demand_v = 0.0
for net_id, (src_node, dst_node) in tasks.items():
src_x, src_y, src_z = lattice.idx_to_coord(src_node)
dst_x, dst_y, dst_z = lattice.idx_to_coord(dst_node)
dx = abs(dst_x - src_x) * grid_pitch_mm
dy = abs(dst_y - src_y) * grid_pitch_mm
hpwl = dx + dy
total_hpwl += hpwl
demand_h += dx
demand_v += dy
avg_net_hpwl = total_hpwl / max(1, net_count)
logger.info(f"Nets: {net_count}")
logger.info(f"Total HPWL: {total_hpwl:.0f}mm")
logger.info(f"Avg net length: {avg_net_hpwl:.1f}mm")
# Compute congestion ratio ρ = demand / capacity
# This is THE key metric for parameter derivation
# Apply detour factor (1.3×) and utilization target (0.75×)
detour_factor = 1.3
utilization_target = 0.75
effective_demand = total_hpwl * detour_factor
effective_capacity = (total_h_channels + total_v_channels) * grid_pitch_mm * utilization_target
congestion_ratio = effective_demand / max(1.0, effective_capacity)
# Classify routing complexity
if congestion_ratio < 0.6:
complexity = "SPARSE"
elif congestion_ratio < 0.9:
complexity = "NORMAL"
elif congestion_ratio < 1.2:
complexity = "TIGHT"
else:
complexity = "DENSE"
logger.info(f"Congestion ratio ρ = {congestion_ratio:.3f} ({complexity})")
logger.info(f" Demand: {effective_demand:.0f}mm (with {detour_factor}× detour)")
logger.info(f" Capacity: {effective_capacity:.0f}mm (at {utilization_target*100:.0f}% target)")
# Compute density
density = net_count / max(1.0, usable_area_mm2)
logger.info(f"Density: {density:.4f} nets/mm²")
# Analyze via spans (simplified - assume all-to-all for now)
allowed_via_spans = set()
for i in range(layer_count):
for j in range(i + 1, layer_count):
allowed_via_spans.add((i, j))
total_possible = layer_count * (layer_count - 1) // 2
via_flexibility = len(allowed_via_spans) / max(1, total_possible)
logger.info(f"Via flexibility: {via_flexibility*100:.0f}% of possible pairs allowed")
logger.info("=" * 80)
return BoardCharacteristics(
signal_layers=signal_layers,
h_layers=h_layers,
v_layers=v_layers,
layer_count=len(signal_layers),
plane_layers=plane_layers,
board_width_mm=board_width_mm,
board_height_mm=board_height_mm,
usable_area_mm2=usable_area_mm2,
grid_pitch_mm=grid_pitch_mm,
total_horizontal_channels=total_h_channels,
total_vertical_channels=total_v_channels,
channels_per_h_layer=channels_per_h_layer,
channels_per_v_layer=channels_per_v_layer,
via_capacity_estimate=via_capacity_estimate,
net_count=net_count,
total_hpwl_mm=total_hpwl,
avg_net_hpwl_mm=avg_net_hpwl,
demand_horizontal_mm=demand_h,
demand_vertical_mm=demand_v,
demand_h_percentage=demand_h / max(1.0, demand_h + demand_v),
congestion_ratio=congestion_ratio,
density_nets_per_mm2=density,
routing_complexity=complexity,
allowed_via_spans=allowed_via_spans,
via_flexibility=via_flexibility,
)
def _assign_hv_layers_by_demand(
signal_layers: List[int],
tasks: Dict,
lattice,
anchor_top_is_h: bool = True
) -> Tuple[Set[int], Set[int], float]:
"""
Assign layers to horizontal or vertical routing based on demand.
Returns:
(h_layers, v_layers, demand_h_percentage)
"""
if not tasks:
# Fallback: alternating assignment
h_layers = set(l for l in signal_layers if (l % 2 == 1) == anchor_top_is_h)
v_layers = set(signal_layers) - h_layers
return h_layers, v_layers, 0.5
# Measure demand orientation
demand_dx = 0.0
demand_dy = 0.0
for net_id, (src_node, dst_node) in tasks.items():
src_x, src_y, _ = lattice.idx_to_coord(src_node)
dst_x, dst_y, _ = lattice.idx_to_coord(dst_node)
demand_dx += abs(dst_x - src_x)
demand_dy += abs(dst_y - src_y)
# Calculate horizontal demand percentage
total_demand = demand_dx + demand_dy
if total_demand < 1e-6:
demand_h_pct = 0.5 # Equal split if no demand
else:
demand_h_pct = demand_dx / total_demand
# Determine how many H layers to allocate
n_signal = len(signal_layers)
h_goal = max(1, min(n_signal - 1, int(round(n_signal * demand_h_pct))))
# Assign layers alternating, starting from anchor
h_layers = set()
v_layers = set()
want_h = anchor_top_is_h
for layer in signal_layers:
if want_h and len(h_layers) < h_goal:
h_layers.add(layer)
else:
v_layers.add(layer)
want_h = not want_h
# Adjust if we didn't hit goal exactly (shouldn't happen with good rounding)
while len(h_layers) < h_goal and v_layers:
# Move a mid-stack V layer to H
mid_layer = sorted(v_layers)[len(v_layers) // 2]
v_layers.remove(mid_layer)
h_layers.add(mid_layer)
while len(h_layers) > h_goal and h_layers:
# Move a mid-stack H layer to V
mid_layer = sorted(h_layers)[len(h_layers) // 2]
h_layers.remove(mid_layer)
v_layers.add(mid_layer)
return h_layers, v_layers, demand_h_pct