From caed5d933f749f9364ec301acf85d0517bfd95f7 Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 27 Jul 2026 16:27:42 -0700 Subject: [PATCH 1/2] Reject crossings in greedy final routes --- lib/core.ts | 9 +++- ...tance-aware-tiny-hypergraph-solver.test.ts | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/lib/core.ts b/lib/core.ts index c224a31..44c8747 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -597,6 +597,8 @@ export class TinyHyperGraphSolver extends BaseSolver { if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { continue } + const g = this.computeG(currentCandidate, neighborPortId) + if (!Number.isFinite(g)) continue this.onPathFound(currentCandidate) return } @@ -1536,8 +1538,11 @@ export class TinyHyperGraphSolver extends BaseSolver { class GreedyFinalRouteSolver extends TinyHyperGraphSolver { override computeG( currentCandidate: Candidate, - _neighborPortId: PortId, + neighborPortId: PortId, ): number { - return currentCandidate.g + const constrainedCost = super.computeG(currentCandidate, neighborPortId) + return Number.isFinite(constrainedCost) + ? currentCandidate.g + : constrainedCost } } diff --git a/tests/distance-aware-tiny-hypergraph-solver.test.ts b/tests/distance-aware-tiny-hypergraph-solver.test.ts index c8740db..79620e4 100644 --- a/tests/distance-aware-tiny-hypergraph-solver.test.ts +++ b/tests/distance-aware-tiny-hypergraph-solver.test.ts @@ -51,3 +51,49 @@ test("queues a costed goal candidate before committing the path", () => { expect(solver.state.regionSegments[0]).toEqual([[0, 0, 1]]) expect(solver.state.currentRouteId).toBeUndefined() }) + +test("timeout fallback does not accept a crossing final hop in a single-layer region", () => { + const topology: TinyHyperGraphTopology = { + portCount: 4, + regionCount: 2, + regionIncidentPorts: [[0, 1, 2, 3], []], + incidentPortRegion: [ + [0, 1], + [0, 1], + [0, 1], + [0, 1], + ], + regionWidth: new Float64Array([10, 10]), + regionHeight: new Float64Array([10, 10]), + regionCenterX: new Float64Array(2), + regionCenterY: new Float64Array(2), + regionAvailableZMask: new Int32Array([1 << 0, 0]), + portAngleForRegion1: new Int32Array([0, 9000, 18000, 27000]), + portAngleForRegion2: new Int32Array(4), + portX: new Float64Array([1, 0, -1, 0]), + portY: new Float64Array([0, 1, 0, -1]), + portZ: new Int32Array(4), + } + const problem: TinyHyperGraphProblem = { + routeCount: 2, + portSectionMask: new Int8Array(4).fill(1), + routeStartPort: new Int32Array([0, 1]), + routeEndPort: new Int32Array([2, 3]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array([-1, -1]), + } + const solver = new DistanceAwareTinyHyperGraphSolver(topology, problem, { + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: true, + GREEDY_FINAL_ROUTE_ITERS: 1, + MAX_ITERATIONS: 2, + STATIC_REACHABILITY_PRECHECK: false, + }) + + solver.solve() + + expect(solver.solved).toBe(false) + expect(solver.stats.acceptedGreedyFinalRouteOnTimeout).not.toBe(true) + expect( + solver.state.regionIntersectionCaches[0]?.existingSameLayerIntersections, + ).toBe(0) +}) From ab2500e641396074537fc862804cef3d9cbfd435 Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 28 Jul 2026 01:16:58 -0700 Subject: [PATCH 2/2] Add strict geometric intersection routing --- lib/bus-solver/TinyHyperGraphBusSolver.ts | 6 + lib/bus-solver/previewRoutingState.ts | 12 + lib/core.ts | 317 +++++++++++++++- lib/section-solver/index.ts | 11 + ...selective-rerip-tiny-hyper-graph-solver.ts | 357 +++++++++++++++++- lib/types.ts | 6 + ...terior-port-geometric-intersection.test.ts | 95 +++++ tests/solver/on-all-routes-routed.test.ts | 35 +- 8 files changed, 810 insertions(+), 29 deletions(-) create mode 100644 tests/solver/interior-port-geometric-intersection.test.ts diff --git a/lib/bus-solver/TinyHyperGraphBusSolver.ts b/lib/bus-solver/TinyHyperGraphBusSolver.ts index 463034b..d8ad7d5 100644 --- a/lib/bus-solver/TinyHyperGraphBusSolver.ts +++ b/lib/bus-solver/TinyHyperGraphBusSolver.ts @@ -1459,6 +1459,12 @@ export class TinyHyperGraphBusSolver extends TinyHyperGraphSolver { regionCache.lesserAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.greaterAngles = EMPTY_PREVIEW_INT32_ARRAY regionCache.layerMasks = EMPTY_PREVIEW_INT32_ARRAY + regionCache.port1Ids = EMPTY_PREVIEW_INT32_ARRAY + regionCache.port2Ids = EMPTY_PREVIEW_INT32_ARRAY + regionCache.x1 = new Float64Array(0) + regionCache.y1 = new Float64Array(0) + regionCache.x2 = new Float64Array(0) + regionCache.y2 = new Float64Array(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..afd4710 100644 --- a/lib/bus-solver/previewRoutingState.ts +++ b/lib/bus-solver/previewRoutingState.ts @@ -58,6 +58,12 @@ export const snapshotPreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), + port1Ids: new Int32Array(cache.port1Ids ?? new Int32Array(0)), + port2Ids: new Int32Array(cache.port2Ids ?? new Int32Array(0)), + x1: new Float64Array(cache.x1 ?? new Float64Array(0)), + y1: new Float64Array(cache.y1 ?? new Float64Array(0)), + x2: new Float64Array(cache.x2 ?? new Float64Array(0)), + y2: new Float64Array(cache.y2 ?? new Float64Array(0)), existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, @@ -81,6 +87,12 @@ export const restorePreviewRoutingState = ( lesserAngles: new Int32Array(cache.lesserAngles), greaterAngles: new Int32Array(cache.greaterAngles), layerMasks: new Int32Array(cache.layerMasks), + port1Ids: new Int32Array(cache.port1Ids ?? new Int32Array(0)), + port2Ids: new Int32Array(cache.port2Ids ?? new Int32Array(0)), + x1: new Float64Array(cache.x1 ?? new Float64Array(0)), + y1: new Float64Array(cache.y1 ?? new Float64Array(0)), + x2: new Float64Array(cache.x2 ?? new Float64Array(0)), + y2: new Float64Array(cache.y2 ?? new Float64Array(0)), existingCrossingLayerIntersections: cache.existingCrossingLayerIntersections, existingSameLayerIntersections: cache.existingSameLayerIntersections, diff --git a/lib/core.ts b/lib/core.ts index 44c8747..a8325e0 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -41,6 +41,12 @@ export const createEmptyRegionIntersectionCache = lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + port1Ids: new Int32Array(0), + port2Ids: new Int32Array(0), + x1: new Float64Array(0), + y1: new Float64Array(0), + x2: new Float64Array(0), + y2: new Float64Array(0), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -65,6 +71,16 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + port1Ids: new Int32Array( + regionIntersectionCache.port1Ids ?? new Int32Array(0), + ), + port2Ids: new Int32Array( + regionIntersectionCache.port2Ids ?? new Int32Array(0), + ), + x1: new Float64Array(regionIntersectionCache.x1 ?? new Float64Array(0)), + y1: new Float64Array(regionIntersectionCache.y1 ?? new Float64Array(0)), + x2: new Float64Array(regionIntersectionCache.x2 ?? new Float64Array(0)), + y2: new Float64Array(regionIntersectionCache.y2 ?? new Float64Array(0)), existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -153,6 +169,22 @@ export interface TinyHyperGraphProblem { * state and may be ripped and rerouted by the normal solver machinery. */ initialAssignments?: TinyHyperGraphInitialAssignment[] + + /** + * Immutable routed geometry that participates in intersection checks but is + * not owned or reripped by a route. Coordinates are expressed in the same + * space as topology ports. + */ + fixedRegionSegments?: Array<{ + regionId: RegionId + netId: NetId + x1: number + y1: number + x2: number + y2: number + layerMask: number + entryExitLayerChanges?: number + }> } export interface TinyHyperGraphProblemSetup { @@ -254,6 +286,7 @@ export interface TinyHyperGraphSolverOptions { STATIC_REACHABILITY_PRECHECK_MAX_HOPS?: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT?: boolean GREEDY_FINAL_ROUTE_ITERS?: number + REQUIRE_ZERO_INTERSECTIONS?: boolean } export interface TinyHyperGraphSolverOptionTarget { @@ -271,6 +304,7 @@ export interface TinyHyperGraphSolverOptionTarget { STATIC_REACHABILITY_PRECHECK_MAX_HOPS: number ACCEPT_BEST_SOLUTION_ON_TIMEOUT: boolean GREEDY_FINAL_ROUTE_ITERS: number + REQUIRE_ZERO_INTERSECTIONS: boolean } export const applyTinyHyperGraphSolverOptions = ( @@ -326,6 +360,9 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.GREEDY_FINAL_ROUTE_ITERS !== undefined) { solver.GREEDY_FINAL_ROUTE_ITERS = options.GREEDY_FINAL_ROUTE_ITERS } + if (options.REQUIRE_ZERO_INTERSECTIONS !== undefined) { + solver.REQUIRE_ZERO_INTERSECTIONS = options.REQUIRE_ZERO_INTERSECTIONS + } } export const getTinyHyperGraphSolverOptions = ( @@ -346,6 +383,7 @@ export const getTinyHyperGraphSolverOptions = ( solver.STATIC_REACHABILITY_PRECHECK_MAX_HOPS, ACCEPT_BEST_SOLUTION_ON_TIMEOUT: solver.ACCEPT_BEST_SOLUTION_ON_TIMEOUT, GREEDY_FINAL_ROUTE_ITERS: solver.GREEDY_FINAL_ROUTE_ITERS, + REQUIRE_ZERO_INTERSECTIONS: solver.REQUIRE_ZERO_INTERSECTIONS, }) const compareCandidatesByF = (left: Candidate, right: Candidate) => @@ -391,6 +429,7 @@ export class TinyHyperGraphSolver extends BaseSolver { STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true GREEDY_FINAL_ROUTE_ITERS = 4 + REQUIRE_ZERO_INTERSECTIONS = false constructor( public topology: TinyHyperGraphTopology, @@ -423,6 +462,7 @@ export class TinyHyperGraphSolver extends BaseSolver { } this.routeAttemptCountByRouteId = new Uint32Array(problem.routeCount) this.routeSuccessCountByRouteId = new Uint32Array(problem.routeCount) + this.appendFixedRegionSegmentsToCaches() const initialAssignmentStats = applyInitialAssignments({ topology, problem, @@ -811,11 +851,11 @@ export class TinyHyperGraphSolver extends BaseSolver { newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, - ] = countNewIntersectionsWithValues( - regionCache, + ] = this.countNewGeometricIntersections( + regionId, + port1Id, + port2Id, state.currentRouteNetId!, - segmentGeometry.lesserAngle, - segmentGeometry.greaterAngle, segmentGeometry.layerMask, segmentGeometry.entryExitLayerChanges, ) @@ -837,6 +877,30 @@ export class TinyHyperGraphSolver extends BaseSolver { layerMasks.set(regionCache.layerMasks) layerMasks[nextLength - 1] = segmentGeometry.layerMask + const port1Ids = new Int32Array(nextLength) + port1Ids.set(regionCache.port1Ids ?? []) + port1Ids[nextLength - 1] = port1Id + + const port2Ids = new Int32Array(nextLength) + port2Ids.set(regionCache.port2Ids ?? []) + port2Ids[nextLength - 1] = port2Id + + const x1 = new Float64Array(nextLength) + x1.set(regionCache.x1 ?? []) + x1[nextLength - 1] = this.topology.portX[port1Id]! + + const y1 = new Float64Array(nextLength) + y1.set(regionCache.y1 ?? []) + y1[nextLength - 1] = this.topology.portY[port1Id]! + + const x2 = new Float64Array(nextLength) + x2.set(regionCache.x2 ?? []) + x2[nextLength - 1] = this.topology.portX[port2Id]! + + const y2 = new Float64Array(nextLength) + y2.set(regionCache.y2 ?? []) + y2[nextLength - 1] = this.topology.portY[port2Id]! + const existingSameLayerIntersections = regionCache.existingSameLayerIntersections + newSameLayerIntersections const existingCrossingLayerIntersections = @@ -851,6 +915,12 @@ export class TinyHyperGraphSolver extends BaseSolver { lesserAngles, greaterAngles, layerMasks, + port1Ids, + port2Ids, + x1, + y1, + x2, + y2, existingSameLayerIntersections, existingCrossingLayerIntersections, existingEntryExitLayerChanges, @@ -865,6 +935,212 @@ export class TinyHyperGraphSolver extends BaseSolver { } } + protected appendFixedRegionSegmentsToCaches(): void { + for (const segment of this.problem.fixedRegionSegments ?? []) { + if ( + segment.regionId < 0 || + segment.regionId >= this.topology.regionCount + ) { + throw new Error( + `Fixed segment references invalid region ${segment.regionId}`, + ) + } + this.appendFixedSegmentToRegionCache(segment) + } + } + + protected appendFixedSegmentToRegionCache( + segment: NonNullable[number], + ): void { + const regionCache = this.state.regionIntersectionCaches[segment.regionId] + const [ + newSameLayerIntersections, + newCrossLayerIntersections, + newEntryExitLayerChanges, + ] = this.countNewGeometricIntersectionsForCoordinates( + segment.regionId, + segment.x1, + segment.y1, + segment.x2, + segment.y2, + segment.netId, + segment.layerMask, + segment.entryExitLayerChanges ?? 0, + ) + const nextLength = regionCache.netIds.length + 1 + const appendInt = (values: Int32Array | undefined, value: number) => { + const next = new Int32Array(nextLength) + next.set(values ?? []) + next[nextLength - 1] = value + return next + } + const appendFloat = (values: Float64Array | undefined, value: number) => { + const next = new Float64Array(nextLength) + next.set(values ?? []) + next[nextLength - 1] = value + return next + } + const existingSameLayerIntersections = + regionCache.existingSameLayerIntersections + newSameLayerIntersections + const existingCrossingLayerIntersections = + regionCache.existingCrossingLayerIntersections + + newCrossLayerIntersections + const existingEntryExitLayerChanges = + regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges + + this.state.regionIntersectionCaches[segment.regionId] = { + netIds: appendInt(regionCache.netIds, segment.netId), + lesserAngles: appendInt(regionCache.lesserAngles, 0), + greaterAngles: appendInt(regionCache.greaterAngles, 0), + layerMasks: appendInt(regionCache.layerMasks, segment.layerMask), + port1Ids: appendInt(regionCache.port1Ids, -1), + port2Ids: appendInt(regionCache.port2Ids, -1), + x1: appendFloat(regionCache.x1, segment.x1), + y1: appendFloat(regionCache.y1, segment.y1), + x2: appendFloat(regionCache.x2, segment.x2), + y2: appendFloat(regionCache.y2, segment.y2), + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + existingSegmentCount: nextLength, + existingRegionCost: this.computeRegionCostForRegion( + segment.regionId, + existingSameLayerIntersections, + existingCrossingLayerIntersections, + existingEntryExitLayerChanges, + nextLength, + ), + } + } + + protected countNewGeometricIntersections( + regionId: RegionId, + port1Id: PortId, + port2Id: PortId, + newNetId: number, + newLayerMask: number, + entryExitLayerChanges: number, + ): [number, number, number] { + const { topology } = this + return this.countNewGeometricIntersectionsForCoordinates( + regionId, + topology.portX[port1Id]!, + topology.portY[port1Id]!, + topology.portX[port2Id]!, + topology.portY[port2Id]!, + newNetId, + newLayerMask, + entryExitLayerChanges, + { port1Id, port2Id }, + ) + } + + protected countNewGeometricIntersectionsForCoordinates( + regionId: RegionId, + newX1: number, + newY1: number, + newX2: number, + newY2: number, + newNetId: number, + newLayerMask: number, + entryExitLayerChanges: number, + fallbackPortIds?: { port1Id: PortId; port2Id: PortId }, + ): [number, number, number] { + const regionCache = this.state.regionIntersectionCaches[regionId] + if ( + !regionCache.x1 || + !regionCache.y1 || + !regionCache.x2 || + !regionCache.y2 || + regionCache.x1.length !== regionCache.netIds.length || + regionCache.y1.length !== regionCache.netIds.length || + regionCache.x2.length !== regionCache.netIds.length || + regionCache.y2.length !== regionCache.netIds.length + ) { + if (!fallbackPortIds) { + return [0, 0, entryExitLayerChanges] + } + const segmentGeometry = this.populateSegmentGeometryScratch( + regionId, + fallbackPortIds.port1Id, + fallbackPortIds.port2Id, + ) + return countNewIntersectionsWithValues( + regionCache, + newNetId, + segmentGeometry.lesserAngle, + segmentGeometry.greaterAngle, + newLayerMask, + entryExitLayerChanges, + ) + } + const orientation = ( + firstX: number, + firstY: number, + secondX: number, + secondY: number, + thirdX: number, + thirdY: number, + ) => + (secondX - firstX) * (thirdY - firstY) - + (secondY - firstY) * (thirdX - firstX) + + let sameLayerIntersections = 0 + let crossingLayerIntersections = 0 + for (let index = 0; index < regionCache.netIds.length; index++) { + if (regionCache.netIds[index] === newNetId) continue + const existingX1 = regionCache.x1[index]! + const existingY1 = regionCache.y1[index]! + const existingX2 = regionCache.x2[index]! + const existingY2 = regionCache.y2[index]! + const firstSideA = orientation( + newX1, + newY1, + newX2, + newY2, + existingX1, + existingY1, + ) + const firstSideB = orientation( + newX1, + newY1, + newX2, + newY2, + existingX2, + existingY2, + ) + const secondSideA = orientation( + existingX1, + existingY1, + existingX2, + existingY2, + newX1, + newY1, + ) + const secondSideB = orientation( + existingX1, + existingY1, + existingX2, + existingY2, + newX2, + newY2, + ) + if (firstSideA * firstSideB >= 0 || secondSideA * secondSideB >= 0) { + continue + } + if ((newLayerMask & regionCache.layerMasks[index]!) !== 0) { + sameLayerIntersections += 1 + } else { + crossingLayerIntersections += 1 + } + } + return [ + sameLayerIntersections, + crossingLayerIntersections, + entryExitLayerChanges, + ] + } + getSolvedPathSegments(finalCandidate: Candidate): Array<{ regionId: RegionId fromPortId: PortId @@ -917,6 +1193,7 @@ export class TinyHyperGraphSolver extends BaseSolver { { length: topology.regionCount }, () => createEmptyRegionIntersectionCache(), ) + this.appendFixedRegionSegmentsToCaches() state.currentRouteNetId = undefined state.currentRouteId = undefined state.unroutedRoutes = shuffle(range(problem.routeCount), state.ripCount) @@ -1283,17 +1560,30 @@ export class TinyHyperGraphSolver extends BaseSolver { const regionIdsOverCostThreshold: RegionId[] = [] const regionCosts = new Float64Array(topology.regionCount) + let sameLayerIntersectionCount = 0 + let crossingLayerIntersectionCount = 0 let maxRegionCost = 0 let totalRegionCost = 0 for (let regionId = 0; regionId < topology.regionCount; regionId++) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + const regionCache = state.regionIntersectionCaches[regionId] + const regionCost = regionCache?.existingRegionCost ?? 0 + const regionIntersectionCount = + (regionCache?.existingSameLayerIntersections ?? 0) + + (regionCache?.existingCrossingLayerIntersections ?? 0) regionCosts[regionId] = regionCost + sameLayerIntersectionCount += + regionCache?.existingSameLayerIntersections ?? 0 + crossingLayerIntersectionCount += + regionCache?.existingCrossingLayerIntersections ?? 0 maxRegionCost = Math.max(maxRegionCost, regionCost) totalRegionCost += regionCost - if (regionCost > currentRipThreshold) { + if ( + this.REQUIRE_ZERO_INTERSECTIONS + ? regionIntersectionCount > 0 + : regionCost > currentRipThreshold + ) { regionIdsOverCostThreshold.push(regionId) } } @@ -1312,11 +1602,14 @@ export class TinyHyperGraphSolver extends BaseSolver { bestMaxRegionCost: this.bestSolvedStateSummary?.maxRegionCost, bestTotalRegionCost: this.bestSolvedStateSummary?.totalRegionCost, ripCount: state.ripCount, + sameLayerIntersectionCount, + crossingLayerIntersectionCount, } if ( regionIdsOverCostThreshold.length === 0 || - state.ripCount >= this.RIP_THRESHOLD_RAMP_ATTEMPTS + (!this.REQUIRE_ZERO_INTERSECTIONS && + state.ripCount >= this.RIP_THRESHOLD_RAMP_ATTEMPTS) ) { this.solved = true return @@ -1437,11 +1730,11 @@ export class TinyHyperGraphSolver extends BaseSolver { newSameLayerIntersections, newCrossLayerIntersections, newEntryExitLayerChanges, - ] = countNewIntersectionsWithValues( - regionCache, + ] = this.countNewGeometricIntersections( + nextRegionId, + currentPortId, + neighborPortId, state.currentRouteNetId!, - lesserAngle, - greaterAngle, layerMask, entryExitLayerChanges, ) diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index e66f4a1..ac28cd8 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -103,6 +103,16 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + port1Ids: new Int32Array( + regionIntersectionCache.port1Ids ?? new Int32Array(0), + ), + port2Ids: new Int32Array( + regionIntersectionCache.port2Ids ?? new Int32Array(0), + ), + x1: new Float64Array(regionIntersectionCache.x1 ?? new Float64Array(0)), + y1: new Float64Array(regionIntersectionCache.y1 ?? new Float64Array(0)), + x2: new Float64Array(regionIntersectionCache.x2 ?? new Float64Array(0)), + y2: new Float64Array(regionIntersectionCache.y2 ?? new Float64Array(0)), existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -897,6 +907,7 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { STATIC_REACHABILITY_PRECHECK_MAX_HOPS = 16 ACCEPT_BEST_SOLUTION_ON_TIMEOUT = true GREEDY_FINAL_ROUTE_ITERS = 4 + REQUIRE_ZERO_INTERSECTIONS = false constructor( public topology: TinyHyperGraphTopology, diff --git a/lib/selective-rerip-tiny-hyper-graph-solver.ts b/lib/selective-rerip-tiny-hyper-graph-solver.ts index 64a7f2c..d050fec 100644 --- a/lib/selective-rerip-tiny-hyper-graph-solver.ts +++ b/lib/selective-rerip-tiny-hyper-graph-solver.ts @@ -1,4 +1,5 @@ import { + type Candidate, createEmptyRegionIntersectionCache, type TinyHyperGraphProblem, type TinyHyperGraphSolverOptions, @@ -63,8 +64,25 @@ export type SelectiveReripTinyHyperGraphStats = { lastRippedRouteIds: RouteId[] lastRelaxedSearchExpandedLabelCount: number lastAlternateSearchExpandedLabelCount: number + bestPartialRoutedRouteCount?: number + bestPartialIntersectionCount?: number + restoredBestPartialSolutionOnTimeout?: boolean } +type PartialStateSnapshot = { + portAssignment: Int32Array + regionSegments: Array<[RouteId, PortId, PortId][]> + regionIntersectionCaches: ReturnType< + typeof createEmptyRegionIntersectionCache + >[] + regionCongestionCost: Float64Array + ripCount: number +} + +const clonePartialStateSnapshot = ( + snapshot: PartialStateSnapshot, +): PartialStateSnapshot => structuredClone(snapshot) + const createInitialSelectiveReripStats = (): SelectiveReripTinyHyperGraphStats => ({ selectiveRipCount: 0, @@ -155,13 +173,22 @@ export class SelectiveReripTinyHyperGraphSolver extends DistanceAwareTinyHyperGr private readonly selectiveReripStats = createInitialSelectiveReripStats() private selectiveReripCongestionUpdateCount = 0 + private readonly restoreBestPartialSolutionOnTimeout: boolean + private bestPartialStateSnapshot: PartialStateSnapshot | undefined + private bestPartialRoutedRouteIds = new Set() + private bestPartialIntersectionCount = Number.POSITIVE_INFINITY constructor( topology: TinyHyperGraphTopology, problem: TinyHyperGraphProblem, - options?: TinyHyperGraphSolverOptions, + options?: TinyHyperGraphSolverOptions & { + RESTORE_BEST_PARTIAL_SOLUTION_ON_TIMEOUT?: boolean + }, ) { super(topology, problem, options) + this.restoreBestPartialSolutionOnTimeout = + options?.RESTORE_BEST_PARTIAL_SOLUTION_ON_TIMEOUT ?? false + this.captureBestPartialState() } getSelectiveReripStats(): SelectiveReripTinyHyperGraphStats { @@ -186,6 +213,7 @@ export class SelectiveReripTinyHyperGraphSolver extends DistanceAwareTinyHyperGr } override onOutOfCandidates(): void { + this.captureBestPartialState() const failedRouteId = this.state.currentRouteId if (failedRouteId === undefined) { throw new Error( @@ -298,6 +326,216 @@ export class SelectiveReripTinyHyperGraphSolver extends DistanceAwareTinyHyperGr this.publishSelectiveReripStats() } + override onPathFound(finalCandidate: Candidate): void { + super.onPathFound(finalCandidate) + this.captureBestPartialState() + } + + override onAllRoutesRouted(): void { + if (!this.REQUIRE_ZERO_INTERSECTIONS) { + super.onAllRoutesRouted() + return + } + + const crossingRoutePairs: Array<[RouteId, RouteId]> = [] + const seenPairs = new Set() + const routesCrossingFixedSegments = new Set() + const fixedSegmentsByRegion = new Map< + RegionId, + NonNullable + >() + for (const fixedSegment of this.problem.fixedRegionSegments ?? []) { + const regionSegments = + fixedSegmentsByRegion.get(fixedSegment.regionId) ?? [] + regionSegments.push(fixedSegment) + fixedSegmentsByRegion.set(fixedSegment.regionId, regionSegments) + } + for ( + let regionId = 0; + regionId < this.state.regionSegments.length; + regionId++ + ) { + const segments = this.state.regionSegments[regionId]! + for (const [routeId, fromPortId, toPortId] of segments) { + for (const fixedSegment of fixedSegmentsByRegion.get(regionId) ?? []) { + if ( + this.problem.routeNet[routeId] === fixedSegment.netId || + !this.segmentIntersectsCoordinates( + fromPortId, + toPortId, + fixedSegment.x1, + fixedSegment.y1, + fixedSegment.x2, + fixedSegment.y2, + ) + ) { + continue + } + routesCrossingFixedSegments.add(routeId) + } + } + for (let firstIndex = 0; firstIndex < segments.length; firstIndex++) { + const [firstRouteId, firstFromPortId, firstToPortId] = + segments[firstIndex]! + for ( + let secondIndex = firstIndex + 1; + secondIndex < segments.length; + secondIndex++ + ) { + const [secondRouteId, secondFromPortId, secondToPortId] = + segments[secondIndex]! + if ( + this.problem.routeNet[firstRouteId] === + this.problem.routeNet[secondRouteId] || + !this.segmentsGeometricallyIntersect( + firstFromPortId, + firstToPortId, + secondFromPortId, + secondToPortId, + ) + ) { + continue + } + const lesserRouteId = Math.min(firstRouteId, secondRouteId) + const greaterRouteId = Math.max(firstRouteId, secondRouteId) + const pairKey = `${lesserRouteId}:${greaterRouteId}` + if (seenPairs.has(pairKey)) continue + seenPairs.add(pairKey) + crossingRoutePairs.push([lesserRouteId, greaterRouteId]) + } + } + } + + if ( + crossingRoutePairs.length === 0 && + routesCrossingFixedSegments.size === 0 + ) { + super.onAllRoutesRouted() + return + } + + const remainingPairs = new Set( + crossingRoutePairs.map((_, pairIndex) => pairIndex), + ) + const rippedRouteIds = new Set(routesCrossingFixedSegments) + while (remainingPairs.size > 0) { + const routeConflictCounts = new Map() + for (const pairIndex of remainingPairs) { + const [firstRouteId, secondRouteId] = crossingRoutePairs[pairIndex]! + routeConflictCounts.set( + firstRouteId, + (routeConflictCounts.get(firstRouteId) ?? 0) + 1, + ) + routeConflictCounts.set( + secondRouteId, + (routeConflictCounts.get(secondRouteId) ?? 0) + 1, + ) + } + const routeIdToRip = [...routeConflictCounts].sort( + ([leftRouteId, leftCount], [rightRouteId, rightCount]) => + rightCount - leftCount || leftRouteId - rightRouteId, + )[0]![0] + rippedRouteIds.add(routeIdToRip) + for (const pairIndex of remainingPairs) { + if (crossingRoutePairs[pairIndex]!.includes(routeIdToRip)) { + remainingPairs.delete(pairIndex) + } + } + } + + this.addCongestionCostForSelectiveRerip() + this.rebuildCommittedState(rippedRouteIds) + this.state.ripCount += 1 + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + this.state.unroutedRoutes = [...rippedRouteIds] + this.state.candidateQueue.clear() + this.resetCandidateBestCosts() + this.state.goalPortId = -1 + this.selectiveReripStats.selectiveRipCount += 1 + this.selectiveReripStats.selectivelyRippedRouteCount += rippedRouteIds.size + this.selectiveReripStats.lastRippedRouteIds = [...rippedRouteIds] + this.stats = { + ...this.stats, + zeroIntersectionCrossingPairCount: crossingRoutePairs.length, + zeroIntersectionFixedSegmentRouteCount: routesCrossingFixedSegments.size, + zeroIntersectionReripRouteCount: rippedRouteIds.size, + ripCount: this.state.ripCount, + } + this.publishSelectiveReripStats() + } + + override tryFinalAcceptance(): void { + super.tryFinalAcceptance() + if ( + this.solved || + !this.restoreBestPartialSolutionOnTimeout || + !this.bestPartialStateSnapshot + ) { + return + } + + const snapshot = clonePartialStateSnapshot(this.bestPartialStateSnapshot) + this.state.portAssignment = snapshot.portAssignment + this.state.regionSegments = snapshot.regionSegments + this.state.regionIntersectionCaches = snapshot.regionIntersectionCaches + this.state.regionCongestionCost = snapshot.regionCongestionCost + this.state.ripCount = snapshot.ripCount + this.state.currentRouteId = undefined + this.state.currentRouteNetId = undefined + this.state.unroutedRoutes = Array.from( + { length: this.problem.routeCount }, + (_, routeId) => routeId, + ).filter((routeId) => !this.bestPartialRoutedRouteIds.has(routeId)) + this.state.candidateQueue.clear() + this.resetCandidateBestCosts() + this.state.goalPortId = -1 + this.selectiveReripStats.bestPartialRoutedRouteCount = + this.bestPartialRoutedRouteIds.size + this.selectiveReripStats.bestPartialIntersectionCount = + this.bestPartialIntersectionCount + this.selectiveReripStats.restoredBestPartialSolutionOnTimeout = true + this.publishSelectiveReripStats() + } + + private captureBestPartialState(): void { + if (!this.restoreBestPartialSolutionOnTimeout) return + + const routedRouteIds = new Set() + for (const regionSegments of this.state.regionSegments) { + for (const [routeId] of regionSegments) routedRouteIds.add(routeId) + } + const intersectionCount = this.state.regionIntersectionCaches.reduce( + (total, cache) => + total + + cache.existingSameLayerIntersections + + cache.existingCrossingLayerIntersections, + 0, + ) + const isNotBetter = this.REQUIRE_ZERO_INTERSECTIONS + ? intersectionCount > this.bestPartialIntersectionCount || + (intersectionCount === this.bestPartialIntersectionCount && + routedRouteIds.size < this.bestPartialRoutedRouteIds.size) + : routedRouteIds.size < this.bestPartialRoutedRouteIds.size || + (routedRouteIds.size === this.bestPartialRoutedRouteIds.size && + intersectionCount >= this.bestPartialIntersectionCount) + if (isNotBetter) { + return + } + + this.bestPartialRoutedRouteIds = routedRouteIds + this.bestPartialIntersectionCount = intersectionCount + this.bestPartialStateSnapshot = clonePartialStateSnapshot({ + portAssignment: this.state.portAssignment, + regionSegments: this.state.regionSegments, + regionIntersectionCaches: this.state.regionIntersectionCaches, + regionCongestionCost: this.state.regionCongestionCost, + ripCount: this.state.ripCount, + }) + this.selectiveReripStats.bestPartialRoutedRouteCount = routedRouteIds.size + this.selectiveReripStats.bestPartialIntersectionCount = intersectionCount + } + private addCongestionCostForSelectiveRerip(): void { for (let regionId = 0; regionId < this.topology.regionCount; regionId++) { const regionCost = @@ -553,22 +791,108 @@ export class SelectiveReripTinyHyperGraphSolver extends DistanceAwareTinyHyperGr ), } if ((first.layerMask & second.layerMask) === 0) return false - if ( - first.lesserAngle === second.lesserAngle || - first.lesserAngle === second.greaterAngle || - first.greaterAngle === second.lesserAngle || - first.greaterAngle === second.greaterAngle - ) { - return false - } + return this.segmentsGeometricallyIntersect( + firstFromPortId, + firstToPortId, + secondFromPortId, + secondToPortId, + ) + } - const secondLesserInsideFirst = - first.lesserAngle < second.lesserAngle && - second.lesserAngle < first.greaterAngle - const secondGreaterInsideFirst = - first.lesserAngle < second.greaterAngle && - second.greaterAngle < first.greaterAngle - return secondLesserInsideFirst !== secondGreaterInsideFirst + private segmentsGeometricallyIntersect( + firstFromPortId: PortId, + firstToPortId: PortId, + secondFromPortId: PortId, + secondToPortId: PortId, + ): boolean { + const { portX, portY } = this.topology + return this.coordinatesGeometricallyIntersect( + portX[firstFromPortId]!, + portY[firstFromPortId]!, + portX[firstToPortId]!, + portY[firstToPortId]!, + portX[secondFromPortId]!, + portY[secondFromPortId]!, + portX[secondToPortId]!, + portY[secondToPortId]!, + ) + } + + private segmentIntersectsCoordinates( + fromPortId: PortId, + toPortId: PortId, + secondX1: number, + secondY1: number, + secondX2: number, + secondY2: number, + ): boolean { + const { portX, portY } = this.topology + return this.coordinatesGeometricallyIntersect( + portX[fromPortId]!, + portY[fromPortId]!, + portX[toPortId]!, + portY[toPortId]!, + secondX1, + secondY1, + secondX2, + secondY2, + ) + } + + private coordinatesGeometricallyIntersect( + firstX1: number, + firstY1: number, + firstX2: number, + firstY2: number, + secondX1: number, + secondY1: number, + secondX2: number, + secondY2: number, + ): boolean { + const orientation = ( + firstX: number, + firstY: number, + secondX: number, + secondY: number, + thirdX: number, + thirdY: number, + ) => + (secondX - firstX) * (thirdY - firstY) - + (secondY - firstY) * (thirdX - firstX) + + const firstSideA = orientation( + firstX1, + firstY1, + firstX2, + firstY2, + secondX1, + secondY1, + ) + const firstSideB = orientation( + firstX1, + firstY1, + firstX2, + firstY2, + secondX2, + secondY2, + ) + const secondSideA = orientation( + secondX1, + secondY1, + secondX2, + secondY2, + firstX1, + firstY1, + ) + const secondSideB = orientation( + secondX1, + secondY1, + secondX2, + secondY2, + firstX2, + firstY2, + ) + return firstSideA * firstSideB < 0 && secondSideA * secondSideB < 0 } private rebuildCommittedState(rippedRouteIds: ReadonlySet): void { @@ -580,6 +904,7 @@ export class SelectiveReripTinyHyperGraphSolver extends DistanceAwareTinyHyperGr { length: this.topology.regionCount }, () => createEmptyRegionIntersectionCache(), ) + this.appendFixedRegionSegmentsToCaches() for ( let regionId = 0; diff --git a/lib/types.ts b/lib/types.ts index b109000..4f364fd 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -25,6 +25,12 @@ export interface DynamicAnglePairArrays { } export interface RegionIntersectionCache extends DynamicAnglePairArrays { + port1Ids?: Int32Array + port2Ids?: Int32Array + x1?: Float64Array + y1?: Float64Array + x2?: Float64Array + y2?: Float64Array existingSameLayerIntersections: Integer existingCrossingLayerIntersections: Integer existingEntryExitLayerChanges: Integer diff --git a/tests/solver/interior-port-geometric-intersection.test.ts b/tests/solver/interior-port-geometric-intersection.test.ts new file mode 100644 index 0000000..20872ad --- /dev/null +++ b/tests/solver/interior-port-geometric-intersection.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test" +import { + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, +} from "lib/index" + +test("counts crossings from port coordinates when a port is inside a region", () => { + const topology: TinyHyperGraphTopology = { + portCount: 4, + regionCount: 1, + regionIncidentPorts: [[0, 1, 2, 3]], + incidentPortRegion: [[0], [0], [0], [0]], + regionWidth: new Float64Array([2]), + regionHeight: new Float64Array([2]), + regionCenterX: new Float64Array([1]), + regionCenterY: new Float64Array([1]), + // The second interval is nested inside the first, so boundary-angle + // ordering alone does not classify these segments as crossing. + portAngleForRegion1: new Int32Array([0, 3000, 1000, 2000]), + portX: new Float64Array([0, 2, 2, 0]), + portY: new Float64Array([0, 2, 0, 2]), + portZ: new Int32Array(4), + } + const problem: TinyHyperGraphProblem = { + routeCount: 2, + portSectionMask: new Int8Array(4).fill(1), + routeStartPort: new Int32Array([0, 2]), + routeEndPort: new Int32Array([1, 3]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array([-1]), + } + const solver = new TinyHyperGraphSolver(topology, problem) + + solver.state.currentRouteNetId = 0 + solver.appendSegmentToRegionCache(0, 0, 1) + solver.state.currentRouteNetId = 1 + solver.appendSegmentToRegionCache(0, 2, 3) + + const cache = solver.state.regionIntersectionCaches[0] + expect(cache.existingSameLayerIntersections).toBe(1) + expect(cache.existingCrossingLayerIntersections).toBe(0) + expect(Array.from(cache.port1Ids ?? [])).toEqual([0, 2]) + expect(Array.from(cache.port2Ids ?? [])).toEqual([1, 3]) +}) + +test("keeps fixed geometry in intersection caches across rerips", () => { + const topology: TinyHyperGraphTopology = { + portCount: 2, + regionCount: 1, + regionIncidentPorts: [[0, 1]], + incidentPortRegion: [[0], [0]], + regionWidth: new Float64Array([2]), + regionHeight: new Float64Array([2]), + regionCenterX: new Float64Array([1]), + regionCenterY: new Float64Array([1]), + portAngleForRegion1: new Int32Array([1000, 2000]), + portX: new Float64Array([2, 0]), + portY: new Float64Array([0, 2]), + portZ: new Int32Array(2), + } + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array(2).fill(1), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([1]), + regionNetId: new Int32Array([-1]), + fixedRegionSegments: [ + { + regionId: 0, + netId: 0, + x1: 0, + y1: 0, + x2: 2, + y2: 2, + layerMask: 1, + }, + ], + } + const solver = new TinyHyperGraphSolver(topology, problem) + + solver.state.currentRouteNetId = 1 + solver.appendSegmentToRegionCache(0, 0, 1) + expect( + solver.state.regionIntersectionCaches[0].existingSameLayerIntersections, + ).toBe(1) + + solver.resetRoutingStateForRerip() + const restoredCache = solver.state.regionIntersectionCaches[0] + expect(restoredCache.netIds.length).toBe(1) + expect(restoredCache.existingSameLayerIntersections).toBe(0) + expect(Array.from(restoredCache.x1 ?? [])).toEqual([0]) + expect(Array.from(restoredCache.x2 ?? [])).toEqual([2]) +}) diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 16431f3..a347788 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -10,13 +10,14 @@ import type { RegionIntersectionCache } from "lib/types" const createRegionCache = ( existingRegionCost: number, + existingSameLayerIntersections = 0, ): RegionIntersectionCache => ({ netIds: new Int32Array(0), lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), existingCrossingLayerIntersections: 0, - existingSameLayerIntersections: 0, + existingSameLayerIntersections, existingEntryExitLayerChanges: 0, existingRegionCost, existingSegmentCount: 0, @@ -159,6 +160,38 @@ test("completed routing rerips when a region exceeds the current threshold", () expect(solver.state.goalPortId).toBe(-1) }) +test("zero-intersection mode ignores non-intersection region cost", () => { + const solver = createTestSolver({ REQUIRE_ZERO_INTERSECTIONS: true }) + + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([0, 0, 1, 1]) + solver.state.regionSegments[0] = [[0, 0, 1]] + solver.state.regionSegments[1] = [[1, 2, 3]] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.5) + solver.state.regionIntersectionCaches[1] = createRegionCache(0.1) + + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.state.ripCount).toBe(0) +}) + +test("zero-intersection mode rerips every real intersection", () => { + const solver = createTestSolver({ REQUIRE_ZERO_INTERSECTIONS: true }) + + solver.state.unroutedRoutes = [] + solver.state.portAssignment.set([0, 0, 1, 1]) + solver.state.regionSegments[0] = [[0, 0, 1]] + solver.state.regionSegments[1] = [[1, 2, 3]] + solver.state.regionIntersectionCaches[0] = createRegionCache(0.01, 1) + solver.state.regionIntersectionCaches[1] = createRegionCache(0.01) + + solver.step() + + expect(solver.solved).toBe(false) + expect(solver.state.ripCount).toBe(1) +}) + test("completed routing can be accepted as best solution on timeout", () => { const solver = createTestSolver({ MAX_ITERATIONS: 1 })