Skip to content

Commit 8dc19cf

Browse files
authored
Merge pull request #16 from tscircuit/region-zmask
Add support for region z mask, fixes impossible single-layer nodes being created
2 parents 5b80494 + 9e270f5 commit 8dc19cf

6 files changed

Lines changed: 197 additions & 1 deletion

File tree

lib/compat/convertToSerializedHyperGraph.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ const toObjectRecord = (value: unknown): Record<string, unknown> => {
2828
const normalizePortIdFallback = (value: string) =>
2929
value.includes("::") ? value.slice(0, value.indexOf("::")) : value
3030

31+
const getAvailableZFromMask = (mask: number): number[] => {
32+
const availableZ: number[] = []
33+
34+
for (let z = 0; z < 31; z++) {
35+
if ((mask & (1 << z)) !== 0) {
36+
availableZ.push(z)
37+
}
38+
}
39+
40+
return availableZ
41+
}
42+
3143
const getSerializedRegionId = (
3244
solver: TinyHyperGraphSolver,
3345
regionId: RegionId,
@@ -89,6 +101,13 @@ const getSerializedRegionData = (
89101
data.height = solver.topology.regionHeight[regionId]
90102
}
91103

104+
if (!Array.isArray(data.availableZ)) {
105+
const availableZMask = solver.topology.regionAvailableZMask?.[regionId] ?? 0
106+
if (availableZMask !== 0) {
107+
data.availableZ = getAvailableZFromMask(availableZMask)
108+
}
109+
}
110+
92111
return data
93112
}
94113

lib/compat/loadSerializedHyperGraph.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ const getRegionGeometry = (
176176
}
177177
}
178178

