diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 6dc4eba..8f8d0ed 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -6,9 +6,13 @@ import type { } from "../index" import { getAvailableZFromMask, getZLayerLabel } from "../layerLabels" -const getSerializedRegionNetId = ( - region: SerializedHyperGraph["regions"][number], -) => { +type SerializedRegion = SerializedHyperGraph["regions"][number] +type SerializedPort = SerializedHyperGraph["ports"][number] +type SerializedConnection = NonNullable< + SerializedHyperGraph["connections"] +>[number] + +const getSerializedRegionNetId = (region: SerializedRegion) => { const netId = typeof region.d?.netId === "number" ? region.d.netId @@ -19,9 +23,7 @@ const getSerializedRegionNetId = ( return Number.isFinite(netId) ? netId : undefined } -const isFullObstacleRegion = ( - region: SerializedHyperGraph["regions"][number], -) => { +const isFullObstacleRegion = (region: SerializedRegion) => { if (region.d?._containsObstacle !== true) { return false } @@ -34,19 +36,166 @@ const isFullObstacleRegion = ( return netId === undefined || netId === -1 } -const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { +const isTargetContainingObstacleRegion = (region: SerializedRegion) => + region.d?._containsObstacle === true && region.d?._containsTarget === true + +/** + * Normalizes serialized obstacle root ids into a string list. + * + * @param region - Obstacle region whose provenance metadata may include + * `_obstacleRootIds`. + * @returns Every string-valued root id in `_obstacleRootIds`, or an empty list + * when the metadata is absent or malformed. + */ +const getObstacleRootIds = (region: SerializedRegion): string[] => { + const rootIds = region.d?._obstacleRootIds + if (!Array.isArray(rootIds)) { + return [] + } + + return rootIds.filter( + (rootId): rootId is string => typeof rootId === "string", + ) +} + +const hasExactlyOneSharedObstacleRootId = ( + regionA: SerializedRegion | undefined, + regionB: SerializedRegion | undefined, +) => { + if (!regionA || !regionB) return false + + const rootIdsA = getObstacleRootIds(regionA) + const rootIdsB = getObstacleRootIds(regionB) + + return ( + rootIdsA.length === 1 && + rootIdsB.length === 1 && + rootIdsA[0] === rootIdsB[0] + ) +} + +/** + * Decides whether two full-obstacle regions can be traversed as the same + * obstacle cluster during preservation. + * + * @param regionA - Current preserved obstacle region. + * @param regionB - Adjacent obstacle region under consideration. + * @returns `true` when both regions can be treated as belonging to the same + * obstacle cluster, otherwise `false`. + * @note When both regions provide obstacle root metadata, this requires at + * least one shared root id. The legacy adjacency-only fallback is used only + * when one side lacks obstacle ancestry metadata entirely. + */ +const sharesObstacleRootId = ( + regionA: SerializedRegion | undefined, + regionB: SerializedRegion | undefined, +) => { + if (!regionA || !regionB) return false + + const rootIdsA = getObstacleRootIds(regionA) + const rootIdsB = getObstacleRootIds(regionB) + + if (rootIdsA.length > 0 && rootIdsB.length > 0) { + return rootIdsA.some((rootId) => rootIdsB.includes(rootId)) + } + + return true +} + +/** + * Walks obstacle-only regions that are still relevant to at least one + * connection endpoint. + * + * @param serializedHyperGraph - Serialized hypergraph to prune. + * @returns Region ids for full-obstacle regions that should remain in the + * graph after preserving connected obstacle clusters. + */ +const getPreservedObstacleRegionIds = ( + serializedHyperGraph: SerializedHyperGraph, +): Set => { const connectedRegionIds = new Set() + const fullObstacleRegionById = new Map( + serializedHyperGraph.regions + .filter((region) => isFullObstacleRegion(region)) + .map((region) => [region.regionId, region]), + ) + const preservedObstacleRegionIds = new Set() + const obstacleQueue: string[] = [] + let queueIndex = 0 + for (const connection of serializedHyperGraph.connections ?? []) { connectedRegionIds.add(connection.startRegionId) connectedRegionIds.add(connection.endRegionId) } + for (const regionId of connectedRegionIds) { + if (!fullObstacleRegionById.has(regionId)) continue + preservedObstacleRegionIds.add(regionId) + obstacleQueue.push(regionId) + } + + while (queueIndex < obstacleQueue.length) { + const currentRegionId = obstacleQueue[queueIndex++]! + + for (const port of serializedHyperGraph.ports) { + const neighborRegionId = + port.region1Id === currentRegionId + ? port.region2Id + : port.region2Id === currentRegionId + ? port.region1Id + : null + + if (neighborRegionId === null) continue + if (!fullObstacleRegionById.has(neighborRegionId)) continue + if (preservedObstacleRegionIds.has(neighborRegionId)) continue + if ( + isTargetContainingObstacleRegion( + fullObstacleRegionById.get(neighborRegionId)!, + ) && + !hasExactlyOneSharedObstacleRootId( + fullObstacleRegionById.get(currentRegionId), + fullObstacleRegionById.get(neighborRegionId), + ) + ) { + continue + } + if ( + !sharesObstacleRootId( + fullObstacleRegionById.get(currentRegionId), + fullObstacleRegionById.get(neighborRegionId), + ) + ) { + continue + } + + preservedObstacleRegionIds.add(neighborRegionId) + obstacleQueue.push(neighborRegionId) + } + } + + return preservedObstacleRegionIds +} + +/** + * Removes disconnected obstacle-only regions that should not participate in + * routing. + * + * @param serializedHyperGraph - Serialized graph to sanitize before loading it + * into the tiny hypergraph topology arrays. + * @returns The original graph when nothing is removed, otherwise a shallow copy + * with filtered `regions` and `ports`. + * @caution This throws when a serialized connection still references a removed + * obstacle region because that graph cannot be loaded safely. + */ +const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { + const preservedObstacleRegionIds = + getPreservedObstacleRegionIds(serializedHyperGraph) const removedRegionIds = new Set( serializedHyperGraph.regions .filter( (region) => isFullObstacleRegion(region) && - !connectedRegionIds.has(region.regionId), + !preservedObstacleRegionIds.has(region.regionId), ) .map((region) => region.regionId), ) @@ -83,7 +232,7 @@ const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { } const addSerializedRegionIdToMetadata = ( - region: SerializedHyperGraph["regions"][number], + region: SerializedRegion, layer: string, ) => { const metadata = @@ -103,10 +252,7 @@ const addSerializedRegionIdToMetadata = ( return metadata } -const addSerializedPortIdToMetadata = ( - port: SerializedHyperGraph["ports"][number], - layer: string, -) => { +const addSerializedPortIdToMetadata = (port: SerializedPort, layer: string) => { const metadata = port.d && typeof port.d === "object" && !Array.isArray(port.d) ? { ...port.d } @@ -124,7 +270,7 @@ const addSerializedPortIdToMetadata = ( return metadata } -const getRegionBounds = (region: SerializedHyperGraph["regions"][number]) => { +const getRegionBounds = (region: SerializedRegion) => { const bounds = region.d?.bounds if (bounds) { return bounds @@ -157,7 +303,7 @@ const getRegionBounds = (region: SerializedHyperGraph["regions"][number]) => { } const getRegionGeometry = ( - region: SerializedHyperGraph["regions"][number], + region: SerializedRegion, ): { centerX: number; centerY: number; width: number; height: number } => { const bounds = getRegionBounds(region) const width = @@ -183,9 +329,7 @@ const getRegionGeometry = ( } } -const getRegionAvailableZMask = ( - region: SerializedHyperGraph["regions"][number], -): number => { +const getRegionAvailableZMask = (region: SerializedRegion): number => { const availableZ = region.d?.availableZ if (!Array.isArray(availableZ)) { return 0 @@ -202,24 +346,20 @@ const getRegionAvailableZMask = ( return mask } -const getSerializedPortZ = ( - port: SerializedHyperGraph["ports"][number], -): number => { +const getSerializedPortZ = (port: SerializedPort): number => { const z = Number(port.d?.z ?? 0) return Number.isFinite(z) ? z : 0 } -const getSerializedPortX = ( - port: SerializedHyperGraph["ports"][number], -): number => Number(port.d?.x ?? 0) +const getSerializedPortX = (port: SerializedPort): number => + Number(port.d?.x ?? 0) -const getSerializedPortY = ( - port: SerializedHyperGraph["ports"][number], -): number => Number(port.d?.y ?? 0) +const getSerializedPortY = (port: SerializedPort): number => + Number(port.d?.y ?? 0) const computePortAngle = ( - port: SerializedHyperGraph["ports"][number], - region: SerializedHyperGraph["regions"][number] | undefined, + port: SerializedPort, + region: SerializedRegion | undefined, ): number => { if (!region) return 0 @@ -285,8 +425,8 @@ const computePortAngle = ( } const getCentermostPortIdForRegion = ( - region: SerializedHyperGraph["regions"][number] | undefined, - portById: Map, + region: SerializedRegion | undefined, + portById: Map, ): string | undefined => { if (!region) return undefined @@ -314,7 +454,7 @@ const getCentermostPortIdForRegion = ( const getSharedPortIdsForConnection = ( serializedHyperGraph: SerializedHyperGraph, - connection: NonNullable[number], + connection: SerializedConnection, ): string[] => serializedHyperGraph.ports .filter( @@ -336,7 +476,7 @@ export const loadSerializedHyperGraph = ( const filteredHyperGraph = filterObstacleRegions(serializedHyperGraph) const regionIdToIndex = new Map() const portIdToIndex = new Map() - const portById = new Map() + const portById = new Map() const solvedRouteByConnectionId = new Map( (filteredHyperGraph.solvedRoutes ?? []).map((route) => [ route.connection.connectionId, diff --git a/tests/solver/ddr5-dense-allocation-repro.test.ts b/tests/solver/ddr5-dense-allocation-repro.test.ts index 49288d2..17ed670 100644 --- a/tests/solver/ddr5-dense-allocation-repro.test.ts +++ b/tests/solver/ddr5-dense-allocation-repro.test.ts @@ -164,10 +164,10 @@ test("repro: DDR5 pipeline7 port-point-pathing input implies multi-GB dense hop }).toMatchInlineSnapshot(` { "boardName": "DDR5", - "denseHopBytes": 4209756768, - "denseHopCount": 350813064, - "portCount": 27597, - "regionCount": 12712, + "denseHopBytes": 4287276000, + "denseHopCount": 357273000, + "portCount": 27825, + "regionCount": 12840, "routeCount": 79, "source": "SRG 18 pipeline 7 circuit 6", }