From 6c5431a6e5a65a51b16357c20d9762503421d22c Mon Sep 17 00:00:00 2001 From: Rbanh Date: Mon, 16 Feb 2026 21:55:23 -0500 Subject: [PATCH] Improve routing quality heuristics and expose detour/bend metrics --- frontend/app.js | 4 +- src/compile.js | 7 +- src/layout.js | 247 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 239 insertions(+), 19 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index cef2ed9..e36c946 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1150,7 +1150,7 @@ async function compileModel(model, opts = {}) { const m = result.layout_metrics; setStatus( - `Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings} crossings, ${m.overlap_edges} overlaps)` + `Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings} crossings, ${m.overlap_edges} overlaps, ${m.total_bends ?? 0} bends, ${(m.detour_ratio ?? 1).toFixed(2)}x detour)` ); } catch (err) { setStatus(`Compile failed: ${err.message}`, false); @@ -1564,7 +1564,7 @@ async function runLayoutAction(path) { fitView(out.compile.layout); saveSnapshot(); setStatus( - `Compiled (${out.compile.errors.length}E, ${out.compile.warnings.length}W | ${out.compile.layout_metrics.crossings} crossings)` + `Compiled (${out.compile.errors.length}E, ${out.compile.warnings.length}W | ${out.compile.layout_metrics.crossings} crossings, ${out.compile.layout_metrics.overlap_edges} overlaps, ${out.compile.layout_metrics.total_bends ?? 0} bends)` ); } catch (err) { setStatus(`Layout action failed: ${err.message}`, false); diff --git a/src/compile.js b/src/compile.js index 2b70968..140cf0a 100644 --- a/src/compile.js +++ b/src/compile.js @@ -255,7 +255,12 @@ export function compile(payload, options = {}) { crossings: 0, label_collisions: 0, tie_points_used: 0, - bus_groups: 0 + bus_groups: 0, + total_bends: 0, + total_length: 0, + direct_length: 0, + detour_ratio: 1, + label_tie_fallbacks: 0 }, bus_groups: [], focus_map: {}, diff --git a/src/layout.js b/src/layout.js index 66f9e4c..57eef95 100644 --- a/src/layout.js +++ b/src/layout.js @@ -18,6 +18,7 @@ const NET_CLASS_PRIORITY = { const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]); const DEFAULT_RENDER_MODE = "schematic_stub"; const ROTATION_STEPS = [0, 90, 180, 270]; +const MIN_CHANNEL_SPACING_STEPS = 2; function toGrid(value) { return Math.round(value / GRID) * GRID; @@ -733,6 +734,65 @@ function hasForeignPointUsage(pointUsage, netName, point) { return false; } +function foreignUsageCount(usageMap, key, ownNet) { + const usage = usageMap.get(key); + if (!usage) { + return 0; + } + + let count = 0; + for (const [net, n] of usage.entries()) { + if (net !== ownNet) { + count += n; + } + } + return count; +} + +function channelCrowdingPenalty(current, next, context) { + const { netName, hLineUsage, vLineUsage } = context; + if (current.x !== next.x && current.y !== next.y) { + return 0; + } + + const isHorizontal = current.y === next.y; + const lineUsage = isHorizontal ? hLineUsage : vLineUsage; + const lineCoord = isHorizontal ? current.y : current.x; + + let penalty = 0; + for (let step = -MIN_CHANNEL_SPACING_STEPS; step <= MIN_CHANNEL_SPACING_STEPS; step += 1) { + const coord = lineCoord + step * GRID; + const foreign = foreignUsageCount(lineUsage, coord, netName); + if (!foreign) { + continue; + } + + const distance = Math.abs(step); + const weight = distance === 0 ? 34 : distance === 1 ? 17 : 7; + penalty += foreign * weight; + } + + return penalty; +} + +function pointCongestionPenalty(point, context) { + const { pointUsage, netName } = context; + let penalty = 0; + for (const nb of [ + { x: point.x, y: point.y }, + { x: point.x + GRID, y: point.y }, + { x: point.x - GRID, y: point.y }, + { x: point.x, y: point.y + GRID }, + { x: point.x, y: point.y - GRID } + ]) { + const foreign = foreignUsageCount(pointUsage, pointKey(nb), netName); + if (foreign) { + penalty += foreign * 10; + } + } + return penalty; +} + function proximityPenalty(point, obstacles, allowedRefs) { let penalty = 0; for (const box of obstacles) { @@ -743,8 +803,8 @@ function proximityPenalty(point, obstacles, allowedRefs) { const dx = Math.max(box.x - point.x, 0, point.x - (box.x + box.w)); const dy = Math.max(box.y - point.y, 0, point.y - (box.y + box.h)); const dist = dx + dy; - if (dist < GRID * 2) { - penalty += (GRID * 2 - dist) * 0.35; + if (dist < GRID * 4) { + penalty += (GRID * 4 - dist) * 0.7; } } return penalty; @@ -816,9 +876,11 @@ function aStar(start, goal, context) { const nbKey = pointKey(nb); const prevCost = gScore.get(currentKey) ?? Number.POSITIVE_INFINITY; - const turnPenalty = current.dir && current.dir !== nb.dir ? 16 : 0; + const turnPenalty = current.dir && current.dir !== nb.dir ? 24 : 0; const obstaclePenalty = proximityPenalty(nb, obstacles, allowedRefs); - const tentative = prevCost + GRID + turnPenalty + obstaclePenalty; + const channelPenalty = channelCrowdingPenalty(current, nb, context); + const congestionPenalty = pointCongestionPenalty(nb, context); + const tentative = prevCost + GRID + turnPenalty + obstaclePenalty + channelPenalty + congestionPenalty; if (tentative >= (gScore.get(nbKey) ?? Number.POSITIVE_INFINITY)) { continue; @@ -898,7 +960,13 @@ function segmentStepPoints(a, b) { return [a, b]; } -function addUsageForSegments(edgeUsage, pointUsage, netName, segments) { +function addLineUsage(lineUsage, coord, netName) { + const usage = lineUsage.get(coord) ?? new Map(); + usage.set(netName, (usage.get(netName) ?? 0) + 1); + lineUsage.set(coord, usage); +} + +function addUsageForSegments(edgeUsage, pointUsage, hLineUsage, vLineUsage, netName, segments) { for (const seg of segments) { const stepPoints = segmentStepPoints(seg.a, seg.b); @@ -912,6 +980,12 @@ function addUsageForSegments(edgeUsage, pointUsage, netName, segments) { edgeUsage.set(eKey, edge); } + if (seg.a.y === seg.b.y) { + addLineUsage(hLineUsage, seg.a.y, netName); + } else if (seg.a.x === seg.b.x) { + addLineUsage(vLineUsage, seg.a.x, netName); + } + for (const p of stepPoints) { const pKey = pointKey(p); const usage = pointUsage.get(pKey) ?? new Map(); @@ -1007,7 +1081,14 @@ function routeLabelTieNet(net, pinNodes, context) { const stub = pointsToSegments([pin.point, pin.exit]); if (stub.length) { routes.push(stub); - addUsageForSegments(context.edgeUsage, context.pointUsage, net.name, stub); + addUsageForSegments( + context.edgeUsage, + context.pointUsage, + context.hLineUsage, + context.vLineUsage, + net.name, + stub + ); } tiePoints.push({ x: pin.exit.x, y: pin.exit.y }); } @@ -1034,7 +1115,15 @@ function routeLabelTieNet(net, pinNodes, context) { routes, labelPoints, tiePoints, - junctionPoints: [] + junctionPoints: [], + route_stats: { + total_length: routeLengthFromSegments(routes), + direct_length: 0, + total_bends: 0, + detour_ratio: 1, + used_label_tie: true, + fallback_reason: null + } }; } @@ -1046,6 +1135,32 @@ function pathLength(points) { return length; } +function routeLengthFromSegments(routes) { + let length = 0; + for (const route of routes) { + for (const seg of route) { + length += Math.abs(seg.a.x - seg.b.x) + Math.abs(seg.a.y - seg.b.y); + } + } + return length; +} + +function countBendsInRoute(routes) { + let bends = 0; + for (const route of routes) { + for (let i = 1; i < route.length; i += 1) { + const prev = route[i - 1]; + const curr = route[i]; + const prevH = prev.a.y === prev.b.y; + const currH = curr.a.y === curr.b.y; + if (prevH !== currH) { + bends += 1; + } + } + } + return bends; +} + function routePointToPointNet(net, pinNodes, context) { if (pinNodes.length < 2) { return { @@ -1053,7 +1168,15 @@ function routePointToPointNet(net, pinNodes, context) { routes: [], labelPoints: [], tiePoints: [], - junctionPoints: [] + junctionPoints: [], + route_stats: { + total_length: 0, + direct_length: 0, + total_bends: 0, + detour_ratio: 1, + used_label_tie: false, + fallback_reason: null + } }; } @@ -1071,7 +1194,14 @@ function routePointToPointNet(net, pinNodes, context) { const sourceStub = pointsToSegments([source.point, source.exit]); if (sourceStub.length) { routes.push(sourceStub); - addUsageForSegments(context.edgeUsage, context.pointUsage, net.name, sourceStub); + addUsageForSegments( + context.edgeUsage, + context.pointUsage, + context.hLineUsage, + context.vLineUsage, + net.name, + sourceStub + ); } const treePoints = new Map(); @@ -1079,6 +1209,8 @@ function routePointToPointNet(net, pinNodes, context) { const allowedRefs = new Set(sorted.map((p) => p.ref)); const remaining = sorted.slice(1); + let accumulatedPath = 0; + let accumulatedDirect = 0; for (const target of remaining) { const candidates = uniquePoints([...treePoints.values()]) @@ -1103,19 +1235,48 @@ function routePointToPointNet(net, pinNodes, context) { } if (!best) { - return routeLabelTieNet(net, sorted, context); + return { + ...routeLabelTieNet(net, sorted, context), + route_stats: { + total_length: 0, + direct_length: 0, + total_bends: 0, + detour_ratio: 1, + used_label_tie: true, + fallback_reason: "no_path" + } + }; } const direct = heuristic(target.exit, best.attach); - if (context.renderMode === "schematic_stub" && best.cost > direct * 1.65) { - return routeLabelTieNet(net, sorted, context); + accumulatedPath += best.cost; + accumulatedDirect += Math.max(GRID, direct); + if (context.renderMode === "schematic_stub" && best.cost > direct * 1.6) { + return { + ...routeLabelTieNet(net, sorted, context), + route_stats: { + total_length: accumulatedPath, + direct_length: accumulatedDirect, + total_bends: 0, + detour_ratio: accumulatedPath / Math.max(GRID, accumulatedDirect), + used_label_tie: true, + fallback_reason: "branch_detour" + } + }; } const branchPoints = [target.point, target.exit, ...best.path.slice(1)]; const branchSegments = pointsToSegments(branchPoints); if (branchSegments.length) { routes.push(branchSegments); - addUsageForSegments(context.edgeUsage, context.pointUsage, net.name, branchSegments); + addUsageForSegments( + context.edgeUsage, + context.pointUsage, + context.hLineUsage, + context.vLineUsage, + net.name, + branchSegments + ); for (const seg of branchSegments) { for (const p of segmentStepPoints(seg.a, seg.b)) { treePoints.set(pointKey(p), p); @@ -1141,12 +1302,42 @@ function routePointToPointNet(net, pinNodes, context) { junctionPoints.push({ x: source.exit.x, y: source.exit.y }); } + const totalLength = routeLengthFromSegments(routes); + const totalBends = countBendsInRoute(routes); + const directLength = Math.max(GRID, accumulatedDirect || heuristic(source.exit, sorted[sorted.length - 1].exit)); + const detourRatio = totalLength / directLength; + const maxAllowedBends = Math.max(6, sorted.length * 3); + if ( + context.renderMode === "schematic_stub" && + (detourRatio > 1.9 || totalBends > maxAllowedBends || totalLength > GRID * 180) + ) { + return { + ...routeLabelTieNet(net, sorted, context), + route_stats: { + total_length: totalLength, + direct_length: directLength, + total_bends: totalBends, + detour_ratio: detourRatio, + used_label_tie: true, + fallback_reason: "global_quality" + } + }; + } + return { mode: "routed", routes, labelPoints, tiePoints: [], - junctionPoints + junctionPoints, + route_stats: { + total_length: totalLength, + direct_length: directLength, + total_bends: totalBends, + detour_ratio: detourRatio, + used_label_tie: false, + fallback_reason: null + } }; } @@ -1194,6 +1385,8 @@ function routeAllNets(model, placed, placedMap, bounds, options) { const obstacles = buildObstacles(model, placed); const edgeUsage = new Map(); const pointUsage = new Map(); + const hLineUsage = new Map(); + const vLineUsage = new Map(); const busGroups = detectBusGroups(model.nets); const busNetNames = new Set(busGroups.flatMap((g) => g.nets)); @@ -1216,6 +1409,8 @@ function routeAllNets(model, placed, placedMap, bounds, options) { obstacles, edgeUsage, pointUsage, + hLineUsage, + vLineUsage, renderMode: options.renderMode, busNetNames }; @@ -1239,7 +1434,15 @@ function routeAllNets(model, placed, placedMap, bounds, options) { routes: [], labelPoints: [], tiePoints: [], - junctionPoints: [] + junctionPoints: [], + route_stats: { + total_length: 0, + direct_length: 0, + total_bends: 0, + detour_ratio: 1, + used_label_tie: false, + fallback_reason: null + } } ); @@ -1367,6 +1570,13 @@ function computeLayoutMetrics(routed, busGroups) { 0 ); const tiePoints = routed.reduce((total, rn) => total + rn.tiePoints.length, 0); + const totalBends = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_bends ?? 0), 0); + const totalLength = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_length ?? 0), 0); + const totalDirect = routed.reduce((total, rn) => total + Number(rn.route_stats?.direct_length ?? 0), 0); + const labelTieFallbacks = routed.reduce( + (total, rn) => total + (rn.route_stats?.used_label_tie && rn.route_stats?.fallback_reason ? 1 : 0), + 0 + ); return { segment_count: segmentCount, @@ -1374,7 +1584,12 @@ function computeLayoutMetrics(routed, busGroups) { crossings: countCrossings(routed), label_collisions: countLabelCollisions(routed), tie_points_used: tiePoints, - bus_groups: busGroups.length + bus_groups: busGroups.length, + total_bends: totalBends, + total_length: totalLength, + direct_length: totalDirect, + detour_ratio: totalDirect > 0 ? totalLength / totalDirect : 1, + label_tie_fallbacks: labelTieFallbacks }; }