Skip to content

Commit e1a1b96

Browse files
committed
add two sided generation, export calculate graph bounds
1 parent 3b669e0 commit e1a1b96

File tree

4 files changed

+231
-4
lines changed

4 files changed

+231
-4
lines changed

lib/JumperGraphSolver/jumper-graph-generator/createProblemFromBaseGraph.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,75 @@ const isValidPoint = (
6262
return true
6363
}
6464

65+
type Side = "top" | "right" | "bottom" | "left"
66+
67+
/**
68+
* Generates a random point on a specific side of the bounds
69+
*/
70+
const getPointOnSide = (
71+
bounds: { minX: number; maxX: number; minY: number; maxY: number },
72+
side: Side,
73+
t: number, // 0-1 position along the side
74+
): { x: number; y: number } => {
75+
const width = bounds.maxX - bounds.minX
76+
const height = bounds.maxY - bounds.minY
77+
78+
switch (side) {
79+
case "top":
80+
return { x: bounds.minX + t * width, y: bounds.maxY }
81+
case "right":
82+
return { x: bounds.maxX, y: bounds.maxY - t * height }
83+
case "bottom":
84+
return { x: bounds.maxX - t * width, y: bounds.minY }
85+
case "left":
86+
return { x: bounds.minX, y: bounds.minY + t * height }
87+
}
88+
}
89+
90+
/**
91+
* Returns the length of a side
92+
*/
93+
const getSideLength = (
94+
bounds: { minX: number; maxX: number; minY: number; maxY: number },
95+
side: Side,
96+
): number => {
97+
const width = bounds.maxX - bounds.minX
98+
const height = bounds.maxY - bounds.minY
99+
return side === "top" || side === "bottom" ? width : height
100+
}
101+
65102
/**
66103
* Generates a random point on the perimeter of the given bounds
104+
* If allowedSides is provided, only generates points on those sides
67105
*/
68106
const getRandomPerimeterPoint = (
69107
bounds: { minX: number; maxX: number; minY: number; maxY: number },
70108
random: () => number,
109+
allowedSides?: Side[],
71110
): { x: number; y: number } => {
72111
const width = bounds.maxX - bounds.minX
73112
const height = bounds.maxY - bounds.minY
113+
114+
if (allowedSides && allowedSides.length > 0) {
115+
// Calculate total length of allowed sides
116+
const sideLengths = allowedSides.map((side) => getSideLength(bounds, side))
117+
const totalLength = sideLengths.reduce((sum, len) => sum + len, 0)
118+
119+
// Pick a random position along the combined length
120+
let pos = random() * totalLength
121+
122+
for (let i = 0; i < allowedSides.length; i++) {
123+
if (pos < sideLengths[i]) {
124+
const t = pos / sideLengths[i]
125+
return getPointOnSide(bounds, allowedSides[i], t)
126+
}
127+
pos -= sideLengths[i]
128+
}
129+
130+
// Fallback to last side (shouldn't happen due to floating point)
131+
return getPointOnSide(bounds, allowedSides[allowedSides.length - 1], random())
132+
}
133+
74134
const perimeter = 2 * width + 2 * height
75135

76136
// Pick a random position along the perimeter
@@ -92,6 +152,20 @@ const getRandomPerimeterPoint = (
92152
return { x: bounds.minX, y: bounds.minY + (pos - 2 * width - height) }
93153
}
94154

155+
const ALL_SIDES: Side[] = ["top", "right", "bottom", "left"]
156+
157+
/**
158+
* Picks two random different sides
159+
*/
160+
const pickTwoRandomSides = (random: () => number): [Side, Side] => {
161+
const firstIndex = Math.floor(random() * 4)
162+
let secondIndex = Math.floor(random() * 3)
163+
if (secondIndex >= firstIndex) {
164+
secondIndex++
165+
}
166+
return [ALL_SIDES[firstIndex], ALL_SIDES[secondIndex]]
167+
}
168+
95169
/**
96170
* Generates a connection ID from an index (0 -> "A", 1 -> "B", etc.)
97171
*/
@@ -103,6 +177,7 @@ export type CreateProblemFromBaseGraphParams = {
103177
baseGraph: JumperGraph
104178
numCrossings: number
105179
randomSeed: number
180+
twoSided?: boolean
106181
}
107182

108183
/**
@@ -114,10 +189,14 @@ export const createProblemFromBaseGraph = ({
114189
baseGraph,
115190
numCrossings,
116191
randomSeed,
192+
twoSided = false,
117193
}: CreateProblemFromBaseGraphParams): JumperGraphWithConnections => {
118194
const random = createSeededRandom(randomSeed)
119195
const graphBounds = calculateGraphBounds(baseGraph.regions)
120196

197+
// If twoSided, pick two random sides to use for all points in this problem
198+
const allowedSides = twoSided ? pickTwoRandomSides(random) : undefined
199+
121200
// Start with minimum connections needed for the desired crossings
122201
// For n connections, max crossings is n*(n-1)/2, so we need at least
123202
// ceil((1 + sqrt(1 + 8*numCrossings)) / 2) connections
@@ -136,7 +215,7 @@ export const createProblemFromBaseGraph = ({
136215
// Try to find a valid start point
137216
let start: { x: number; y: number } | null = null
138217
for (let tryCount = 0; tryCount < 100; tryCount++) {
139-
const candidate = getRandomPerimeterPoint(graphBounds, random)
218+
const candidate = getRandomPerimeterPoint(graphBounds, random, allowedSides)
140219
if (isValidPoint(candidate, allPoints)) {
141220
start = candidate
142221
break
@@ -151,7 +230,7 @@ export const createProblemFromBaseGraph = ({
151230
// Try to find a valid end point
152231
let end: { x: number; y: number } | null = null
153232
for (let tryCount = 0; tryCount < 100; tryCount++) {
154-
const candidate = getRandomPerimeterPoint(graphBounds, random)
233+
const candidate = getRandomPerimeterPoint(graphBounds, random, allowedSides)
155234
if (isValidPoint(candidate, allPoints)) {
156235
end = candidate
157236
break

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from "./HyperGraphSolver"
66
export * from "./JumperGraphSolver/geometry/applyTransformToGraph"
77
export * from "./types"
88
export * from "./JumperGraphSolver/jumper-types"
9+
export { calculateGraphBounds } from "./JumperGraphSolver/jumper-graph-generator/calculateGraphBounds"

scripts/run-benchmark-4x4-1206x4-both-orientations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ const createBaseGraph = (orientation: "vertical" | "horizontal" = "vertical") =>
2929
marginY: 1.2,
3030
outerPaddingX: 2,
3131
outerPaddingY: 2,
32+
parallelTracesUnderJumperCount: 2,
3233
innerColChannelPointCount: 3,
3334
innerRowChannelPointCount: 3,
34-
outerChannelXPointCount: 5,
35-
outerChannelYPointCount: 5,
35+
outerChannelXPointCount: 3,
36+
outerChannelYPointCount: 3,
3637
regionsBetweenPads: true,
3738
orientation,
3839
})
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { generateJumperX4Grid } from "../lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid"
2+
import { createProblemFromBaseGraph } from "../lib/JumperGraphSolver/jumper-graph-generator/createProblemFromBaseGraph"
3+
import { JumperGraphSolver } from "../lib/JumperGraphSolver/JumperGraphSolver"
4+
import { calculateGraphBounds } from "../lib/JumperGraphSolver/jumper-graph-generator/calculateGraphBounds"
5+
6+
const SAMPLES_PER_CROSSING_COUNT = 100
7+
const MIN_CROSSINGS = 2
8+
const MAX_CROSSINGS = 20
9+
10+
const median = (numbers: number[]): number | undefined => {
11+
if (numbers.length === 0) return undefined
12+
const sorted = numbers.slice().sort((a, b) => a - b)
13+
const middle = Math.floor(sorted.length / 2)
14+
return sorted[middle]
15+
}
16+
17+
const percentile = (numbers: number[], p: number): number | undefined => {
18+
if (numbers.length === 0) return undefined
19+
const sorted = numbers.slice().sort((a, b) => a - b)
20+
const index = Math.floor((p / 100) * (sorted.length - 1))
21+
return sorted[index]
22+
}
23+
24+
const createBaseGraph = (orientation: "vertical" | "horizontal" = "vertical") =>
25+
generateJumperX4Grid({
26+
cols: 4,
27+
rows: 4,
28+
marginX: 1.2,
29+
marginY: 1.2,
30+
outerPaddingX: 2,
31+
outerPaddingY: 2,
32+
parallelTracesUnderJumperCount: 2,
33+
innerColChannelPointCount: 3,
34+
innerRowChannelPointCount: 3,
35+
outerChannelXPointCount: 3,
36+
outerChannelYPointCount: 3,
37+
regionsBetweenPads: true,
38+
orientation,
39+
})
40+
41+
// Calculate and display graph sizes for both orientations
42+
const verticalGraph = createBaseGraph("vertical")
43+
const verticalBounds = calculateGraphBounds(verticalGraph.regions)
44+
const vWidth = verticalBounds.maxX - verticalBounds.minX
45+
const vHeight = verticalBounds.maxY - verticalBounds.minY
46+
console.log(
47+
`Graph size (vertical): ${vWidth.toFixed(1)}x${vHeight.toFixed(1)}mm`,
48+
)
49+
50+
const horizontalGraph = createBaseGraph("horizontal")
51+
const horizontalBounds = calculateGraphBounds(horizontalGraph.regions)
52+
const hWidth = horizontalBounds.maxX - horizontalBounds.minX
53+
const hHeight = horizontalBounds.maxY - horizontalBounds.minY
54+
console.log(
55+
`Graph size (horizontal): ${hWidth.toFixed(1)}x${hHeight.toFixed(1)}mm`,
56+
)
57+
58+
console.log("Benchmark: 4x4 1206x4 Jumper Grid Solver (Two-Sided Points, Both Orientations)")
59+
console.log("=".repeat(50))
60+
console.log(
61+
`Testing ${MIN_CROSSINGS}-${MAX_CROSSINGS} connections with ${SAMPLES_PER_CROSSING_COUNT} samples each\n`,
62+
)
63+
64+
const results: {
65+
numConnections: number
66+
successRate: number
67+
successes: number
68+
}[] = []
69+
70+
for (
71+
let numCrossings = MIN_CROSSINGS;
72+
numCrossings <= MAX_CROSSINGS;
73+
numCrossings++
74+
) {
75+
let successes = 0
76+
77+
const iterationsTaken: number[] = []
78+
const solverDurations: number[] = []
79+
for (
80+
let sampleIndex = 0;
81+
sampleIndex < SAMPLES_PER_CROSSING_COUNT;
82+
sampleIndex++
83+
) {
84+
const randomSeed = 1000 * numCrossings + sampleIndex
85+
let cumulativeDuration = 0
86+
87+
for (const orientation of ["vertical", "horizontal"] as const) {
88+
const graphWithConnections = createProblemFromBaseGraph({
89+
baseGraph: createBaseGraph(orientation),
90+
numCrossings: numCrossings,
91+
randomSeed,
92+
twoSided: true,
93+
})
94+
95+
const solver = new JumperGraphSolver({
96+
inputGraph: {
97+
regions: graphWithConnections.regions,
98+
ports: graphWithConnections.ports,
99+
},
100+
inputConnections: graphWithConnections.connections,
101+
})
102+
103+
const startTime = performance.now()
104+
solver.solve()
105+
const duration = performance.now() - startTime
106+
cumulativeDuration += duration
107+
108+
if (solver.solved) {
109+
iterationsTaken.push(solver.iterations)
110+
solverDurations.push(cumulativeDuration)
111+
successes++
112+
break
113+
}
114+
}
115+
}
116+
117+
const successRate = (successes / SAMPLES_PER_CROSSING_COUNT) * 100
118+
results.push({ numConnections: numCrossings, successRate, successes })
119+
120+
const med = median(iterationsTaken)
121+
const p95 = percentile(iterationsTaken, 95)
122+
const p99 = percentile(iterationsTaken, 99)
123+
const medDuration = median(solverDurations)
124+
console.log(
125+
`Crossings: ${numCrossings.toString().padStart(2)} | ` +
126+
`Success: ${successes.toString().padStart(3)}/${SAMPLES_PER_CROSSING_COUNT} | ` +
127+
`Rate: ${successRate.toFixed(1).padStart(5)}%`,
128+
` Med iters: ${med?.toFixed(0) ?? "N/A"}`,
129+
` P95: ${p95?.toFixed(0) ?? "N/A"}, P99: ${p99?.toFixed(0) ?? "N/A"}`,
130+
` Med time: ${medDuration !== undefined ? medDuration.toFixed(1) + "ms" : "N/A"}`,
131+
)
132+
}
133+
134+
console.log("\n" + "=".repeat(50))
135+
console.log("Summary:")
136+
console.log("=".repeat(50))
137+
138+
const avgSuccessRate =
139+
results.reduce((sum, r) => sum + r.successRate, 0) / results.length
140+
console.log(`Average success rate: ${avgSuccessRate.toFixed(1)}%`)
141+
142+
const perfectScores = results.filter((r) => r.successRate === 100).length
143+
console.log(`Crossing counts with 100% success: ${perfectScores}`)
144+
145+
const zeroScores = results.filter((r) => r.successRate === 0).length
146+
console.log(`Crossing counts with 0% success: ${zeroScores}`)

0 commit comments

Comments
 (0)