From 77404fa6153eea1fded1c6561607b0bd0fa73813 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Thu, 16 Jul 2026 21:34:15 +0200 Subject: [PATCH 1/6] Add layer-aware trace-density routing cost --- lib/computeRegionCost.ts | 16 ++- lib/core.ts | 118 ++++++++++++++++-- lib/index.ts | 3 + lib/poly.ts | 12 ++ lib/section-solver/index.ts | 8 +- .../solver/trace-density-region-cost.test.ts | 105 ++++++++++++++++ 6 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 tests/solver/trace-density-region-cost.test.ts diff --git a/lib/computeRegionCost.ts b/lib/computeRegionCost.ts index 5100584..7380bdb 100644 --- a/lib/computeRegionCost.ts +++ b/lib/computeRegionCost.ts @@ -1,8 +1,22 @@ export const DEFAULT_MIN_VIA_PAD_DIAMETER = 0.3 +export const DEFAULT_MIN_TRACE_WIDTH = 0.1 +export const DEFAULT_MIN_TRACE_CLEARANCE = 0.1 export const TRACE_VIA_MARGIN = 0.15 -const traceWidth = 0.1 const IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST = 10 +export const computeTraceDensityCost = ( + shortRegionDimension: number, + distinctNetCount: number, + minTraceWidth = DEFAULT_MIN_TRACE_WIDTH, + minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE, +): number => { + const additionalDistinctNets = Math.max(0, distinctNetCount - 1) + if (additionalDistinctNets === 0) return 0 + + const tracePitch = minTraceWidth + minTraceClearance + return ((additionalDistinctNets * tracePitch) / shortRegionDimension) ** 2 +} + export const isKnownSingleLayerMask = (regionAvailableZMask: number) => regionAvailableZMask > 0 && (regionAvailableZMask & (regionAvailableZMask - 1)) === 0 diff --git a/lib/core.ts b/lib/core.ts index d334dc5..40ee840 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -2,7 +2,10 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { convertToSerializedHyperGraph } from "./compat/convertToSerializedHyperGraph" import { + computeTraceDensityCost, computeRegionCost, + DEFAULT_MIN_TRACE_CLEARANCE, + DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, isKnownSingleLayerMask, } from "./computeRegionCost" @@ -225,6 +228,10 @@ export interface TinyHyperGraphWorkingState { export interface TinyHyperGraphSolverOptions { minViaPadDiameter?: number + /** Minimum routed trace width in millimeters. Defaults to 0.1mm. */ + minTraceWidth?: number + /** Minimum trace-to-trace clearance in millimeters. Defaults to 0.1mm. */ + minTraceClearance?: number DISTANCE_TO_COST?: number RIP_THRESHOLD_START?: number RIP_THRESHOLD_END?: number @@ -242,6 +249,8 @@ export interface TinyHyperGraphSolverOptions { export interface TinyHyperGraphSolverOptionTarget { minViaPadDiameter: number + minTraceWidth: number + minTraceClearance: number DISTANCE_TO_COST: number RIP_THRESHOLD_START: number RIP_THRESHOLD_END: number @@ -268,6 +277,12 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.minViaPadDiameter !== undefined) { solver.minViaPadDiameter = options.minViaPadDiameter } + if (options.minTraceWidth !== undefined) { + solver.minTraceWidth = options.minTraceWidth + } + if (options.minTraceClearance !== undefined) { + solver.minTraceClearance = options.minTraceClearance + } if (options.DISTANCE_TO_COST !== undefined) { solver.DISTANCE_TO_COST = options.DISTANCE_TO_COST } @@ -316,6 +331,8 @@ export const getTinyHyperGraphSolverOptions = ( solver: TinyHyperGraphSolverOptionTarget, ): TinyHyperGraphSolverOptions => ({ minViaPadDiameter: solver.minViaPadDiameter, + minTraceWidth: solver.minTraceWidth, + minTraceClearance: solver.minTraceClearance, DISTANCE_TO_COST: solver.DISTANCE_TO_COST, RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, @@ -357,9 +374,17 @@ export class TinyHyperGraphSolver extends BaseSolver { layerMask: 0, entryExitLayerChanges: 0, } + private traceDensityNetIdsByLayer = Array.from( + { length: 32 }, + () => new Set(), + ) + private traceDensityActiveLayers = new Uint8Array(32) + private traceDensityActiveLayerCount = 0 DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER + minTraceWidth = DEFAULT_MIN_TRACE_WIDTH + minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE RIP_THRESHOLD_START = 0.05 RIP_THRESHOLD_END = 0.8 @@ -728,6 +753,68 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } + private resetTraceDensityScratch(): void { + for (let i = 0; i < this.traceDensityActiveLayerCount; i++) { + this.traceDensityNetIdsByLayer[this.traceDensityActiveLayers[i]]!.clear() + } + this.traceDensityActiveLayerCount = 0 + } + + private addNetToTraceDensityScratch(netId: NetId, layerMask: number): void { + for (let layerId = 0; layerId < 32; layerId++) { + if ((layerMask & (1 << layerId)) === 0) continue + + const netIds = this.traceDensityNetIdsByLayer[layerId]! + if (netIds.size === 0) { + this.traceDensityActiveLayers[this.traceDensityActiveLayerCount++] = + layerId + } + netIds.add(netId) + } + } + + protected getTraceDensityRegionDimension(regionId: RegionId): number { + return Math.min( + this.topology.regionWidth[regionId], + this.topology.regionHeight[regionId], + ) + } + + protected computeTraceDensityCostForRegion( + regionId: RegionId, + regionCache: RegionIntersectionCache, + prospectiveNetId?: NetId, + prospectiveLayerMask = 0, + ): number { + this.resetTraceDensityScratch() + + for (let i = 0; i < regionCache.netIds.length; i++) { + this.addNetToTraceDensityScratch( + regionCache.netIds[i], + regionCache.layerMasks[i], + ) + } + if (prospectiveNetId !== undefined) { + this.addNetToTraceDensityScratch(prospectiveNetId, prospectiveLayerMask) + } + + let maxDistinctNetCount = 0 + for (let i = 0; i < this.traceDensityActiveLayerCount; i++) { + const layerId = this.traceDensityActiveLayers[i] + maxDistinctNetCount = Math.max( + maxDistinctNetCount, + this.traceDensityNetIdsByLayer[layerId]!.size, + ) + } + + return computeTraceDensityCost( + this.getTraceDensityRegionDimension(regionId), + maxDistinctNetCount, + this.minTraceWidth, + this.minTraceClearance, + ) + } + populateSegmentGeometryScratch( regionId: RegionId, port1Id: PortId, @@ -809,6 +896,20 @@ export class TinyHyperGraphSolver extends BaseSolver { const existingEntryExitLayerChanges = regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges const existingSegmentCount = lesserAngles.length + const existingRegionCost = + this.computeRegionCostForRegion( + regionId, + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + existingSegmentCount, + ) + + this.computeTraceDensityCostForRegion( + regionId, + regionCache, + state.currentRouteNetId!, + segmentGeometry.layerMask, + ) state.regionIntersectionCaches[regionId] = { netIds, @@ -819,13 +920,7 @@ export class TinyHyperGraphSolver extends BaseSolver { existingCrossingLayerIntersections, existingEntryExitLayerChanges, existingSegmentCount, - existingRegionCost: this.computeRegionCostForRegion( - regionId, - existingSameLayerIntersections, - existingCrossingLayerIntersections, - existingEntryExitLayerChanges, - existingSegmentCount, - ), + existingRegionCost, } } @@ -1401,7 +1496,14 @@ export class TinyHyperGraphSolver extends BaseSolver { newCrossLayerIntersections, regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges, regionCache.existingSegmentCount + 1, - ) - regionCache.existingRegionCost + ) + + this.computeTraceDensityCostForRegion( + nextRegionId, + regionCache, + state.currentRouteNetId!, + segmentGeometry.layerMask, + ) - + regionCache.existingRegionCost return ( currentCandidate.g + diff --git a/lib/index.ts b/lib/index.ts index 65eb648..799e6fb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,6 +8,9 @@ export * from "./selective-rerip-tiny-hyper-graph-solver" export * from "./bus-solver" export * from "./region-graph" export { + computeTraceDensityCost, + DEFAULT_MIN_TRACE_CLEARANCE, + DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, TRACE_VIA_MARGIN, } from "./computeRegionCost" diff --git a/lib/poly.ts b/lib/poly.ts index 5cfbda0..a54aabb 100644 --- a/lib/poly.ts +++ b/lib/poly.ts @@ -676,6 +676,18 @@ export class PolyHyperGraphSolver extends TinyHyperGraphSolver { ) } + protected override getTraceDensityRegionDimension( + regionId: RegionId, + ): number { + const width = + this.topology.regionBoundsMaxX[regionId] - + this.topology.regionBoundsMinX[regionId] + const height = + this.topology.regionBoundsMaxY[regionId] - + this.topology.regionBoundsMinY[regionId] + return Math.min(width, height) + } + override visualize(): GraphicsObject { return visualizePolyHyperGraph(this) } diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index e66f4a1..74ee65b 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -11,7 +11,11 @@ import { type TinyHyperGraphTopology, type TinyHyperGraphSolverOptions, } from "../core" -import { DEFAULT_MIN_VIA_PAD_DIAMETER } from "../computeRegionCost" +import { + DEFAULT_MIN_TRACE_CLEARANCE, + DEFAULT_MIN_TRACE_WIDTH, + DEFAULT_MIN_VIA_PAD_DIAMETER, +} from "../computeRegionCost" import { shuffle } from "../shuffle" import type { PortId, @@ -881,6 +885,8 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { DISTANCE_TO_COST = 0.05 minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER + minTraceWidth = DEFAULT_MIN_TRACE_WIDTH + minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE VERBOSE = false RIP_THRESHOLD_START = 0.05 diff --git a/tests/solver/trace-density-region-cost.test.ts b/tests/solver/trace-density-region-cost.test.ts new file mode 100644 index 0000000..dae849a --- /dev/null +++ b/tests/solver/trace-density-region-cost.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from "bun:test" +import { + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, +} from "lib/index" + +const createProblem = (portCount: number): TinyHyperGraphProblem => ({ + routeCount: 3, + portSectionMask: new Int8Array(portCount).fill(1), + routeStartPort: new Int32Array([0, 2, 4]), + routeEndPort: new Int32Array([1, 3, 5]), + routeNet: new Int32Array([0, 1, 2]), + regionNetId: new Int32Array(2).fill(-1), +}) + +const createSingleRegionTopology = (): TinyHyperGraphTopology => ({ + portCount: 8, + regionCount: 1, + regionIncidentPorts: [[0, 1, 2, 3, 4, 5, 6, 7]], + incidentPortRegion: Array.from({ length: 8 }, () => [0]), + regionWidth: new Float64Array([1]), + regionHeight: new Float64Array([2]), + regionCenterX: new Float64Array([0]), + regionCenterY: new Float64Array([0]), + regionAvailableZMask: new Int32Array([3]), + portAngleForRegion1: new Int32Array([ + 0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, + ]), + portX: new Float64Array(8), + portY: new Float64Array(8), + portZ: new Int32Array([0, 0, 0, 0, 1, 1, 0, 0]), +}) + +test("trace density counts distinct nets only on shared layers", () => { + const solver = new TinyHyperGraphSolver( + createSingleRegionTopology(), + createProblem(8), + ) + + solver.state.currentRouteNetId = 0 + solver.appendSegmentToRegionCache(0, 0, 1) + solver.appendSegmentToRegionCache(0, 2, 3) + + solver.state.currentRouteNetId = 1 + solver.appendSegmentToRegionCache(0, 4, 5) + expect(solver.state.regionIntersectionCaches[0].existingRegionCost).toBe(0) + + solver.state.currentRouteNetId = 2 + const candidateCost = solver.computeG( + { nextRegionId: 0, portId: 6, f: 0, g: 0, h: 0 }, + 7, + ) + expect(candidateCost).toBeCloseTo(0.04) + + solver.appendSegmentToRegionCache(0, 6, 7) + expect( + solver.state.regionIntersectionCaches[0].existingRegionCost, + ).toBeCloseTo(candidateCost) +}) + +test("trace density prefers wider regions for an additional net", () => { + const topology: TinyHyperGraphTopology = { + portCount: 8, + regionCount: 2, + regionIncidentPorts: [ + [0, 1, 2, 3], + [4, 5, 6, 7], + ], + incidentPortRegion: [[0], [0], [0], [0], [1], [1], [1], [1]], + regionWidth: new Float64Array([0.5, 2]), + regionHeight: new Float64Array([3, 3]), + regionCenterX: new Float64Array(2), + regionCenterY: new Float64Array(2), + regionAvailableZMask: new Int32Array([1, 1]), + portAngleForRegion1: new Int32Array([ + 0, 1000, 2000, 3000, 0, 1000, 2000, 3000, + ]), + portX: new Float64Array(8), + portY: new Float64Array(8), + portZ: new Int32Array(8), + } + const solver = new TinyHyperGraphSolver(topology, createProblem(8), { + minTraceWidth: 0.2, + minTraceClearance: 0.1, + }) + + solver.state.currentRouteNetId = 0 + solver.appendSegmentToRegionCache(0, 0, 1) + solver.appendSegmentToRegionCache(1, 4, 5) + + solver.state.currentRouteNetId = 1 + const narrowRegionCost = solver.computeG( + { nextRegionId: 0, portId: 2, f: 0, g: 0, h: 0 }, + 3, + ) + const wideRegionCost = solver.computeG( + { nextRegionId: 1, portId: 6, f: 0, g: 0, h: 0 }, + 7, + ) + + expect(narrowRegionCost).toBeCloseTo(0.36) + expect(wideRegionCost).toBeCloseTo(0.0225) + expect(narrowRegionCost).toBeGreaterThan(wideRegionCost) +}) From c7d8373458b2351d314f9c21c85c9c25b84f8e54 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Thu, 16 Jul 2026 22:25:21 +0200 Subject: [PATCH 2/6] Cache layer net counts for trace density --- lib/bus-solver/TinyHyperGraphBusSolver.ts | 2 + lib/bus-solver/previewRoutingState.ts | 4 + lib/core.ts | 126 ++++++++---------- lib/countNewIntersections.ts | 27 ++-- lib/section-solver/index.ts | 4 + lib/types.ts | 2 + tests/solver/on-all-routes-routed.test.ts | 2 + tests/solver/region-net-id.test.ts | 2 + .../solver/trace-density-region-cost.test.ts | 8 +- 9 files changed, 95 insertions(+), 82 deletions(-) diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 463034b..5111293 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -1459,6 +1459,8 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { regionCache.lesserAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.greaterAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.layerMasks = EMPTY_PREVIEW_INT32_ARRAY + regionCache.distinctNetCountByLayer.fill(0) + regionCache.maxDistinctNetCount = 0 regionCache.existingCrossingLayerIntersections = 0 regionCache.existingSameLayerIntersections = 0 regionCache.existingEntryExitLayerChanges = 0 diff --git a/lib/bus-solver/previewRoutingState.ts b/lib/bus-solver/previewRoutingState.ts index c537b75..e44e343 100644 --- a/lib/bus-solver/previewRoutingState.ts +++ b/lib/bus-solver/previewRoutingState.ts @@ -58,6 +58,8 @@ export const snapshotPreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), + distinctNetCountByLayer: new Uint32Array(cache.distinctNetCountByLayer), + maxDistinctNetCount: cache.maxDistinctNetCount, existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, @@ -81,6 +83,8 @@ export const restorePreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), + distinctNetCountByLayer: new Uint32Array(cache.distinctNetCountByLayer), + maxDistinctNetCount: cache.maxDistinctNetCount, existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, diff --git a/lib/core.ts b/lib/core.ts index 40ee840..899606b 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -39,6 +39,8 @@ export const createEmptyRegionIntersectionCache = lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + distinctNetCountByLayer: new Uint32Array(32), + maxDistinctNetCount: 0, existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -63,6 +65,10 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + distinctNetCountByLayer: new Uint32Array( + regionIntersectionCache.distinctNetCountByLayer, + ), + maxDistinctNetCount: regionIntersectionCache.maxDistinctNetCount, existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -374,13 +380,6 @@ export class TinyHyperGraphSolver extends BaseSolver { layerMask: 0, entryExitLayerChanges: 0, } - private traceDensityNetIdsByLayer = Array.from( - { length: 32 }, - () => new Set(), - ) - private traceDensityActiveLayers = new Uint8Array(32) - private traceDensityActiveLayerCount = 0 - DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH @@ -753,26 +752,6 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } - private resetTraceDensityScratch(): void { - for (let i = 0; i < this.traceDensityActiveLayerCount; i++) { - this.traceDensityNetIdsByLayer[this.traceDensityActiveLayers[i]]!.clear() - } - this.traceDensityActiveLayerCount = 0 - } - - private addNetToTraceDensityScratch(netId: NetId, layerMask: number): void { - for (let layerId = 0; layerId < 32; layerId++) { - if ((layerMask & (1 << layerId)) === 0) continue - - const netIds = this.traceDensityNetIdsByLayer[layerId]! - if (netIds.size === 0) { - this.traceDensityActiveLayers[this.traceDensityActiveLayerCount++] = - layerId - } - netIds.add(netId) - } - } - protected getTraceDensityRegionDimension(regionId: RegionId): number { return Math.min( this.topology.regionWidth[regionId], @@ -780,36 +759,29 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } - protected computeTraceDensityCostForRegion( + protected computeProspectiveTraceDensityCostForRegion( regionId: RegionId, regionCache: RegionIntersectionCache, - prospectiveNetId?: NetId, - prospectiveLayerMask = 0, + prospectiveLayerMask: number, + sameNetLayerMask: number, ): number { - this.resetTraceDensityScratch() - - for (let i = 0; i < regionCache.netIds.length; i++) { - this.addNetToTraceDensityScratch( - regionCache.netIds[i], - regionCache.layerMasks[i], - ) - } - if (prospectiveNetId !== undefined) { - this.addNetToTraceDensityScratch(prospectiveNetId, prospectiveLayerMask) - } - - let maxDistinctNetCount = 0 - for (let i = 0; i < this.traceDensityActiveLayerCount; i++) { - const layerId = this.traceDensityActiveLayers[i] - maxDistinctNetCount = Math.max( - maxDistinctNetCount, - this.traceDensityNetIdsByLayer[layerId]!.size, + const shortRegionDimension = this.getTraceDensityRegionDimension(regionId) + let prospectiveMaxDistinctNetCount = regionCache.maxDistinctNetCount + for (let layerId = 0; layerId < 32; layerId++) { + const layerBit = 1 << layerId + if ((prospectiveLayerMask & layerBit) === 0) continue + + const prospectiveDistinctNetCount = + regionCache.distinctNetCountByLayer[layerId] + + ((sameNetLayerMask & layerBit) === 0 ? 1 : 0) + prospectiveMaxDistinctNetCount = Math.max( + prospectiveMaxDistinctNetCount, + prospectiveDistinctNetCount, ) } - return computeTraceDensityCost( - this.getTraceDensityRegionDimension(regionId), - maxDistinctNetCount, + shortRegionDimension, + prospectiveMaxDistinctNetCount, this.minTraceWidth, this.minTraceClearance, ) @@ -862,6 +834,7 @@ export class TinyHyperGraphSolver extends BaseSolver { newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, + sameNetLayerMask, ] = countNewIntersectionsWithValues( regionCache, state.currentRouteNetId!, @@ -896,31 +869,47 @@ export class TinyHyperGraphSolver extends BaseSolver { const existingEntryExitLayerChanges = regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges const existingSegmentCount = lesserAngles.length - const existingRegionCost = - this.computeRegionCostForRegion( - regionId, - existingSameLayerIntersections, - existingCrossingLayerIntersections, - existingEntryExitLayerChanges, - existingSegmentCount, - ) + - this.computeTraceDensityCostForRegion( - regionId, - regionCache, - state.currentRouteNetId!, - segmentGeometry.layerMask, - ) + const distinctNetCountByLayer = new Uint32Array( + regionCache.distinctNetCountByLayer, + ) + let maxDistinctNetCount = regionCache.maxDistinctNetCount + for (let layerId = 0; layerId < 32; layerId++) { + const layerBit = 1 << layerId + if ( + (segmentGeometry.layerMask & layerBit) === 0 || + (sameNetLayerMask & layerBit) !== 0 + ) { + continue + } + const distinctNetCount = ++distinctNetCountByLayer[layerId] + maxDistinctNetCount = Math.max(maxDistinctNetCount, distinctNetCount) + } state.regionIntersectionCaches[regionId] = { netIds, lesserAngles, greaterAngles, layerMasks, + distinctNetCountByLayer, + maxDistinctNetCount, existingSameLayerIntersections, existingCrossingLayerIntersections, existingEntryExitLayerChanges, existingSegmentCount, - existingRegionCost, + existingRegionCost: + this.computeRegionCostForRegion( + regionId, + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + existingSegmentCount, + ) + + computeTraceDensityCost( + this.getTraceDensityRegionDimension(regionId), + maxDistinctNetCount, + this.minTraceWidth, + this.minTraceClearance, + ), } } @@ -1472,6 +1461,7 @@ export class TinyHyperGraphSolver extends BaseSolver { newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, + sameNetLayerMask, ] = countNewIntersectionsWithValues( regionCache, state.currentRouteNetId!, @@ -1497,11 +1487,11 @@ export class TinyHyperGraphSolver extends BaseSolver { regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges, regionCache.existingSegmentCount + 1, ) + - this.computeTraceDensityCostForRegion( + this.computeProspectiveTraceDensityCostForRegion( nextRegionId, regionCache, - state.currentRouteNetId!, segmentGeometry.layerMask, + sameNetLayerMask, ) - regionCache.existingRegionCost diff --git a/lib/countNewIntersections.ts b/lib/countNewIntersections.ts index 355f8e2..7c76472 100644 --- a/lib/countNewIntersections.ts +++ b/lib/countNewIntersections.ts @@ -31,14 +31,18 @@ export const countNewIntersectionsWithValues = ( newGreaterAngle: number, newLayerMask: number, entryExitLayerChanges: number, -): [number, number, number] => { +): [number, number, number, number] => { const { netIds, lesserAngles, greaterAngles, layerMasks } = existingPairs let sameLayerIntersectionCount = 0 let crossingLayerIntersectionCount = 0 + let sameNetLayerMask = 0 for (let i = 0; i < netIds.length; i++) { - if (newNet === netIds[i]) continue + if (newNet === netIds[i]) { + sameNetLayerMask |= newLayerMask & layerMasks[i] + continue + } const lesserAngleIsInsideInterval = newLesserAngle < lesserAngles[i] && lesserAngles[i] < newGreaterAngle @@ -58,6 +62,7 @@ export const countNewIntersectionsWithValues = ( sameLayerIntersectionCount, crossingLayerIntersectionCount, entryExitLayerChanges, + sameNetLayerMask, ] } @@ -66,14 +71,16 @@ export const countNewIntersections = ( newPair: DynamicAnglePair, ): [number, number, number] => { const [newNet, newLesserAngle, newZ1, newGreaterAngle, newZ2] = newPair - return countNewIntersectionsWithValues( - existingPairs, - newNet, - newLesserAngle, - newGreaterAngle, - (1 << newZ1) | (1 << newZ2), - newZ1 !== newZ2 ? 1 : 0, - ) + const [sameLayerIntersections, crossLayerIntersections, layerChanges] = + countNewIntersectionsWithValues( + existingPairs, + newNet, + newLesserAngle, + newGreaterAngle, + (1 << newZ1) | (1 << newZ2), + newZ1 !== newZ2 ? 1 : 0, + ) + return [sameLayerIntersections, crossLayerIntersections, layerChanges] } export const countIntersectionsFromAnglePairsDynamic = countNewIntersections diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index 74ee65b..490edd8 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -107,6 +107,10 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + distinctNetCountByLayer: new Uint32Array( + regionIntersectionCache.distinctNetCountByLayer, + ), + maxDistinctNetCount: regionIntersectionCache.maxDistinctNetCount, existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: diff --git a/lib/types.ts b/lib/types.ts index b109000..e0b461a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -25,6 +25,8 @@ export interface DynamicAnglePairArrays { } export interface RegionIntersectionCache extends DynamicAnglePairArrays { + distinctNetCountByLayer: Uint32Array + maxDistinctNetCount: number existingSameLayerIntersections: Integer existingCrossingLayerIntersections: Integer existingEntryExitLayerChanges: Integer diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 16431f3..7837e18 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -15,6 +15,8 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + distinctNetCountByLayer: new Uint32Array(32), + maxDistinctNetCount: 0, existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, diff --git a/tests/solver/region-net-id.test.ts b/tests/solver/region-net-id.test.ts index 528bacf..26d004f 100644 --- a/tests/solver/region-net-id.test.ts +++ b/tests/solver/region-net-id.test.ts @@ -13,6 +13,8 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + distinctNetCountByLayer: new Uint32Array(32), + maxDistinctNetCount: 0, existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, diff --git a/tests/solver/trace-density-region-cost.test.ts b/tests/solver/trace-density-region-cost.test.ts index dae849a..3a34aac 100644 --- a/tests/solver/trace-density-region-cost.test.ts +++ b/tests/solver/trace-density-region-cost.test.ts @@ -19,7 +19,7 @@ const createSingleRegionTopology = (): TinyHyperGraphTopology => ({ regionCount: 1, regionIncidentPorts: [[0, 1, 2, 3, 4, 5, 6, 7]], incidentPortRegion: Array.from({ length: 8 }, () => [0]), - regionWidth: new Float64Array([1]), + regionWidth: new Float64Array([0.1]), regionHeight: new Float64Array([2]), regionCenterX: new Float64Array([0]), regionCenterY: new Float64Array([0]), @@ -51,7 +51,7 @@ test("trace density counts distinct nets only on shared layers", () => { { nextRegionId: 0, portId: 6, f: 0, g: 0, h: 0 }, 7, ) - expect(candidateCost).toBeCloseTo(0.04) + expect(candidateCost).toBeCloseTo(4) solver.appendSegmentToRegionCache(0, 6, 7) expect( @@ -68,7 +68,7 @@ test("trace density prefers wider regions for an additional net", () => { [4, 5, 6, 7], ], incidentPortRegion: [[0], [0], [0], [0], [1], [1], [1], [1]], - regionWidth: new Float64Array([0.5, 2]), + regionWidth: new Float64Array([0.2, 2]), regionHeight: new Float64Array([3, 3]), regionCenterX: new Float64Array(2), regionCenterY: new Float64Array(2), @@ -99,7 +99,7 @@ test("trace density prefers wider regions for an additional net", () => { 7, ) - expect(narrowRegionCost).toBeCloseTo(0.36) + expect(narrowRegionCost).toBeCloseTo(2.25) expect(wideRegionCost).toBeCloseTo(0.0225) expect(narrowRegionCost).toBeGreaterThan(wideRegionCost) }) From f4b157f57dd9c5f704f02cc936dffc749e9f0a4c Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Sat, 18 Jul 2026 15:04:30 +0200 Subject: [PATCH 3/6] Add configurable trace density cost factor --- lib/computeRegionCost.ts | 7 +++++- lib/core.ts | 11 +++++++++ lib/index.ts | 1 + lib/section-solver/index.ts | 2 ++ .../solver/trace-density-region-cost.test.ts | 23 +++++++++++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/computeRegionCost.ts b/lib/computeRegionCost.ts index 7380bdb..40bd971 100644 --- a/lib/computeRegionCost.ts +++ b/lib/computeRegionCost.ts @@ -1,6 +1,7 @@ export const DEFAULT_MIN_VIA_PAD_DIAMETER = 0.3 export const DEFAULT_MIN_TRACE_WIDTH = 0.1 export const DEFAULT_MIN_TRACE_CLEARANCE = 0.1 +export const DEFAULT_TRACE_DENSITY_COST_FACTOR = 1 export const TRACE_VIA_MARGIN = 0.15 const IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST = 10 @@ -9,12 +10,16 @@ export const computeTraceDensityCost = ( distinctNetCount: number, minTraceWidth = DEFAULT_MIN_TRACE_WIDTH, minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE, + traceDensityCostFactor = DEFAULT_TRACE_DENSITY_COST_FACTOR, ): number => { const additionalDistinctNets = Math.max(0, distinctNetCount - 1) if (additionalDistinctNets === 0) return 0 const tracePitch = minTraceWidth + minTraceClearance - return ((additionalDistinctNets * tracePitch) / shortRegionDimension) ** 2 + return ( + ((additionalDistinctNets * tracePitch) / shortRegionDimension) ** 2 * + traceDensityCostFactor + ) } export const isKnownSingleLayerMask = (regionAvailableZMask: number) => diff --git a/lib/core.ts b/lib/core.ts index 899606b..7d3b565 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -7,6 +7,7 @@ import { DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, + DEFAULT_TRACE_DENSITY_COST_FACTOR, isKnownSingleLayerMask, } from "./computeRegionCost" import { countNewIntersectionsWithValues } from "./countNewIntersections" @@ -238,6 +239,8 @@ export interface TinyHyperGraphSolverOptions { minTraceWidth?: number /** Minimum trace-to-trace clearance in millimeters. Defaults to 0.1mm. */ minTraceClearance?: number + /** Multiplier applied to the trace-density routing cost. Defaults to 1. */ + TRACE_DENSITY_COST_FACTOR?: number DISTANCE_TO_COST?: number RIP_THRESHOLD_START?: number RIP_THRESHOLD_END?: number @@ -257,6 +260,7 @@ export interface TinyHyperGraphSolverOptionTarget { minViaPadDiameter: number minTraceWidth: number minTraceClearance: number + TRACE_DENSITY_COST_FACTOR: number DISTANCE_TO_COST: number RIP_THRESHOLD_START: number RIP_THRESHOLD_END: number @@ -289,6 +293,9 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.minTraceClearance !== undefined) { solver.minTraceClearance = options.minTraceClearance } + if (options.TRACE_DENSITY_COST_FACTOR !== undefined) { + solver.TRACE_DENSITY_COST_FACTOR = options.TRACE_DENSITY_COST_FACTOR + } if (options.DISTANCE_TO_COST !== undefined) { solver.DISTANCE_TO_COST = options.DISTANCE_TO_COST } @@ -339,6 +346,7 @@ export const getTinyHyperGraphSolverOptions = ( minViaPadDiameter: solver.minViaPadDiameter, minTraceWidth: solver.minTraceWidth, minTraceClearance: solver.minTraceClearance, + TRACE_DENSITY_COST_FACTOR: solver.TRACE_DENSITY_COST_FACTOR, DISTANCE_TO_COST: solver.DISTANCE_TO_COST, RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, @@ -384,6 +392,7 @@ export class TinyHyperGraphSolver extends BaseSolver { minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE + TRACE_DENSITY_COST_FACTOR = DEFAULT_TRACE_DENSITY_COST_FACTOR RIP_THRESHOLD_START = 0.05 RIP_THRESHOLD_END = 0.8 @@ -784,6 +793,7 @@ export class TinyHyperGraphSolver extends BaseSolver { prospectiveMaxDistinctNetCount, this.minTraceWidth, this.minTraceClearance, + this.TRACE_DENSITY_COST_FACTOR, ) } @@ -909,6 +919,7 @@ export class TinyHyperGraphSolver extends BaseSolver { maxDistinctNetCount, this.minTraceWidth, this.minTraceClearance, + this.TRACE_DENSITY_COST_FACTOR, ), } } diff --git a/lib/index.ts b/lib/index.ts index 799e6fb..16add11 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -12,6 +12,7 @@ export { DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, + DEFAULT_TRACE_DENSITY_COST_FACTOR, TRACE_VIA_MARGIN, } from "./computeRegionCost" export { convertPortPointPathingSolverInputToSerializedHyperGraph } from "./compat/convertPortPointPathingSolverInputToSerializedHyperGraph" diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index 490edd8..65643fc 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -15,6 +15,7 @@ import { DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, + DEFAULT_TRACE_DENSITY_COST_FACTOR, } from "../computeRegionCost" import { shuffle } from "../shuffle" import type { @@ -891,6 +892,7 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE + TRACE_DENSITY_COST_FACTOR = DEFAULT_TRACE_DENSITY_COST_FACTOR VERBOSE = false RIP_THRESHOLD_START = 0.05 diff --git a/tests/solver/trace-density-region-cost.test.ts b/tests/solver/trace-density-region-cost.test.ts index 3a34aac..4e483fa 100644 --- a/tests/solver/trace-density-region-cost.test.ts +++ b/tests/solver/trace-density-region-cost.test.ts @@ -103,3 +103,26 @@ test("trace density prefers wider regions for an additional net", () => { expect(wideRegionCost).toBeCloseTo(0.0225) expect(narrowRegionCost).toBeGreaterThan(wideRegionCost) }) + +test("trace density cost factor scales prospective and cached costs", () => { + const solver = new TinyHyperGraphSolver( + createSingleRegionTopology(), + createProblem(8), + { TRACE_DENSITY_COST_FACTOR: 0.1 }, + ) + + solver.state.currentRouteNetId = 0 + solver.appendSegmentToRegionCache(0, 0, 1) + + solver.state.currentRouteNetId = 1 + const candidateCost = solver.computeG( + { nextRegionId: 0, portId: 2, f: 0, g: 0, h: 0 }, + 3, + ) + expect(candidateCost).toBeCloseTo(0.4) + + solver.appendSegmentToRegionCache(0, 2, 3) + expect( + solver.state.regionIntersectionCaches[0].existingRegionCost, + ).toBeCloseTo(candidateCost) +}) From 70bae0d858348618341544e90d240694ae1c4c51 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Sun, 19 Jul 2026 08:41:34 +0200 Subject: [PATCH 4/6] Adapt trace density to optimized cost path --- lib/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core.ts b/lib/core.ts index 89ba2a1..604c6d4 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -1531,7 +1531,7 @@ export class TinyHyperGraphSolver extends BaseSolver { this.computeProspectiveTraceDensityCostForRegion( nextRegionId, regionCache, - segmentGeometry.layerMask, + layerMask, sameNetLayerMask, ) - regionCache.existingRegionCost From 2af2791b68d72ea39aef82ad31b182cfee939626 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Mon, 20 Jul 2026 08:59:20 +0200 Subject: [PATCH 5/6] Narrow trace density change to routing dimensions --- lib/bus-solver/TinyHyperGraphBusSolver.ts | 2 - lib/bus-solver/previewRoutingState.ts | 4 - lib/computeRegionCost.ts | 20 +-- lib/core.ts | 104 ++------------ lib/countNewIntersections.ts | 27 ++-- lib/index.ts | 3 +- lib/poly.ts | 12 -- lib/section-solver/index.ts | 6 - lib/types.ts | 2 - tests/solver/on-all-routes-routed.test.ts | 2 - tests/solver/region-net-id.test.ts | 2 - .../solver/trace-density-region-cost.test.ts | 128 ------------------ tests/solver/trace-pitch.test.ts | 52 +++++++ 13 files changed, 80 insertions(+), 284 deletions(-) delete mode 100644 tests/solver/trace-density-region-cost.test.ts create mode 100644 tests/solver/trace-pitch.test.ts diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 5111293..463034b 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -1459,8 +1459,6 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { regionCache.lesserAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.greaterAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.layerMasks = EMPTY_PREVIEW_INT32_ARRAY - regionCache.distinctNetCountByLayer.fill(0) - regionCache.maxDistinctNetCount = 0 regionCache.existingCrossingLayerIntersections = 0 regionCache.existingSameLayerIntersections = 0 regionCache.existingEntryExitLayerChanges = 0 diff --git a/lib/bus-solver/previewRoutingState.ts b/lib/bus-solver/previewRoutingState.ts index e44e343..c537b75 100644 --- a/lib/bus-solver/previewRoutingState.ts +++ b/lib/bus-solver/previewRoutingState.ts @@ -58,8 +58,6 @@ export const snapshotPreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), - distinctNetCountByLayer: new Uint32Array(cache.distinctNetCountByLayer), - maxDistinctNetCount: cache.maxDistinctNetCount, existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, @@ -83,8 +81,6 @@ export const restorePreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), - distinctNetCountByLayer: new Uint32Array(cache.distinctNetCountByLayer), - maxDistinctNetCount: cache.maxDistinctNetCount, existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, diff --git a/lib/computeRegionCost.ts b/lib/computeRegionCost.ts index 40bd971..537f590 100644 --- a/lib/computeRegionCost.ts +++ b/lib/computeRegionCost.ts @@ -1,26 +1,16 @@ export const DEFAULT_MIN_VIA_PAD_DIAMETER = 0.3 +/** Default minimum routed trace width, in millimeters. */ export const DEFAULT_MIN_TRACE_WIDTH = 0.1 +/** Default minimum trace-to-trace clearance, in millimeters. */ export const DEFAULT_MIN_TRACE_CLEARANCE = 0.1 -export const DEFAULT_TRACE_DENSITY_COST_FACTOR = 1 export const TRACE_VIA_MARGIN = 0.15 const IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST = 10 -export const computeTraceDensityCost = ( - shortRegionDimension: number, - distinctNetCount: number, +/** Derives center-to-center trace pitch from width and clearance, in mm. */ +export const computeTracePitch = ( minTraceWidth = DEFAULT_MIN_TRACE_WIDTH, minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE, - traceDensityCostFactor = DEFAULT_TRACE_DENSITY_COST_FACTOR, -): number => { - const additionalDistinctNets = Math.max(0, distinctNetCount - 1) - if (additionalDistinctNets === 0) return 0 - - const tracePitch = minTraceWidth + minTraceClearance - return ( - ((additionalDistinctNets * tracePitch) / shortRegionDimension) ** 2 * - traceDensityCostFactor - ) -} +): number => minTraceWidth + minTraceClearance export const isKnownSingleLayerMask = (regionAvailableZMask: number) => regionAvailableZMask > 0 && diff --git a/lib/core.ts b/lib/core.ts index 604c6d4..801dea5 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -2,12 +2,11 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { convertToSerializedHyperGraph } from "./compat/convertToSerializedHyperGraph" import { - computeTraceDensityCost, computeRegionCost, + computeTracePitch, DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, - DEFAULT_TRACE_DENSITY_COST_FACTOR, isKnownSingleLayerMask, } from "./computeRegionCost" import { countNewIntersectionsWithValues } from "./countNewIntersections" @@ -40,8 +39,6 @@ export const createEmptyRegionIntersectionCache = lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), - distinctNetCountByLayer: new Uint32Array(32), - maxDistinctNetCount: 0, existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -66,10 +63,6 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), - distinctNetCountByLayer: new Uint32Array( - regionIntersectionCache.distinctNetCountByLayer, - ), - maxDistinctNetCount: regionIntersectionCache.maxDistinctNetCount, existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -241,8 +234,6 @@ export interface TinyHyperGraphSolverOptions { minTraceWidth?: number /** Minimum trace-to-trace clearance in millimeters. Defaults to 0.1mm. */ minTraceClearance?: number - /** Multiplier applied to the trace-density routing cost. Defaults to 1. */ - TRACE_DENSITY_COST_FACTOR?: number DISTANCE_TO_COST?: number RIP_THRESHOLD_START?: number RIP_THRESHOLD_END?: number @@ -262,7 +253,6 @@ export interface TinyHyperGraphSolverOptionTarget { minViaPadDiameter: number minTraceWidth: number minTraceClearance: number - TRACE_DENSITY_COST_FACTOR: number DISTANCE_TO_COST: number RIP_THRESHOLD_START: number RIP_THRESHOLD_END: number @@ -295,9 +285,6 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.minTraceClearance !== undefined) { solver.minTraceClearance = options.minTraceClearance } - if (options.TRACE_DENSITY_COST_FACTOR !== undefined) { - solver.TRACE_DENSITY_COST_FACTOR = options.TRACE_DENSITY_COST_FACTOR - } if (options.DISTANCE_TO_COST !== undefined) { solver.DISTANCE_TO_COST = options.DISTANCE_TO_COST } @@ -348,7 +335,6 @@ export const getTinyHyperGraphSolverOptions = ( minViaPadDiameter: solver.minViaPadDiameter, minTraceWidth: solver.minTraceWidth, minTraceClearance: solver.minTraceClearance, - TRACE_DENSITY_COST_FACTOR: solver.TRACE_DENSITY_COST_FACTOR, DISTANCE_TO_COST: solver.DISTANCE_TO_COST, RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, @@ -394,7 +380,6 @@ export class TinyHyperGraphSolver extends BaseSolver { minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE - TRACE_DENSITY_COST_FACTOR = DEFAULT_TRACE_DENSITY_COST_FACTOR RIP_THRESHOLD_START = 0.05 RIP_THRESHOLD_END = 0.8 @@ -767,40 +752,9 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } - protected getTraceDensityRegionDimension(regionId: RegionId): number { - return Math.min( - this.topology.regionWidth[regionId], - this.topology.regionHeight[regionId], - ) - } - - protected computeProspectiveTraceDensityCostForRegion( - regionId: RegionId, - regionCache: RegionIntersectionCache, - prospectiveLayerMask: number, - sameNetLayerMask: number, - ): number { - const shortRegionDimension = this.getTraceDensityRegionDimension(regionId) - let prospectiveMaxDistinctNetCount = regionCache.maxDistinctNetCount - for (let layerId = 0; layerId < 32; layerId++) { - const layerBit = 1 << layerId - if ((prospectiveLayerMask & layerBit) === 0) continue - - const prospectiveDistinctNetCount = - regionCache.distinctNetCountByLayer[layerId] + - ((sameNetLayerMask & layerBit) === 0 ? 1 : 0) - prospectiveMaxDistinctNetCount = Math.max( - prospectiveMaxDistinctNetCount, - prospectiveDistinctNetCount, - ) - } - return computeTraceDensityCost( - shortRegionDimension, - prospectiveMaxDistinctNetCount, - this.minTraceWidth, - this.minTraceClearance, - this.TRACE_DENSITY_COST_FACTOR, - ) + /** Returns the configured center-to-center routed trace pitch, in mm. */ + getTracePitch(): number { + return computeTracePitch(this.minTraceWidth, this.minTraceClearance) } populateSegmentGeometryScratch( @@ -850,7 +804,6 @@ export class TinyHyperGraphSolver extends BaseSolver { newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, - sameNetLayerMask, ] = countNewIntersectionsWithValues( regionCache, state.currentRouteNetId!, @@ -885,48 +838,23 @@ export class TinyHyperGraphSolver extends BaseSolver { const existingEntryExitLayerChanges = regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges const existingSegmentCount = lesserAngles.length - const distinctNetCountByLayer = new Uint32Array( - regionCache.distinctNetCountByLayer, - ) - let maxDistinctNetCount = regionCache.maxDistinctNetCount - for (let layerId = 0; layerId < 32; layerId++) { - const layerBit = 1 << layerId - if ( - (segmentGeometry.layerMask & layerBit) === 0 || - (sameNetLayerMask & layerBit) !== 0 - ) { - continue - } - const distinctNetCount = ++distinctNetCountByLayer[layerId] - maxDistinctNetCount = Math.max(maxDistinctNetCount, distinctNetCount) - } state.regionIntersectionCaches[regionId] = { netIds, lesserAngles, greaterAngles, layerMasks, - distinctNetCountByLayer, - maxDistinctNetCount, existingSameLayerIntersections, existingCrossingLayerIntersections, existingEntryExitLayerChanges, existingSegmentCount, - existingRegionCost: - this.computeRegionCostForRegion( - regionId, - existingSameLayerIntersections, - existingCrossingLayerIntersections, - existingEntryExitLayerChanges, - existingSegmentCount, - ) + - computeTraceDensityCost( - this.getTraceDensityRegionDimension(regionId), - maxDistinctNetCount, - this.minTraceWidth, - this.minTraceClearance, - this.TRACE_DENSITY_COST_FACTOR, - ), + existingRegionCost: this.computeRegionCostForRegion( + regionId, + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + existingSegmentCount, + ), } } @@ -1502,7 +1430,6 @@ export class TinyHyperGraphSolver extends BaseSolver { newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, - sameNetLayerMask, ] = countNewIntersectionsWithValues( regionCache, state.currentRouteNetId!, @@ -1527,14 +1454,7 @@ export class TinyHyperGraphSolver extends BaseSolver { newCrossLayerIntersections, regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges, regionCache.existingSegmentCount + 1, - ) + - this.computeProspectiveTraceDensityCostForRegion( - nextRegionId, - regionCache, - layerMask, - sameNetLayerMask, - ) - - regionCache.existingRegionCost + ) - regionCache.existingRegionCost return ( currentCandidate.g + diff --git a/lib/countNewIntersections.ts b/lib/countNewIntersections.ts index 7c76472..355f8e2 100644 --- a/lib/countNewIntersections.ts +++ b/lib/countNewIntersections.ts @@ -31,18 +31,14 @@ export const countNewIntersectionsWithValues = ( newGreaterAngle: number, newLayerMask: number, entryExitLayerChanges: number, -): [number, number, number, number] => { +): [number, number, number] => { const { netIds, lesserAngles, greaterAngles, layerMasks } = existingPairs let sameLayerIntersectionCount = 0 let crossingLayerIntersectionCount = 0 - let sameNetLayerMask = 0 for (let i = 0; i < netIds.length; i++) { - if (newNet === netIds[i]) { - sameNetLayerMask |= newLayerMask & layerMasks[i] - continue - } + if (newNet === netIds[i]) continue const lesserAngleIsInsideInterval = newLesserAngle < lesserAngles[i] && lesserAngles[i] < newGreaterAngle @@ -62,7 +58,6 @@ export const countNewIntersectionsWithValues = ( sameLayerIntersectionCount, crossingLayerIntersectionCount, entryExitLayerChanges, - sameNetLayerMask, ] } @@ -71,16 +66,14 @@ export const countNewIntersections = ( newPair: DynamicAnglePair, ): [number, number, number] => { const [newNet, newLesserAngle, newZ1, newGreaterAngle, newZ2] = newPair - const [sameLayerIntersections, crossLayerIntersections, layerChanges] = - countNewIntersectionsWithValues( - existingPairs, - newNet, - newLesserAngle, - newGreaterAngle, - (1 << newZ1) | (1 << newZ2), - newZ1 !== newZ2 ? 1 : 0, - ) - return [sameLayerIntersections, crossLayerIntersections, layerChanges] + return countNewIntersectionsWithValues( + existingPairs, + newNet, + newLesserAngle, + newGreaterAngle, + (1 << newZ1) | (1 << newZ2), + newZ1 !== newZ2 ? 1 : 0, + ) } export const countIntersectionsFromAnglePairsDynamic = countNewIntersections diff --git a/lib/index.ts b/lib/index.ts index 16add11..421d5ad 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,11 +8,10 @@ export * from "./selective-rerip-tiny-hyper-graph-solver" export * from "./bus-solver" export * from "./region-graph" export { - computeTraceDensityCost, + computeTracePitch, DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, - DEFAULT_TRACE_DENSITY_COST_FACTOR, TRACE_VIA_MARGIN, } from "./computeRegionCost" export { convertPortPointPathingSolverInputToSerializedHyperGraph } from "./compat/convertPortPointPathingSolverInputToSerializedHyperGraph" diff --git a/lib/poly.ts b/lib/poly.ts index a54aabb..5cfbda0 100644 --- a/lib/poly.ts +++ b/lib/poly.ts @@ -676,18 +676,6 @@ export class PolyHyperGraphSolver extends TinyHyperGraphSolver { ) } - protected override getTraceDensityRegionDimension( - regionId: RegionId, - ): number { - const width = - this.topology.regionBoundsMaxX[regionId] - - this.topology.regionBoundsMinX[regionId] - const height = - this.topology.regionBoundsMaxY[regionId] - - this.topology.regionBoundsMinY[regionId] - return Math.min(width, height) - } - override visualize(): GraphicsObject { return visualizePolyHyperGraph(this) } diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index 65643fc..74ee65b 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -15,7 +15,6 @@ import { DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, - DEFAULT_TRACE_DENSITY_COST_FACTOR, } from "../computeRegionCost" import { shuffle } from "../shuffle" import type { @@ -108,10 +107,6 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), - distinctNetCountByLayer: new Uint32Array( - regionIntersectionCache.distinctNetCountByLayer, - ), - maxDistinctNetCount: regionIntersectionCache.maxDistinctNetCount, existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -892,7 +887,6 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE - TRACE_DENSITY_COST_FACTOR = DEFAULT_TRACE_DENSITY_COST_FACTOR VERBOSE = false RIP_THRESHOLD_START = 0.05 diff --git a/lib/types.ts b/lib/types.ts index e0b461a..b109000 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -25,8 +25,6 @@ export interface DynamicAnglePairArrays { } export interface RegionIntersectionCache extends DynamicAnglePairArrays { - distinctNetCountByLayer: Uint32Array - maxDistinctNetCount: number existingSameLayerIntersections: Integer existingCrossingLayerIntersections: Integer existingEntryExitLayerChanges: Integer diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 7837e18..16431f3 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -15,8 +15,6 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), - distinctNetCountByLayer: new Uint32Array(32), - maxDistinctNetCount: 0, existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, diff --git a/tests/solver/region-net-id.test.ts b/tests/solver/region-net-id.test.ts index 26d004f..528bacf 100644 --- a/tests/solver/region-net-id.test.ts +++ b/tests/solver/region-net-id.test.ts @@ -13,8 +13,6 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), - distinctNetCountByLayer: new Uint32Array(32), - maxDistinctNetCount: 0, existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, diff --git a/tests/solver/trace-density-region-cost.test.ts b/tests/solver/trace-density-region-cost.test.ts deleted file mode 100644 index 4e483fa..0000000 --- a/tests/solver/trace-density-region-cost.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { expect, test } from "bun:test" -import { - type TinyHyperGraphProblem, - TinyHyperGraphSolver, - type TinyHyperGraphTopology, -} from "lib/index" - -const createProblem = (portCount: number): TinyHyperGraphProblem => ({ - routeCount: 3, - portSectionMask: new Int8Array(portCount).fill(1), - routeStartPort: new Int32Array([0, 2, 4]), - routeEndPort: new Int32Array([1, 3, 5]), - routeNet: new Int32Array([0, 1, 2]), - regionNetId: new Int32Array(2).fill(-1), -}) - -const createSingleRegionTopology = (): TinyHyperGraphTopology => ({ - portCount: 8, - regionCount: 1, - regionIncidentPorts: [[0, 1, 2, 3, 4, 5, 6, 7]], - incidentPortRegion: Array.from({ length: 8 }, () => [0]), - regionWidth: new Float64Array([0.1]), - regionHeight: new Float64Array([2]), - regionCenterX: new Float64Array([0]), - regionCenterY: new Float64Array([0]), - regionAvailableZMask: new Int32Array([3]), - portAngleForRegion1: new Int32Array([ - 0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, - ]), - portX: new Float64Array(8), - portY: new Float64Array(8), - portZ: new Int32Array([0, 0, 0, 0, 1, 1, 0, 0]), -}) - -test("trace density counts distinct nets only on shared layers", () => { - const solver = new TinyHyperGraphSolver( - createSingleRegionTopology(), - createProblem(8), - ) - - solver.state.currentRouteNetId = 0 - solver.appendSegmentToRegionCache(0, 0, 1) - solver.appendSegmentToRegionCache(0, 2, 3) - - solver.state.currentRouteNetId = 1 - solver.appendSegmentToRegionCache(0, 4, 5) - expect(solver.state.regionIntersectionCaches[0].existingRegionCost).toBe(0) - - solver.state.currentRouteNetId = 2 - const candidateCost = solver.computeG( - { nextRegionId: 0, portId: 6, f: 0, g: 0, h: 0 }, - 7, - ) - expect(candidateCost).toBeCloseTo(4) - - solver.appendSegmentToRegionCache(0, 6, 7) - expect( - solver.state.regionIntersectionCaches[0].existingRegionCost, - ).toBeCloseTo(candidateCost) -}) - -test("trace density prefers wider regions for an additional net", () => { - const topology: TinyHyperGraphTopology = { - portCount: 8, - regionCount: 2, - regionIncidentPorts: [ - [0, 1, 2, 3], - [4, 5, 6, 7], - ], - incidentPortRegion: [[0], [0], [0], [0], [1], [1], [1], [1]], - regionWidth: new Float64Array([0.2, 2]), - regionHeight: new Float64Array([3, 3]), - regionCenterX: new Float64Array(2), - regionCenterY: new Float64Array(2), - regionAvailableZMask: new Int32Array([1, 1]), - portAngleForRegion1: new Int32Array([ - 0, 1000, 2000, 3000, 0, 1000, 2000, 3000, - ]), - portX: new Float64Array(8), - portY: new Float64Array(8), - portZ: new Int32Array(8), - } - const solver = new TinyHyperGraphSolver(topology, createProblem(8), { - minTraceWidth: 0.2, - minTraceClearance: 0.1, - }) - - solver.state.currentRouteNetId = 0 - solver.appendSegmentToRegionCache(0, 0, 1) - solver.appendSegmentToRegionCache(1, 4, 5) - - solver.state.currentRouteNetId = 1 - const narrowRegionCost = solver.computeG( - { nextRegionId: 0, portId: 2, f: 0, g: 0, h: 0 }, - 3, - ) - const wideRegionCost = solver.computeG( - { nextRegionId: 1, portId: 6, f: 0, g: 0, h: 0 }, - 7, - ) - - expect(narrowRegionCost).toBeCloseTo(2.25) - expect(wideRegionCost).toBeCloseTo(0.0225) - expect(narrowRegionCost).toBeGreaterThan(wideRegionCost) -}) - -test("trace density cost factor scales prospective and cached costs", () => { - const solver = new TinyHyperGraphSolver( - createSingleRegionTopology(), - createProblem(8), - { TRACE_DENSITY_COST_FACTOR: 0.1 }, - ) - - solver.state.currentRouteNetId = 0 - solver.appendSegmentToRegionCache(0, 0, 1) - - solver.state.currentRouteNetId = 1 - const candidateCost = solver.computeG( - { nextRegionId: 0, portId: 2, f: 0, g: 0, h: 0 }, - 3, - ) - expect(candidateCost).toBeCloseTo(0.4) - - solver.appendSegmentToRegionCache(0, 2, 3) - expect( - solver.state.regionIntersectionCaches[0].existingRegionCost, - ).toBeCloseTo(candidateCost) -}) diff --git a/tests/solver/trace-pitch.test.ts b/tests/solver/trace-pitch.test.ts new file mode 100644 index 0000000..70c8fbe --- /dev/null +++ b/tests/solver/trace-pitch.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test" +import { + computeTracePitch, + DEFAULT_MIN_TRACE_CLEARANCE, + DEFAULT_MIN_TRACE_WIDTH, + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, +} from "lib/index" + +const topology: TinyHyperGraphTopology = { + portCount: 0, + regionCount: 0, + regionIncidentPorts: [], + incidentPortRegion: [], + regionWidth: new Float64Array(0), + regionHeight: new Float64Array(0), + regionCenterX: new Float64Array(0), + regionCenterY: new Float64Array(0), + portAngleForRegion1: new Int32Array(0), + portX: new Float64Array(0), + portY: new Float64Array(0), + portZ: new Int32Array(0), +} + +const problem: TinyHyperGraphProblem = { + routeCount: 0, + portSectionMask: new Int8Array(0), + routeStartPort: new Int32Array(0), + routeEndPort: new Int32Array(0), + routeNet: new Int32Array(0), + regionNetId: new Int32Array(0), +} + +test("trace pitch defaults to 0.1mm width plus 0.1mm clearance", () => { + expect(DEFAULT_MIN_TRACE_WIDTH).toBe(0.1) + expect(DEFAULT_MIN_TRACE_CLEARANCE).toBe(0.1) + expect(computeTracePitch()).toBeCloseTo(0.2) + expect( + new TinyHyperGraphSolver(topology, problem).getTracePitch(), + ).toBeCloseTo(0.2) +}) + +test("trace pitch is derived from configured width and clearance", () => { + expect(computeTracePitch(0.15, 0.22)).toBeCloseTo(0.37) + expect( + new TinyHyperGraphSolver(topology, problem, { + minTraceWidth: 0.15, + minTraceClearance: 0.22, + }).getTracePitch(), + ).toBeCloseTo(0.37) +}) From b28456ce679937de65618051de15855427f59413 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Mon, 20 Jul 2026 17:38:37 +0200 Subject: [PATCH 6/6] Model shared trace occupancy after baseline routing --- lib/bus-solver/TinyHyperGraphBusSolver.ts | 2 + lib/bus-solver/previewRoutingState.ts | 8 + lib/computeRegionCost.ts | 31 +++- lib/core.ts | 172 ++++++++++++++++-- lib/index.ts | 2 +- lib/poly.ts | 10 + lib/section-solver/index.ts | 8 + lib/types.ts | 2 + tests/solver/on-all-routes-routed.test.ts | 2 + tests/solver/region-net-id.test.ts | 2 + .../solver/trace-occupancy-activation.test.ts | 42 +++++ tests/solver/trace-occupancy-cost.test.ts | 10 + tests/solver/trace-pitch.test.ts | 52 ------ 13 files changed, 273 insertions(+), 70 deletions(-) create mode 100644 tests/solver/trace-occupancy-activation.test.ts create mode 100644 tests/solver/trace-occupancy-cost.test.ts delete mode 100644 tests/solver/trace-pitch.test.ts diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 463034b..18c9994 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -1459,6 +1459,8 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { regionCache.lesserAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.greaterAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.layerMasks = EMPTY_PREVIEW_INT32_ARRAY + regionCache.traceLengthByLayer.fill(0) + regionCache.longestTraceLengthByLayer.fill(0) regionCache.existingCrossingLayerIntersections = 0 regionCache.existingSameLayerIntersections = 0 regionCache.existingEntryExitLayerChanges = 0 diff --git a/lib/bus-solver/previewRoutingState.ts b/lib/bus-solver/previewRoutingState.ts index c537b75..9675796 100644 --- a/lib/bus-solver/previewRoutingState.ts +++ b/lib/bus-solver/previewRoutingState.ts @@ -58,6 +58,10 @@ export const snapshotPreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), + traceLengthByLayer: new Float64Array(cache.traceLengthByLayer), + longestTraceLengthByLayer: new Float64Array( + cache.longestTraceLengthByLayer, + ), existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, @@ -81,6 +85,10 @@ export const restorePreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), + traceLengthByLayer: new Float64Array(cache.traceLengthByLayer), + longestTraceLengthByLayer: new Float64Array( + cache.longestTraceLengthByLayer, + ), existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, diff --git a/lib/computeRegionCost.ts b/lib/computeRegionCost.ts index 537f590..e1a1d81 100644 --- a/lib/computeRegionCost.ts +++ b/lib/computeRegionCost.ts @@ -1,16 +1,37 @@ export const DEFAULT_MIN_VIA_PAD_DIAMETER = 0.3 -/** Default minimum routed trace width, in millimeters. */ export const DEFAULT_MIN_TRACE_WIDTH = 0.1 -/** Default minimum trace-to-trace clearance, in millimeters. */ export const DEFAULT_MIN_TRACE_CLEARANCE = 0.1 export const TRACE_VIA_MARGIN = 0.15 +const traceWidth = 0.1 const IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST = 10 -/** Derives center-to-center trace pitch from width and clearance, in mm. */ -export const computeTracePitch = ( +/** + * Estimates per-layer copper occupancy as swept trace area divided by region + * area. This cost is used only by post-solution optimization, where a complete + * baseline can always be restored, so utilization can remain physically + * meaningful without weakening initial-route completeness. + */ +export const computeTraceOccupancyCost = ( + regionArea: number, + traceLengthByLayer: ArrayLike, + longestTraceLengthByLayer: ArrayLike, minTraceWidth = DEFAULT_MIN_TRACE_WIDTH, minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE, -): number => minTraceWidth + minTraceClearance +): number => { + let maxSharedTraceLength = 0 + for (let layerId = 0; layerId < traceLengthByLayer.length; layerId++) { + const sharedTraceLength = Math.max( + 0, + (traceLengthByLayer[layerId] ?? 0) - + (longestTraceLengthByLayer[layerId] ?? 0), + ) + maxSharedTraceLength = Math.max(maxSharedTraceLength, sharedTraceLength) + } + + const tracePitch = minTraceWidth + minTraceClearance + const sharedTraceArea = maxSharedTraceLength * tracePitch + return (sharedTraceArea / regionArea) ** 2 +} export const isKnownSingleLayerMask = (regionAvailableZMask: number) => regionAvailableZMask > 0 && diff --git a/lib/core.ts b/lib/core.ts index 801dea5..71793f3 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -2,8 +2,8 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { convertToSerializedHyperGraph } from "./compat/convertToSerializedHyperGraph" import { + computeTraceOccupancyCost, computeRegionCost, - computeTracePitch, DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, @@ -33,12 +33,37 @@ export type { StaticallyUnroutableRouteSummary } from "./static-reachability" const GREEDY_FINAL_ROUTE_MAX_ITERATIONS = 50e3 +const addSegmentLengthByLayer = ( + traceLengthByLayer: Float64Array, + longestTraceLengthByLayer: Float64Array, + layerMask: number, + segmentLength: number, +) => { + let layerCount = 0 + for (let layerId = 0; layerId < 32; layerId++) { + if ((layerMask & (1 << layerId)) !== 0) layerCount += 1 + } + if (layerCount === 0) return + + const lengthOnLayer = segmentLength / layerCount + for (let layerId = 0; layerId < 32; layerId++) { + if ((layerMask & (1 << layerId)) === 0) continue + traceLengthByLayer[layerId] += lengthOnLayer + longestTraceLengthByLayer[layerId] = Math.max( + longestTraceLengthByLayer[layerId], + lengthOnLayer, + ) + } +} + export const createEmptyRegionIntersectionCache = (): RegionIntersectionCache => ({ netIds: new Int32Array(0), lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + traceLengthByLayer: new Float64Array(32), + longestTraceLengthByLayer: new Float64Array(32), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -63,6 +88,12 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + traceLengthByLayer: new Float64Array( + regionIntersectionCache.traceLengthByLayer, + ), + longestTraceLengthByLayer: new Float64Array( + regionIntersectionCache.longestTraceLengthByLayer, + ), existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -234,6 +265,8 @@ export interface TinyHyperGraphSolverOptions { minTraceWidth?: number /** Minimum trace-to-trace clearance in millimeters. Defaults to 0.1mm. */ minTraceClearance?: number + /** Enables physical trace-occupancy scoring during solution optimization. */ + USE_TRACE_OCCUPANCY_COST?: boolean DISTANCE_TO_COST?: number RIP_THRESHOLD_START?: number RIP_THRESHOLD_END?: number @@ -253,6 +286,7 @@ export interface TinyHyperGraphSolverOptionTarget { minViaPadDiameter: number minTraceWidth: number minTraceClearance: number + USE_TRACE_OCCUPANCY_COST: boolean DISTANCE_TO_COST: number RIP_THRESHOLD_START: number RIP_THRESHOLD_END: number @@ -285,6 +319,9 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.minTraceClearance !== undefined) { solver.minTraceClearance = options.minTraceClearance } + if (options.USE_TRACE_OCCUPANCY_COST !== undefined) { + solver.USE_TRACE_OCCUPANCY_COST = options.USE_TRACE_OCCUPANCY_COST + } if (options.DISTANCE_TO_COST !== undefined) { solver.DISTANCE_TO_COST = options.DISTANCE_TO_COST } @@ -335,6 +372,7 @@ export const getTinyHyperGraphSolverOptions = ( minViaPadDiameter: solver.minViaPadDiameter, minTraceWidth: solver.minTraceWidth, minTraceClearance: solver.minTraceClearance, + USE_TRACE_OCCUPANCY_COST: solver.USE_TRACE_OCCUPANCY_COST, DISTANCE_TO_COST: solver.DISTANCE_TO_COST, RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, @@ -359,6 +397,7 @@ interface SegmentGeometryScratch { greaterAngle: number layerMask: number entryExitLayerChanges: number + length: number } export class TinyHyperGraphSolver extends BaseSolver { @@ -375,11 +414,17 @@ export class TinyHyperGraphSolver extends BaseSolver { greaterAngle: 0, layerMask: 0, entryExitLayerChanges: 0, + length: 0, } + private traceLengthByLayerScratch = new Float64Array(32) + private longestTraceLengthByLayerScratch = new Float64Array(32) + private traceOccupancyCostActive = false + DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE + USE_TRACE_OCCUPANCY_COST = true RIP_THRESHOLD_START = 0.05 RIP_THRESHOLD_END = 0.8 @@ -752,9 +797,52 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } - /** Returns the configured center-to-center routed trace pitch, in mm. */ - getTracePitch(): number { - return computeTracePitch(this.minTraceWidth, this.minTraceClearance) + protected getRegionArea(regionId: RegionId): number { + return ( + this.topology.regionWidth[regionId] * this.topology.regionHeight[regionId] + ) + } + + protected computeTraceOccupancyCostForRegion( + regionId: RegionId, + traceLengthByLayer: ArrayLike, + longestTraceLengthByLayer: ArrayLike, + ): number { + if (!this.USE_TRACE_OCCUPANCY_COST || !this.traceOccupancyCostActive) { + return 0 + } + return computeTraceOccupancyCost( + this.getRegionArea(regionId), + traceLengthByLayer, + longestTraceLengthByLayer, + this.minTraceWidth, + this.minTraceClearance, + ) + } + + protected computeProspectiveTraceOccupancyCostForRegion( + regionId: RegionId, + regionCache: RegionIntersectionCache, + segmentGeometry: SegmentGeometryScratch, + ): number { + if (!this.USE_TRACE_OCCUPANCY_COST || !this.traceOccupancyCostActive) { + return 0 + } + this.traceLengthByLayerScratch.set(regionCache.traceLengthByLayer) + this.longestTraceLengthByLayerScratch.set( + regionCache.longestTraceLengthByLayer, + ) + addSegmentLengthByLayer( + this.traceLengthByLayerScratch, + this.longestTraceLengthByLayerScratch, + segmentGeometry.layerMask, + segmentGeometry.length, + ) + return this.computeTraceOccupancyCostForRegion( + regionId, + this.traceLengthByLayerScratch, + this.longestTraceLengthByLayerScratch, + ) } populateSegmentGeometryScratch( @@ -784,6 +872,10 @@ export class TinyHyperGraphSolver extends BaseSolver { scratch.greaterAngle = angle1 < angle2 ? angle2 : angle1 scratch.layerMask = (1 << z1) | (1 << z2) scratch.entryExitLayerChanges = z1 !== z2 ? 1 : 0 + scratch.length = Math.hypot( + topology.portX[port2Id] - topology.portX[port1Id], + topology.portY[port2Id] - topology.portY[port1Id], + ) return scratch } @@ -838,23 +930,45 @@ export class TinyHyperGraphSolver extends BaseSolver { const existingEntryExitLayerChanges = regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges const existingSegmentCount = lesserAngles.length + const traceLengthByLayer = this.USE_TRACE_OCCUPANCY_COST + ? new Float64Array(regionCache.traceLengthByLayer) + : regionCache.traceLengthByLayer + const longestTraceLengthByLayer = this.USE_TRACE_OCCUPANCY_COST + ? new Float64Array(regionCache.longestTraceLengthByLayer) + : regionCache.longestTraceLengthByLayer + if (this.USE_TRACE_OCCUPANCY_COST) { + addSegmentLengthByLayer( + traceLengthByLayer, + longestTraceLengthByLayer, + segmentGeometry.layerMask, + segmentGeometry.length, + ) + } state.regionIntersectionCaches[regionId] = { netIds, lesserAngles, greaterAngles, layerMasks, + traceLengthByLayer, + longestTraceLengthByLayer, existingSameLayerIntersections, existingCrossingLayerIntersections, existingEntryExitLayerChanges, existingSegmentCount, - existingRegionCost: this.computeRegionCostForRegion( - regionId, - existingSameLayerIntersections, - existingCrossingLayerIntersections, - existingEntryExitLayerChanges, - existingSegmentCount, - ), + existingRegionCost: + this.computeRegionCostForRegion( + regionId, + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + existingSegmentCount, + ) + + this.computeTraceOccupancyCostForRegion( + regionId, + traceLengthByLayer, + longestTraceLengthByLayer, + ), } } @@ -1266,6 +1380,7 @@ export class TinyHyperGraphSolver extends BaseSolver { onAllRoutesRouted() { const { topology, state } = this + this.activateTraceOccupancyCost() const ripThresholdProgress = this.RIP_THRESHOLD_RAMP_ATTEMPTS <= 0 ? 1 @@ -1334,6 +1449,21 @@ export class TinyHyperGraphSolver extends BaseSolver { }) } + activateTraceOccupancyCost() { + if (!this.USE_TRACE_OCCUPANCY_COST || this.traceOccupancyCostActive) return + this.traceOccupancyCostActive = true + + for (let regionId = 0; regionId < this.topology.regionCount; regionId++) { + const regionCache = this.state.regionIntersectionCaches[regionId] + if (!regionCache) continue + regionCache.existingRegionCost += this.computeTraceOccupancyCostForRegion( + regionId, + regionCache.traceLengthByLayer, + regionCache.longestTraceLengthByLayer, + ) + } + } + onOutOfCandidates() { const { topology, state } = this const currentRouteId = state.currentRouteId @@ -1425,6 +1555,12 @@ export class TinyHyperGraphSolver extends BaseSolver { const neighborPortZ = topology.portZ[neighborPortId] const layerMask = (1 << currentPortZ) | (1 << neighborPortZ) const entryExitLayerChanges = currentPortZ !== neighborPortZ ? 1 : 0 + const segmentLength = this.traceOccupancyCostActive + ? Math.hypot( + topology.portX[neighborPortId] - topology.portX[currentPortId], + topology.portY[neighborPortId] - topology.portY[currentPortId], + ) + : 0 const [ newSameLayerIntersections, @@ -1454,7 +1590,19 @@ export class TinyHyperGraphSolver extends BaseSolver { newCrossLayerIntersections, regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges, regionCache.existingSegmentCount + 1, - ) - regionCache.existingRegionCost + ) + + this.computeProspectiveTraceOccupancyCostForRegion( + nextRegionId, + regionCache, + { + lesserAngle, + greaterAngle, + layerMask, + entryExitLayerChanges, + length: segmentLength, + }, + ) - + regionCache.existingRegionCost return ( currentCandidate.g + diff --git a/lib/index.ts b/lib/index.ts index 421d5ad..7d68ce8 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,7 +8,7 @@ export * from "./selective-rerip-tiny-hyper-graph-solver" export * from "./bus-solver" export * from "./region-graph" export { - computeTracePitch, + computeTraceOccupancyCost, DEFAULT_MIN_TRACE_CLEARANCE, DEFAULT_MIN_TRACE_WIDTH, DEFAULT_MIN_VIA_PAD_DIAMETER, diff --git a/lib/poly.ts b/lib/poly.ts index 5cfbda0..a4cc23d 100644 --- a/lib/poly.ts +++ b/lib/poly.ts @@ -65,6 +65,7 @@ interface SegmentGeometryScratch { greaterAngle: number layerMask: number entryExitLayerChanges: number + length: number } const isRecord = (value: unknown): value is Record => @@ -620,6 +621,7 @@ export class PolyHyperGraphSolver extends TinyHyperGraphSolver { greaterAngle: 0, layerMask: 0, entryExitLayerChanges: 0, + length: 0, } constructor( @@ -654,6 +656,10 @@ export class PolyHyperGraphSolver extends TinyHyperGraphSolver { scratch.greaterAngle = position1 < position2 ? position2 : position1 scratch.layerMask = (1 << z1) | (1 << z2) scratch.entryExitLayerChanges = z1 !== z2 ? 1 : 0 + scratch.length = Math.hypot( + topology.portX[port2Id] - topology.portX[port1Id], + topology.portY[port2Id] - topology.portY[port1Id], + ) return scratch } @@ -676,6 +682,10 @@ export class PolyHyperGraphSolver extends TinyHyperGraphSolver { ) } + protected override getRegionArea(regionId: RegionId): number { + return this.topology.regionArea[regionId] + } + override visualize(): GraphicsObject { return visualizePolyHyperGraph(this) } diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index 74ee65b..b9d5422 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -107,6 +107,12 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + traceLengthByLayer: new Float64Array( + regionIntersectionCache.traceLengthByLayer, + ), + longestTraceLengthByLayer: new Float64Array( + regionIntersectionCache.longestTraceLengthByLayer, + ), existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -400,6 +406,7 @@ const createSolvedSolverFromRegionSegments = ( ) => { const solver = new TinyHyperGraphSolver(topology, problem, options) applyRouteSegmentsToSolver(solver, routeSegmentsByRegion) + solver.activateTraceOccupancyCost() return solver } @@ -887,6 +894,7 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER minTraceWidth = DEFAULT_MIN_TRACE_WIDTH minTraceClearance = DEFAULT_MIN_TRACE_CLEARANCE + USE_TRACE_OCCUPANCY_COST = true VERBOSE = false RIP_THRESHOLD_START = 0.05 diff --git a/lib/types.ts b/lib/types.ts index b109000..4b62470 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -25,6 +25,8 @@ export interface DynamicAnglePairArrays { } export interface RegionIntersectionCache extends DynamicAnglePairArrays { + traceLengthByLayer: Float64Array + longestTraceLengthByLayer: Float64Array existingSameLayerIntersections: Integer existingCrossingLayerIntersections: Integer existingEntryExitLayerChanges: Integer diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 16431f3..8fa748e 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -15,6 +15,8 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + traceLengthByLayer: new Float64Array(32), + longestTraceLengthByLayer: new Float64Array(32), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, diff --git a/tests/solver/region-net-id.test.ts b/tests/solver/region-net-id.test.ts index 528bacf..513fb0a 100644 --- a/tests/solver/region-net-id.test.ts +++ b/tests/solver/region-net-id.test.ts @@ -13,6 +13,8 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + traceLengthByLayer: new Float64Array(32), + longestTraceLengthByLayer: new Float64Array(32), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, diff --git a/tests/solver/trace-occupancy-activation.test.ts b/tests/solver/trace-occupancy-activation.test.ts new file mode 100644 index 0000000..b2edc5f --- /dev/null +++ b/tests/solver/trace-occupancy-activation.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test" +import { + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, +} from "lib" + +test("trace occupancy activates only after a complete baseline exists", () => { + const topology: TinyHyperGraphTopology = { + portCount: 0, + regionCount: 1, + regionIncidentPorts: [[]], + incidentPortRegion: [], + regionWidth: new Float64Array([2]), + regionHeight: new Float64Array([1]), + regionCenterX: new Float64Array([1]), + regionCenterY: new Float64Array([0.5]), + portAngleForRegion1: new Int32Array(0), + portX: new Float64Array(0), + portY: new Float64Array(0), + portZ: new Int32Array(0), + } + const problem: TinyHyperGraphProblem = { + routeCount: 0, + portSectionMask: new Int8Array(0), + routeStartPort: new Int32Array(0), + routeEndPort: new Int32Array(0), + routeNet: new Int32Array(0), + regionNetId: new Int32Array([-1]), + } + const solver = new TinyHyperGraphSolver(topology, problem, { + RIP_THRESHOLD_START: 1, + }) + const regionCache = solver.state.regionIntersectionCaches[0]! + regionCache.traceLengthByLayer[0] = 7 + regionCache.longestTraceLengthByLayer[0] = 4 + + expect(regionCache.existingRegionCost).toBe(0) + solver.onAllRoutesRouted() + expect(regionCache.existingRegionCost).toBeCloseTo(0.09) + expect(solver.solved).toBe(true) +}) diff --git a/tests/solver/trace-occupancy-cost.test.ts b/tests/solver/trace-occupancy-cost.test.ts new file mode 100644 index 0000000..81a5dc8 --- /dev/null +++ b/tests/solver/trace-occupancy-cost.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test" +import { computeTraceOccupancyCost } from "lib" + +test("trace occupancy charges only shared swept copper on the busiest layer", () => { + expect(computeTraceOccupancyCost(2, [4], [4])).toBe(0) + expect(computeTraceOccupancyCost(2, [7, 20], [4, 19])).toBeCloseTo(0.09) + expect( + computeTraceOccupancyCost(2, [7, 20], [4, 19], 0.15, 0.25), + ).toBeCloseTo(0.36) +}) diff --git a/tests/solver/trace-pitch.test.ts b/tests/solver/trace-pitch.test.ts deleted file mode 100644 index 70c8fbe..0000000 --- a/tests/solver/trace-pitch.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect, test } from "bun:test" -import { - computeTracePitch, - DEFAULT_MIN_TRACE_CLEARANCE, - DEFAULT_MIN_TRACE_WIDTH, - type TinyHyperGraphProblem, - TinyHyperGraphSolver, - type TinyHyperGraphTopology, -} from "lib/index" - -const topology: TinyHyperGraphTopology = { - portCount: 0, - regionCount: 0, - regionIncidentPorts: [], - incidentPortRegion: [], - regionWidth: new Float64Array(0), - regionHeight: new Float64Array(0), - regionCenterX: new Float64Array(0), - regionCenterY: new Float64Array(0), - portAngleForRegion1: new Int32Array(0), - portX: new Float64Array(0), - portY: new Float64Array(0), - portZ: new Int32Array(0), -} - -const problem: TinyHyperGraphProblem = { - routeCount: 0, - portSectionMask: new Int8Array(0), - routeStartPort: new Int32Array(0), - routeEndPort: new Int32Array(0), - routeNet: new Int32Array(0), - regionNetId: new Int32Array(0), -} - -test("trace pitch defaults to 0.1mm width plus 0.1mm clearance", () => { - expect(DEFAULT_MIN_TRACE_WIDTH).toBe(0.1) - expect(DEFAULT_MIN_TRACE_CLEARANCE).toBe(0.1) - expect(computeTracePitch()).toBeCloseTo(0.2) - expect( - new TinyHyperGraphSolver(topology, problem).getTracePitch(), - ).toBeCloseTo(0.2) -}) - -test("trace pitch is derived from configured width and clearance", () => { - expect(computeTracePitch(0.15, 0.22)).toBeCloseTo(0.37) - expect( - new TinyHyperGraphSolver(topology, problem, { - minTraceWidth: 0.15, - minTraceClearance: 0.22, - }).getTracePitch(), - ).toBeCloseTo(0.37) -})