Improve routing quality heuristics and expose detour/bend metrics

This commit is contained in:
Rbanh 2026-02-16 21:55:23 -05:00
parent f2d48cee85
commit 6c5431a6e5
3 changed files with 239 additions and 19 deletions

View File

@ -1150,7 +1150,7 @@ async function compileModel(model, opts = {}) {
const m = result.layout_metrics; const m = result.layout_metrics;
setStatus( 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) { } catch (err) {
setStatus(`Compile failed: ${err.message}`, false); setStatus(`Compile failed: ${err.message}`, false);
@ -1564,7 +1564,7 @@ async function runLayoutAction(path) {
fitView(out.compile.layout); fitView(out.compile.layout);
saveSnapshot(); saveSnapshot();
setStatus( 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) { } catch (err) {
setStatus(`Layout action failed: ${err.message}`, false); setStatus(`Layout action failed: ${err.message}`, false);

View File

@ -255,7 +255,12 @@ export function compile(payload, options = {}) {
crossings: 0, crossings: 0,
label_collisions: 0, label_collisions: 0,
tie_points_used: 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: [], bus_groups: [],
focus_map: {}, focus_map: {},

View File

@ -18,6 +18,7 @@ const NET_CLASS_PRIORITY = {
const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]); const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
const DEFAULT_RENDER_MODE = "schematic_stub"; const DEFAULT_RENDER_MODE = "schematic_stub";
const ROTATION_STEPS = [0, 90, 180, 270]; const ROTATION_STEPS = [0, 90, 180, 270];
const MIN_CHANNEL_SPACING_STEPS = 2;
function toGrid(value) { function toGrid(value) {
return Math.round(value / GRID) * GRID; return Math.round(value / GRID) * GRID;
@ -733,6 +734,65 @@ function hasForeignPointUsage(pointUsage, netName, point) {
return false; 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) { function proximityPenalty(point, obstacles, allowedRefs) {
let penalty = 0; let penalty = 0;
for (const box of obstacles) { 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 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 dy = Math.max(box.y - point.y, 0, point.y - (box.y + box.h));
const dist = dx + dy; const dist = dx + dy;
if (dist < GRID * 2) { if (dist < GRID * 4) {
penalty += (GRID * 2 - dist) * 0.35; penalty += (GRID * 4 - dist) * 0.7;
} }
} }
return penalty; return penalty;
@ -816,9 +876,11 @@ function aStar(start, goal, context) {
const nbKey = pointKey(nb); const nbKey = pointKey(nb);
const prevCost = gScore.get(currentKey) ?? Number.POSITIVE_INFINITY; 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 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)) { if (tentative >= (gScore.get(nbKey) ?? Number.POSITIVE_INFINITY)) {
continue; continue;
@ -898,7 +960,13 @@ function segmentStepPoints(a, b) {
return [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) { for (const seg of segments) {
const stepPoints = segmentStepPoints(seg.a, seg.b); const stepPoints = segmentStepPoints(seg.a, seg.b);
@ -912,6 +980,12 @@ function addUsageForSegments(edgeUsage, pointUsage, netName, segments) {
edgeUsage.set(eKey, edge); 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) { for (const p of stepPoints) {
const pKey = pointKey(p); const pKey = pointKey(p);
const usage = pointUsage.get(pKey) ?? new Map(); const usage = pointUsage.get(pKey) ?? new Map();
@ -1007,7 +1081,14 @@ function routeLabelTieNet(net, pinNodes, context) {
const stub = pointsToSegments([pin.point, pin.exit]); const stub = pointsToSegments([pin.point, pin.exit]);
if (stub.length) { if (stub.length) {
routes.push(stub); 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 }); tiePoints.push({ x: pin.exit.x, y: pin.exit.y });
} }
@ -1034,7 +1115,15 @@ function routeLabelTieNet(net, pinNodes, context) {
routes, routes,
labelPoints, labelPoints,
tiePoints, 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; 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) { function routePointToPointNet(net, pinNodes, context) {
if (pinNodes.length < 2) { if (pinNodes.length < 2) {
return { return {
@ -1053,7 +1168,15 @@ function routePointToPointNet(net, pinNodes, context) {
routes: [], routes: [],
labelPoints: [], labelPoints: [],
tiePoints: [], 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]); const sourceStub = pointsToSegments([source.point, source.exit]);
if (sourceStub.length) { if (sourceStub.length) {
routes.push(sourceStub); 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(); const treePoints = new Map();
@ -1079,6 +1209,8 @@ function routePointToPointNet(net, pinNodes, context) {
const allowedRefs = new Set(sorted.map((p) => p.ref)); const allowedRefs = new Set(sorted.map((p) => p.ref));
const remaining = sorted.slice(1); const remaining = sorted.slice(1);
let accumulatedPath = 0;
let accumulatedDirect = 0;
for (const target of remaining) { for (const target of remaining) {
const candidates = uniquePoints([...treePoints.values()]) const candidates = uniquePoints([...treePoints.values()])
@ -1103,19 +1235,48 @@ function routePointToPointNet(net, pinNodes, context) {
} }
if (!best) { 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); const direct = heuristic(target.exit, best.attach);
if (context.renderMode === "schematic_stub" && best.cost > direct * 1.65) { accumulatedPath += best.cost;
return routeLabelTieNet(net, sorted, context); 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 branchPoints = [target.point, target.exit, ...best.path.slice(1)];
const branchSegments = pointsToSegments(branchPoints); const branchSegments = pointsToSegments(branchPoints);
if (branchSegments.length) { if (branchSegments.length) {
routes.push(branchSegments); 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 seg of branchSegments) {
for (const p of segmentStepPoints(seg.a, seg.b)) { for (const p of segmentStepPoints(seg.a, seg.b)) {
treePoints.set(pointKey(p), p); treePoints.set(pointKey(p), p);
@ -1141,12 +1302,42 @@ function routePointToPointNet(net, pinNodes, context) {
junctionPoints.push({ x: source.exit.x, y: source.exit.y }); 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 { return {
mode: "routed", mode: "routed",
routes, routes,
labelPoints, labelPoints,
tiePoints: [], 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 obstacles = buildObstacles(model, placed);
const edgeUsage = new Map(); const edgeUsage = new Map();
const pointUsage = new Map(); const pointUsage = new Map();
const hLineUsage = new Map();
const vLineUsage = new Map();
const busGroups = detectBusGroups(model.nets); const busGroups = detectBusGroups(model.nets);
const busNetNames = new Set(busGroups.flatMap((g) => g.nets)); const busNetNames = new Set(busGroups.flatMap((g) => g.nets));
@ -1216,6 +1409,8 @@ function routeAllNets(model, placed, placedMap, bounds, options) {
obstacles, obstacles,
edgeUsage, edgeUsage,
pointUsage, pointUsage,
hLineUsage,
vLineUsage,
renderMode: options.renderMode, renderMode: options.renderMode,
busNetNames busNetNames
}; };
@ -1239,7 +1434,15 @@ function routeAllNets(model, placed, placedMap, bounds, options) {
routes: [], routes: [],
labelPoints: [], labelPoints: [],
tiePoints: [], 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 0
); );
const tiePoints = routed.reduce((total, rn) => total + rn.tiePoints.length, 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 { return {
segment_count: segmentCount, segment_count: segmentCount,
@ -1374,7 +1584,12 @@ function computeLayoutMetrics(routed, busGroups) {
crossings: countCrossings(routed), crossings: countCrossings(routed),
label_collisions: countLabelCollisions(routed), label_collisions: countLabelCollisions(routed),
tie_points_used: tiePoints, 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
}; };
} }