diff --git a/lib/DuplicateCongestedPortSolver.ts b/lib/DuplicateCongestedPortSolver.ts index 2de9653..ea8f86b 100644 --- a/lib/DuplicateCongestedPortSolver.ts +++ b/lib/DuplicateCongestedPortSolver.ts @@ -272,6 +272,7 @@ const createSingleRouteProblem = ( routeEndPort: Int32Array.from([problem.routeEndPort[routeId]]), routeNet: Int32Array.from([problem.routeNet[routeId]]), regionNetId: new Int32Array(problem.regionNetId), + portNetId: problem.portNetId && new Int32Array(problem.portNetId), portPenalty: problem.portPenalty === undefined ? undefined diff --git a/lib/core.ts b/lib/core.ts index da9b133..c6b3ab8 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -143,6 +143,9 @@ export interface TinyHyperGraphProblem { /** regionNetId[regionId] = reserved net id for the region, -1 means freely traversable */ regionNetId: Int32Array + /** Optional port net restrictions: -1 is free, -2 is blocked, otherwise the sole allowed net. */ + portNetId?: Int32Array + /** portPenalty[portId] = extra cost paid when a route traverses the port */ portPenalty?: Float64Array @@ -638,6 +641,11 @@ export class TinyHyperGraphSolver extends BaseSolver { } } + for (let portId = 0; portId < topology.portCount; portId++) { + const netId = problem.portNetId?.[portId] ?? -1 + if (netId !== -1) recordEndpointNet(portId, netId) + } + for (let routeId = 0; routeId < problem.routeCount; routeId++) { const routeNetId = problem.routeNet[routeId]! recordEndpointNet(problem.routeStartPort[routeId]!, routeNetId) @@ -667,6 +675,24 @@ export class TinyHyperGraphSolver extends BaseSolver { override _setup() { void this.problemSetup + if (this.problem.portNetId) { + for (let routeId = 0; routeId < this.problem.routeCount; routeId++) { + for (const portId of [ + this.problem.routeStartPort[routeId]!, + this.problem.routeEndPort[routeId]!, + ]) { + const requiredNetId = this.problem.portNetId[portId]! + if ( + requiredNetId === -1 || + requiredNetId === this.problem.routeNet[routeId] + ) continue + this.failed = true + this.error = `Route ${routeId} endpoint ${portId} violates its port net restriction` + return + } + } + } + if (this.STATIC_REACHABILITY_PRECHECK) { const staticallyUnroutableRoutes = getStaticallyUnroutableRoutes({ topology: this.topology, diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index 9ccd4f9..5a10a90 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -123,6 +123,7 @@ const createProblemWithPortSectionMask = ( routeEndPort: new Int32Array(problem.routeEndPort), routeNet: new Int32Array(problem.routeNet), regionNetId: new Int32Array(problem.regionNetId), + portNetId: problem.portNetId && new Int32Array(problem.portNetId), portPenalty: problem.portPenalty === undefined ? undefined diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index e66f4a1..6b1753e 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -551,6 +551,7 @@ const createSectionRoutePlans = ( routeEndPort, routeNet: new Int32Array(problem.routeNet), regionNetId: new Int32Array(problem.regionNetId), + portNetId: problem.portNetId && new Int32Array(problem.portNetId), portPenalty: problem.portPenalty === undefined ? undefined diff --git a/lib/selective-rerip-tiny-hyper-graph-solver.ts b/lib/selective-rerip-tiny-hyper-graph-solver.ts index 83874dc..285ad57 100644 --- a/lib/selective-rerip-tiny-hyper-graph-solver.ts +++ b/lib/selective-rerip-tiny-hyper-graph-solver.ts @@ -16,6 +16,13 @@ type RelaxedSearchState = { nextRegionId: RegionId } +type CommittedCrossingGeometry = { + ownerRouteId: RouteId + lesserAngle: number + greaterAngle: number + layerMask: number +} + type PortDistanceSearch = { costs: Float64Array settled: Uint8Array @@ -428,6 +435,12 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH } const portOwners = this.getPortOwners() + // Committed segments stay unchanged throughout this synchronous search. + // Keep geometry local so the next search observes any intervening rerips. + const crossingGeometryByRegion = new Map< + RegionId, + CommittedCrossingGeometry[] + >() return findResourceBlockerPath({ start: { portId: startPortId, nextRegionId: startRegionId }, getStateKey: ({ portId, nextRegionId }): number => @@ -440,15 +453,19 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH routeNetId, portOwners, forbiddenOwnerRouteIds, + crossingGeometryByRegion, }), - getBlockerCost: (hop): number => - (hop.data?.resources ?? []).reduce((cost, resource) => { + getBlockerCost: (hop): number => { + let cost = 0 + for (const resource of hop.data?.resources ?? []) { const previousConflicts = this.blockerResourceHistory.get( this.getBlockerResourceKey(resource), ) ?? 0 - return cost + resource.owners.length * (1 + previousConflicts) - }, 0), + cost += resource.owners.length * (1 + previousConflicts) + } + return cost + }, maxExpandedLabels: this.getRelaxedSearchExpansionLimit(), }) } @@ -498,6 +515,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH routeNetId: number portOwners: ReadonlyMap> forbiddenOwnerRouteIds: ReadonlySet + crossingGeometryByRegion: Map }): Array<{ state: RelaxedSearchState distance: number @@ -525,16 +543,20 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH continue } - const resources = this.getHopBlockerResources({ - regionId: state.nextRegionId, - fromPortId: state.portId, - toPortId: neighborPortId, + const resources = this.getHopBlockerResources( + state.nextRegionId, + state.portId, + neighborPortId, routeNetId, - portOwners: params.portOwners, - }) - const owners = [ - ...new Set(resources.flatMap((resource) => resource.owners)), - ] + params.portOwners, + params.crossingGeometryByRegion, + ) + const owners: RouteId[] = [] + for (const resource of resources) { + for (const owner of resource.owners) { + if (!owners.includes(owner)) owners.push(owner) + } + } if ( owners.some((ownerRouteId) => params.forbiddenOwnerRouteIds.has(ownerRouteId), @@ -586,40 +608,46 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH return hops } - private getHopBlockerResources(params: { - regionId: RegionId - fromPortId: PortId - toPortId: PortId - routeNetId: number - portOwners: ReadonlyMap> - }): SelectiveReripBlockerResource[] { + private getHopBlockerResources( + regionId: RegionId, + fromPortId: PortId, + toPortId: PortId, + routeNetId: number, + portOwners: ReadonlyMap>, + crossingGeometryByRegion: Map, + ): SelectiveReripBlockerResource[] { const resources: SelectiveReripBlockerResource[] = [] - const assignedNetId = this.state.portAssignment[params.toPortId]! - if (assignedNetId !== -1 && assignedNetId !== params.routeNetId) { - const owners = [ - ...(params.portOwners.get(params.toPortId) ?? new Set()), - ].filter( - (routeId) => this.problem.routeNet[routeId] !== params.routeNetId, - ) + const assignedNetId = this.state.portAssignment[toPortId]! + if (assignedNetId !== -1 && assignedNetId !== routeNetId) { + const owners: RouteId[] = [] + const toPortOwners = portOwners.get(toPortId) + if (toPortOwners) { + for (const routeId of toPortOwners) { + if (this.problem.routeNet[routeId] !== routeNetId) { + owners.push(routeId) + } + } + } if (owners.length === 0) { throw new Error( - `SelectiveReripTinyHyperGraphSolver: port ${params.toPortId} is assigned to foreign net ${assignedNetId} without a committed route owner`, + `SelectiveReripTinyHyperGraphSolver: port ${toPortId} is assigned to foreign net ${assignedNetId} without a committed route owner`, ) } - resources.push({ kind: "port", portId: params.toPortId, owners }) + resources.push({ kind: "port", portId: toPortId, owners }) } const sameLayerIntersectionOwners = this.getHardBlockedCrossingOwners( - params.regionId, - params.fromPortId, - params.toPortId, + regionId, + fromPortId, + toPortId, + crossingGeometryByRegion, ) if (sameLayerIntersectionOwners.length > 0) { resources.push({ kind: "same_layer_intersection", - regionId: params.regionId, - fromPortId: params.fromPortId, - toPortId: params.toPortId, + regionId, + fromPortId, + toPortId, owners: sameLayerIntersectionOwners, }) } @@ -646,6 +674,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH regionId: RegionId, fromPortId: PortId, toPortId: PortId, + crossingGeometryByRegion: Map, ): RouteId[] { if (!this.isKnownSingleLayerRegion(regionId)) return [] @@ -655,64 +684,62 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH "SelectiveReripTinyHyperGraphSolver: crossing ownership requires a current route net", ) } - const owners = new Set() - for (const [ownerRouteId, ownerFromPortId, ownerToPortId] of this.state - .regionSegments[regionId] ?? []) { - if (this.problem.routeNet[ownerRouteId] === routeNetId) continue - if ( - this.segmentsCrossOnSameLayer( + let committed = crossingGeometryByRegion.get(regionId) + if (!committed) { + committed = [] + for (const [ownerRouteId, ownerFromPortId, ownerToPortId] of this.state + .regionSegments[regionId] ?? []) { + if (this.problem.routeNet[ownerRouteId] === routeNetId) continue + const geometry = this.populateSegmentGeometryScratch( regionId, - fromPortId, - toPortId, ownerFromPortId, ownerToPortId, ) - ) { - owners.add(ownerRouteId) + committed.push({ + ownerRouteId, + lesserAngle: geometry.lesserAngle, + greaterAngle: geometry.greaterAngle, + layerMask: geometry.layerMask, + }) } + crossingGeometryByRegion.set(regionId, committed) } - - return [...owners] - } - - private segmentsCrossOnSameLayer( - regionId: RegionId, - firstFromPortId: PortId, - firstToPortId: PortId, - secondFromPortId: PortId, - secondToPortId: PortId, - ): boolean { - const first = { - ...this.populateSegmentGeometryScratch( - regionId, - firstFromPortId, - firstToPortId, - ), - } - const second = { - ...this.populateSegmentGeometryScratch( - regionId, - secondFromPortId, - secondToPortId, - ), - } - 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 + const owners: RouteId[] = [] + if (committed.length === 0) return owners + const first = this.populateSegmentGeometryScratch( + regionId, + fromPortId, + toPortId, + ) + const firstLesserAngle = first.lesserAngle + const firstGreaterAngle = first.greaterAngle + const firstLayerMask = first.layerMask + for (const second of committed) { + const ownerRouteId = second.ownerRouteId + if ((firstLayerMask & second.layerMask) === 0) continue + if ( + firstLesserAngle === second.lesserAngle || + firstLesserAngle === second.greaterAngle || + firstGreaterAngle === second.lesserAngle || + firstGreaterAngle === second.greaterAngle + ) { + continue + } + const secondLesserInsideFirst = + firstLesserAngle < second.lesserAngle && + second.lesserAngle < firstGreaterAngle + const secondGreaterInsideFirst = + firstLesserAngle < second.greaterAngle && + second.greaterAngle < firstGreaterAngle + if ( + secondLesserInsideFirst !== secondGreaterInsideFirst && + !owners.includes(ownerRouteId) + ) { + owners.push(ownerRouteId) + } } - const secondLesserInsideFirst = - first.lesserAngle < second.lesserAngle && - second.lesserAngle < first.greaterAngle - const secondGreaterInsideFirst = - first.lesserAngle < second.greaterAngle && - second.greaterAngle < first.greaterAngle - return secondLesserInsideFirst !== secondGreaterInsideFirst + return owners } private rebuildCommittedState(rippedRouteIds: ReadonlySet): void { diff --git a/tests/selective-rerip-crossing-ownership-refresh.test.ts b/tests/selective-rerip-crossing-ownership-refresh.test.ts new file mode 100644 index 0000000..d2eae3e --- /dev/null +++ b/tests/selective-rerip-crossing-ownership-refresh.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test" +import type { + TinyHyperGraphProblem, + TinyHyperGraphTopology, +} from "../lib/core" +import { SelectiveReripTinyHyperGraphSolver } from "../lib/selective-rerip-tiny-hyper-graph-solver" + +test("blocker search preserves owner order across committed route changes", () => { + const topology: TinyHyperGraphTopology = { + portCount: 4, + regionCount: 5, + regionIncidentPorts: [[0, 1, 2, 3], [], [], [], []], + incidentPortRegion: [ + [0, 1], + [0, 2], + [0, 3], + [0, 4], + ], + regionWidth: new Float64Array(5).fill(10), + regionHeight: new Float64Array(5).fill(10), + regionCenterX: new Float64Array(5), + regionCenterY: new Float64Array(5), + regionAvailableZMask: new Int32Array(5).fill(1), + portAngleForRegion1: new Int32Array([0, 180, 90, 270]), + portAngleForRegion2: new Int32Array(4), + portX: new Float64Array([-5, 5, 0, 0]), + portY: new Float64Array([0, 0, -5, 5]), + portZ: new Int32Array(4), + } + const problem: TinyHyperGraphProblem = { + routeCount: 3, + portSectionMask: new Int8Array(4).fill(1), + routeStartPort: new Int32Array([0, 2, 2]), + routeEndPort: new Int32Array([1, 3, 3]), + routeNet: new Int32Array([0, 1, 1]), + regionNetId: new Int32Array(5).fill(-1), + } + class SearchableSolver extends SelectiveReripTinyHyperGraphSolver { + searchOwners(): number[] { + const result = this.findRelaxedBlockerPath() + if (!result.found) throw new Error("Expected a blocker path") + return [...result.owners] + } + } + const solver = new SearchableSolver(topology, problem, { + STATIC_REACHABILITY_PRECHECK: false, + PARTIAL_RIP_ENABLED: false, + OUTSIDE_IN_ROUTING: false, + }) + solver.state.currentRouteId = 0 + solver.state.currentRouteNetId = 0 + solver.state.goalPortId = 1 + solver.state.regionSegments[0] = [ + [2, 2, 3], + [1, 2, 3], + [2, 2, 3], + ] + + expect(solver.searchOwners()).toEqual([2, 1]) + + solver.state.regionSegments[0] = [] + expect(solver.searchOwners()).toEqual([]) + + solver.state.regionSegments[0] = [[1, 2, 3]] + expect(solver.searchOwners()).toEqual([1]) +}) diff --git a/tests/solver/port-net-id.test.ts b/tests/solver/port-net-id.test.ts new file mode 100644 index 0000000..f6d2eb6 --- /dev/null +++ b/tests/solver/port-net-id.test.ts @@ -0,0 +1,61 @@ +import { expect, test } from "bun:test" +import { + TinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphTopology, +} from "lib/index" + +test("port restrictions preserve same-net access and reject foreign or blocked copper", () => { + for (const reservedNetId of [-1, 0, 1, -2]) { + const topology: TinyHyperGraphTopology = { + portCount: 4, + regionCount: 5, + regionIncidentPorts: [[0, 1], [1, 2], [2, 3], [0], [3]], + incidentPortRegion: [ + [0, 3], + [0, 1], + [1, 2], + [2, 4], + ], + regionWidth: new Float64Array(5).fill(1), + regionHeight: new Float64Array(5).fill(1), + regionCenterX: new Float64Array(5).fill(0), + regionCenterY: new Float64Array(5).fill(0), + portAngleForRegion1: new Int32Array(4), + portAngleForRegion2: new Int32Array(4), + portX: new Float64Array([0, 1, 2, 3]), + portY: new Float64Array(4), + portZ: new Int32Array(4), + } + + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array(4).fill(1), + routeMetadata: [{ connectionId: "blocked-route" }], + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([3]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array(5).fill(-1), + portNetId: Int32Array.from([-1, reservedNetId, -1, -1]), + } + + const solver = new TinyHyperGraphSolver(topology, problem) + solver.setup() + const blocked = reservedNetId === 1 || reservedNetId === -2 + expect(solver.failed).toBe(blocked) + solver.state.currentRouteNetId = 0 + expect(solver.isPortReservedForDifferentNet(1)).toBe(blocked) + if (blocked) { + expect(solver.stats.staticallyUnroutableRouteCount).toBe(1) + } + for (const endpoint of [0, 3]) { + problem.portNetId = new Int32Array(4).fill(-1) + problem.portNetId[endpoint] = reservedNetId + const endpointSolver = new TinyHyperGraphSolver(topology, problem, { + STATIC_REACHABILITY_PRECHECK: false, + }) + endpointSolver.setup() + expect(endpointSolver.failed).toBe(blocked) + } + } +})