Improve routing quality heuristics and expose detour/bend metrics
This commit is contained in:
parent
f2d48cee85
commit
6c5431a6e5
@ -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);
|
||||
|
||||
@ -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: {},
|
||||
|
||||
247
src/layout.js
247
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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user