From 8a22e571689e82e130a8ea2ba82a8d18cebd07db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 27 May 2026 20:54:51 +0530 Subject: [PATCH 1/5] Keep endpoint-connected sibling obstacle regions --- lib/compat/loadSerializedHyperGraph.ts | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 6dc4eba..2116b7e 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -41,12 +41,47 @@ const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { connectedRegionIds.add(connection.endRegionId) } + const fullObstacleRegions = new Map( + serializedHyperGraph.regions + .filter((region) => isFullObstacleRegion(region)) + .map((region) => [region.regionId, region]), + ) + + const preservedObstacleRegionIds = new Set() + const obstacleQueue: string[] = [] + + for (const regionId of connectedRegionIds) { + if (!fullObstacleRegions.has(regionId)) continue + preservedObstacleRegionIds.add(regionId) + obstacleQueue.push(regionId) + } + + while (obstacleQueue.length > 0) { + const currentRegionId = obstacleQueue.shift()! + + for (const port of serializedHyperGraph.ports) { + const neighborRegionId = + port.region1Id === currentRegionId + ? port.region2Id + : port.region2Id === currentRegionId + ? port.region1Id + : null + + if (neighborRegionId === null) continue + if (!fullObstacleRegions.has(neighborRegionId)) continue + if (preservedObstacleRegionIds.has(neighborRegionId)) continue + + preservedObstacleRegionIds.add(neighborRegionId) + obstacleQueue.push(neighborRegionId) + } + } + const removedRegionIds = new Set( serializedHyperGraph.regions .filter( (region) => isFullObstacleRegion(region) && - !connectedRegionIds.has(region.regionId), + !preservedObstacleRegionIds.has(region.regionId), ) .map((region) => region.regionId), ) From acdc25960b422609739ef9b5f2be248cc4f2cddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 27 May 2026 21:44:13 +0530 Subject: [PATCH 2/5] update test --- tests/solver/ddr5-dense-allocation-repro.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/solver/ddr5-dense-allocation-repro.test.ts b/tests/solver/ddr5-dense-allocation-repro.test.ts index 49288d2..c63103e 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": 4376940276, + "denseHopCount": 364745023, + "portCount": 28277, + "regionCount": 12899, "routeCount": 79, "source": "SRG 18 pipeline 7 circuit 6", } From 2fa262b8cc7ca81caec1de7be237b75c945d28cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 28 May 2026 01:25:17 +0530 Subject: [PATCH 3/5] refactor --- lib/compat/loadSerializedHyperGraph.ts | 159 ++++++++++++++++++------- 1 file changed, 117 insertions(+), 42 deletions(-) diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 2116b7e..b465912 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,30 +36,87 @@ const isFullObstacleRegion = ( return netId === undefined || netId === -1 } -const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { - const connectedRegionIds = new Set() - for (const connection of serializedHyperGraph.connections ?? []) { - connectedRegionIds.add(connection.startRegionId) - connectedRegionIds.add(connection.endRegionId) +/** + * 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", + ) +} + +/** + * 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 resolve to exactly one obstacle root id, this + * requires an exact root match. Ambiguous multi-root regions intentionally fall + * back to the previous adjacency-only behavior to avoid dropping legacy data. + */ +const sharesObstacleRootId = ( + regionA: SerializedRegion | undefined, + regionB: SerializedRegion | undefined, +) => { + if (!regionA || !regionB) return false + + const rootIdsA = getObstacleRootIds(regionA) + const rootIdsB = getObstacleRootIds(regionB) + + if (rootIdsA.length === 1 && rootIdsB.length === 1) { + return rootIdsA[0] === rootIdsB[0] } - const fullObstacleRegions = new Map( + 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 (!fullObstacleRegions.has(regionId)) continue + if (!fullObstacleRegionById.has(regionId)) continue preservedObstacleRegionIds.add(regionId) obstacleQueue.push(regionId) } - while (obstacleQueue.length > 0) { - const currentRegionId = obstacleQueue.shift()! + while (queueIndex < obstacleQueue.length) { + const currentRegionId = obstacleQueue[queueIndex++]! for (const port of serializedHyperGraph.ports) { const neighborRegionId = @@ -68,14 +127,39 @@ const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { : null if (neighborRegionId === null) continue - if (!fullObstacleRegions.has(neighborRegionId)) continue + if (!fullObstacleRegionById.has(neighborRegionId)) continue if (preservedObstacleRegionIds.has(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( @@ -118,7 +202,7 @@ const filterObstacleRegions = (serializedHyperGraph: SerializedHyperGraph) => { } const addSerializedRegionIdToMetadata = ( - region: SerializedHyperGraph["regions"][number], + region: SerializedRegion, layer: string, ) => { const metadata = @@ -138,10 +222,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 } @@ -159,7 +240,7 @@ const addSerializedPortIdToMetadata = ( return metadata } -const getRegionBounds = (region: SerializedHyperGraph["regions"][number]) => { +const getRegionBounds = (region: SerializedRegion) => { const bounds = region.d?.bounds if (bounds) { return bounds @@ -192,7 +273,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 = @@ -218,9 +299,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 @@ -237,24 +316,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 @@ -320,8 +395,8 @@ const computePortAngle = ( } const getCentermostPortIdForRegion = ( - region: SerializedHyperGraph["regions"][number] | undefined, - portById: Map, + region: SerializedRegion | undefined, + portById: Map, ): string | undefined => { if (!region) return undefined @@ -349,7 +424,7 @@ const getCentermostPortIdForRegion = ( const getSharedPortIdsForConnection = ( serializedHyperGraph: SerializedHyperGraph, - connection: NonNullable[number], + connection: SerializedConnection, ): string[] => serializedHyperGraph.ports .filter( @@ -371,7 +446,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, From daf66ff3ff089af4c6267b988b91ed6a1f348d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 28 May 2026 02:07:47 +0530 Subject: [PATCH 4/5] fix --- lib/compat/loadSerializedHyperGraph.ts | 40 ++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index b465912..8f8d0ed 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -36,6 +36,9 @@ const isFullObstacleRegion = (region: SerializedRegion) => { return netId === undefined || netId === -1 } +const isTargetContainingObstacleRegion = (region: SerializedRegion) => + region.d?._containsObstacle === true && region.d?._containsTarget === true + /** * Normalizes serialized obstacle root ids into a string list. * @@ -55,6 +58,22 @@ const getObstacleRootIds = (region: SerializedRegion): 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. @@ -63,9 +82,9 @@ const getObstacleRootIds = (region: SerializedRegion): string[] => { * @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 resolve to exactly one obstacle root id, this - * requires an exact root match. Ambiguous multi-root regions intentionally fall - * back to the previous adjacency-only behavior to avoid dropping legacy data. + * @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, @@ -76,8 +95,8 @@ const sharesObstacleRootId = ( const rootIdsA = getObstacleRootIds(regionA) const rootIdsB = getObstacleRootIds(regionB) - if (rootIdsA.length === 1 && rootIdsB.length === 1) { - return rootIdsA[0] === rootIdsB[0] + if (rootIdsA.length > 0 && rootIdsB.length > 0) { + return rootIdsA.some((rootId) => rootIdsB.includes(rootId)) } return true @@ -129,6 +148,17 @@ const getPreservedObstacleRegionIds = ( 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), From 953b843062d80996173131e770da1ca7297eb760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 28 May 2026 02:59:23 +0530 Subject: [PATCH 5/5] udpate test --- tests/solver/ddr5-dense-allocation-repro.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/solver/ddr5-dense-allocation-repro.test.ts b/tests/solver/ddr5-dense-allocation-repro.test.ts index c63103e..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": 4376940276, - "denseHopCount": 364745023, - "portCount": 28277, - "regionCount": 12899, + "denseHopBytes": 4287276000, + "denseHopCount": 357273000, + "portCount": 27825, + "regionCount": 12840, "routeCount": 79, "source": "SRG 18 pipeline 7 circuit 6", }