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;
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);

View File

@ -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: {},

View File

@ -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
};
}