179+
const getRegionAvailableZMask = (
180+
region: SerializedHyperGraph["regions"][number],
181+
): number => {
182+
const availableZ = region.d?.availableZ
183+
if (!Array.isArray(availableZ)) {
184+
return 0
185+
}
186+
187+
let mask = 0
188+
for (const z of availableZ) {
189+
if (!Number.isInteger(z) || z < 0 || z >= 31) {
190+
continue
191+
}
192+
mask |= 1 << z
193+
}
194+
195+
return mask
196+
}
197+
179198
const computePortAngle = (
180199
port: SerializedHyperGraph["ports"][number],
181200
region: SerializedHyperGraph["regions"][number] | undefined,
@@ -300,6 +319,7 @@ export const loadSerializedHyperGraph = (
300319
const regionHeight = new Float64Array(regionCount)
301320
const regionCenterX = new Float64Array(regionCount)
302321
const regionCenterY = new Float64Array(regionCount)
322+
const regionAvailableZMask = new Int32Array(regionCount)
303323
const regionNetId = new Int32Array(regionCount).fill(-1)
304324

305325
filteredHyperGraph.regions.forEach((region, regionIndex) => {
@@ -308,6 +328,7 @@ export const loadSerializedHyperGraph = (
308328
regionHeight[regionIndex] = geometry.height
309329
regionCenterX[regionIndex] = geometry.centerX
310330
regionCenterY[regionIndex] = geometry.centerY
331+
regionAvailableZMask[regionIndex] = getRegionAvailableZMask(region)
311332
})
312333

313334
const portAngleForRegion1 = new Int32Array(portCount)
@@ -450,6 +471,7 @@ export const loadSerializedHyperGraph = (
450471
regionHeight,
451472
regionCenterX,
452473
regionCenterY,
474+
regionAvailableZMask,
453475
regionMetadata: filteredHyperGraph.regions.map((region) =>
454476
addSerializedRegionIdToMetadata(region),
455477
),

lib/computeRegionCost.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const viaSize = 0.45
22
const viaSizeSq = viaSize ** 2
33
const traceWidth = 0.1
4+
const IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST = 10
5+
6+
const isKnownSingleLayerMask = (regionAvailableZMask: number) =>
7+
regionAvailableZMask === 1 || regionAvailableZMask === 2
48

59
export const computeRegionCost = (
610
regionWidth: number,
@@ -9,6 +13,7 @@ export const computeRegionCost = (
913
numCrossLayerIntersections: number,
1014
numEntryExitChanges: number,
1115
traceCount: number,
16+
regionAvailableZMask = 0,
1217
) => {
1318
const area = regionWidth * regionHeight
1419

@@ -18,6 +23,14 @@ export const computeRegionCost = (
1823
numEntryExitChanges * 1
1924

2025
const traceCountMult = 1 + traceCount / 5
26+
const impossibleSingleLayerIntersectionCost = isKnownSingleLayerMask(
27+
regionAvailableZMask,
28+
)
29+
? numSameLayerIntersections * IMPOSSIBLE_SINGLE_LAYER_INTERSECTION_COST
30+
: 0
2131

22-
return (estViasRequired * viaSizeSq * traceCountMult) / area
32+
return (
33+
(estViasRequired * viaSizeSq * traceCountMult) / area +
34+
impossibleSingleLayerIntersectionCost
35+
)
2336
}

lib/core.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export interface TinyHyperGraphTopology {
4444
regionHeight: Float64Array
4545
regionCenterX: Float64Array
4646
regionCenterY: Float64Array
47+
/**
48+
* regionAvailableZMask[regionId] is a bitmask of the routed layers available
49+
* within the region. A zero mask means "unknown", which preserves legacy cost
50+
* behavior for manually-constructed topologies that do not provide this data.
51+
*/
52+
regionAvailableZMask?: Int32Array
4753

4854
/** regionMetadata[regionId] = metadata for the region */
4955
regionMetadata?: any[]
@@ -392,6 +398,7 @@ export class TinyHyperGraphSolver extends BaseSolver {
392398
if (problem.portSectionMask[neighborPortId] === 0) continue
393399

394400
const g = this.computeG(currentCandidate, neighborPortId)
401+
if (!Number.isFinite(g)) continue
395402
const h = this.computeH(neighborPortId)
396403

397404
const nextRegionId =
@@ -504,6 +511,11 @@ export class TinyHyperGraphSolver extends BaseSolver {
504511
)
505512
}
506513

514+
isKnownSingleLayerRegion(regionId: RegionId): boolean {
515+
const regionAvailableZMask = this.topology.regionAvailableZMask?.[regionId] ?? 0
516+
return regionAvailableZMask === 1 || regionAvailableZMask === 2
517+
}
518+
507519
populateSegmentGeometryScratch(
508520
regionId: RegionId,
509521
port1Id: PortId,
@@ -600,6 +612,7 @@ export class TinyHyperGraphSolver extends BaseSolver {
600612
existingCrossingLayerIntersections,
601613
existingEntryExitLayerChanges,
602614
existingSegmentCount,
615+
topology.regionAvailableZMask?.[regionId] ?? 0,
603616
),
604617
}
605618
}
@@ -787,6 +800,13 @@ export class TinyHyperGraphSolver extends BaseSolver {
787800
segmentGeometry.entryExitLayerChanges,
788801
)
789802

803+
if (
804+
newSameLayerIntersections > 0 &&
805+
this.isKnownSingleLayerRegion(nextRegionId)
806+
) {
807+
return Number.POSITIVE_INFINITY
808+
}
809+
790810
const newRegionCost =
791811
computeRegionCost(
792812
topology.regionWidth[nextRegionId],
@@ -796,6 +816,7 @@ export class TinyHyperGraphSolver extends BaseSolver {
796816
newCrossLayerIntersections,
797817
regionCache.existingEntryExitLayerChanges + newEntryExitLayerChanges,
798818
regionCache.existingSegmentCount + 1,
819+
topology.regionAvailableZMask?.[nextRegionId] ?? 0,
799820
) - regionCache.existingRegionCost
800821

801822
return (
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect, test } from "bun:test"
2+
import type { SerializedHyperGraph } from "@tscircuit/hypergraph"
3+
import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph"
4+
5+
test("loadSerializedHyperGraph maps region availableZ arrays into layer masks", () => {
6+
const graph: SerializedHyperGraph = {
7+
regions: [
8+
{
9+
regionId: "single-layer",
10+
pointIds: [],
11+
d: {
12+
center: { x: 0, y: 0 },
13+
width: 1,
14+
height: 1,
15+
availableZ: [0],
16+
},
17+
},
18+
{
19+
regionId: "inner-layers",
20+
pointIds: [],
21+
d: {
22+
center: { x: 1, y: 0 },
23+
width: 1,
24+
height: 1,
25+
availableZ: [1, 2],
26+
},
27+
},
28+
{
29+
regionId: "unknown",
30+
pointIds: [],
31+
d: {
32+
center: { x: 2, y: 0 },
33+
width: 1,
34+
height: 1,
35+
},
36+
},
37+
],
38+
ports: [],
39+
connections: [],
40+
}
41+
42+
const { topology } = loadSerializedHyperGraph(graph)
43+
44+
expect(Array.from(topology.regionAvailableZMask ?? [])).toEqual([
45+
1 << 0,
46+
(1 << 1) | (1 << 2),
47+
0,
48+
])
49+
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test } from "bun:test"
2+
import {
3+
type TinyHyperGraphProblem,
4+
TinyHyperGraphSolver,
5+
type TinyHyperGraphTopology,
6+
} from "lib/index"
7+
8+
const createTopology = (
9+
regionAvailableZMask: number,
10+
portZ: number,
11+
): TinyHyperGraphTopology => ({
12+
portCount: 4,
13+
regionCount: 2,
14+
regionIncidentPorts: [[0, 1, 2, 3], []],
15+
incidentPortRegion: [
16+
[0, 1],
17+
[0, 1],
18+
[0, 1],
19+
[0, 1],
20+
],
21+
regionWidth: new Float64Array([3, 1]),
22+
regionHeight: new Float64Array([3, 1]),
23+
regionCenterX: new Float64Array(2).fill(0),
24+
regionCenterY: new Float64Array(2).fill(0),
25+
regionAvailableZMask: new Int32Array([regionAvailableZMask, 0]),
26+
portAngleForRegion1: new Int32Array([0, 9000, 18000, 27000]),
27+
portAngleForRegion2: new Int32Array(4),
28+
portX: new Float64Array([1, 0, -1, 0]),
29+
portY: new Float64Array([0, 1, 0, -1]),
30+
portZ: new Int32Array([portZ, portZ, portZ, portZ]),
31+
})
32+
33+
const createProblem = (): TinyHyperGraphProblem => ({
34+
routeCount: 2,
35+
portSectionMask: new Int8Array(4).fill(1),
36+
routeStartPort: new Int32Array([0, 1]),
37+
routeEndPort: new Int32Array([2, 3]),
38+
routeNet: new Int32Array([0, 1]),
39+
regionNetId: new Int32Array(2).fill(-1),
40+
})
41+
42+
const getCrossingCost = (regionAvailableZMask: number, portZ: number) => {
43+
const solver = new TinyHyperGraphSolver(
44+
createTopology(regionAvailableZMask, portZ),
45+
createProblem(),
46+
)
47+
48+
solver.state.currentRouteNetId = 0
49+
solver.appendSegmentToRegionCache(0, 0, 2)
50+
51+
solver.state.currentRouteNetId = 1
52+
return solver.computeG(
53+
{
54+
nextRegionId: 0,
55+
portId: 1,
56+
f: 0,
57+
g: 0,
58+
h: 0,
59+
},
60+
3,
61+
)
62+
}
63+
64+
test("same-layer crossings in known single-layer regions are rejected as candidates", () => {
65+
const topLayerCrossingCost = getCrossingCost(1 << 0, 0)
66+
const bottomLayerCrossingCost = getCrossingCost(1 << 1, 1)
67+
const multiLayerCrossingCost = getCrossingCost((1 << 0) | (1 << 1), 0)
68+
69+
expect(topLayerCrossingCost).toBe(Number.POSITIVE_INFINITY)
70+
expect(bottomLayerCrossingCost).toBe(Number.POSITIVE_INFINITY)
71+
expect(multiLayerCrossingCost).toBeLessThan(0.1)
72+
})

0 commit comments

Comments
 (0)