@tscircuit/rectdiff 0.0.39 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.github/workflows/bun-test.yml +1 -1
  2. package/benchmark.sh +61 -0
  3. package/global.d.ts +7 -0
  4. package/lib/RectDiffPipeline.ts +0 -27
  5. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +4 -108
  6. package/package.json +3 -1
  7. package/scripts/benchmark/benchmark-child/constants.ts +6 -0
  8. package/scripts/benchmark/benchmark-child/createSolver.ts +34 -0
  9. package/scripts/benchmark/benchmark-child/getAutorouterPipeline4ModulePath.ts +7 -0
  10. package/scripts/benchmark/benchmark-child/handleWorkerTaskLine.ts +64 -0
  11. package/scripts/benchmark/benchmark-child/importRuntimeModule.ts +8 -0
  12. package/scripts/benchmark/benchmark-child/types.ts +20 -0
  13. package/scripts/benchmark/benchmark-child/writeWorkerResultMessage.ts +10 -0
  14. package/scripts/benchmark/benchmark-types.ts +34 -0
  15. package/scripts/benchmark/benchmark.child.ts +11 -0
  16. package/scripts/benchmark/index.ts +413 -0
  17. package/tests/benchmark-rectdiff-override.test.ts +41 -0
  18. package/tests/benchmark-results.test.ts +30 -0
  19. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +3 -3
  20. package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +3 -3
  21. package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +1 -1
  22. package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
  23. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +3 -3
  24. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  25. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  26. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  27. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +3 -3
  28. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +3 -3
  29. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
  30. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
  31. package/tests/solver/bugreport18-1b2d06/__snapshots__/bugreport18-1b2d06.snap.svg +3 -3
  32. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  33. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  34. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +2 -2
  35. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  36. package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +3 -3
  37. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +2 -2
  38. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +3 -3
  39. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  40. package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
  41. package/tests/solver/offboardconnects01/__snapshots__/offboardconnects01.snap.svg +1 -1
  42. package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +3 -3
  43. package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +3 -3
  44. package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
  45. package/lib/math/layers/getUnionZ.ts +0 -6
  46. package/lib/math/layers/getZLayerName.ts +0 -6
  47. package/lib/math/layers/getZSpanMask.ts +0 -6
  48. package/lib/math/layers/hasContiguousZSpan.ts +0 -11
  49. package/lib/math/rects/intersectRects.ts +0 -28
  50. package/lib/math/rects/mergeRects.ts +0 -12
  51. package/lib/math/rects/rectArea.ts +0 -7
  52. package/lib/math/rects/rectContainsRect.ts +0 -18
  53. package/lib/math/rects/rectsTouchOrOverlap.ts +0 -12
  54. package/lib/math/rects/subtractRects.ts +0 -23
  55. package/lib/solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver.ts +0 -134
  56. package/lib/solvers/SparseMultilayerPromotionSolver/cloneNode.ts +0 -15
  57. package/lib/solvers/SparseMultilayerPromotionSolver/cloneNodeWithRect.ts +0 -34
  58. package/lib/solvers/SparseMultilayerPromotionSolver/createResidualNodes.ts +0 -42
  59. package/lib/solvers/SparseMultilayerPromotionSolver/findBestCoalesceCandidate.ts +0 -98
  60. package/lib/solvers/SparseMultilayerPromotionSolver/findBestPromotionCandidate.ts +0 -72
  61. package/lib/solvers/SparseMultilayerPromotionSolver/getUsableMultilayerVolumeShare.ts +0 -34
  62. package/lib/solvers/SparseMultilayerPromotionSolver/isFreeNode.ts +0 -8
  63. package/lib/solvers/SparseMultilayerPromotionSolver/nodeToRect.ts +0 -13
  64. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/CoalesceMultilayerTilesSolver.ts +0 -104
  65. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts +0 -148
  66. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/TrimContainedSingleLayerCoverageSolver.ts +0 -137
  67. package/lib/solvers/SparseMultilayerPromotionSolver/types.ts +0 -23
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"
4
+ import * as os from "node:os"
5
+ import * as path from "node:path"
6
+ import * as readline from "node:readline"
7
+ import * as dataset from "@tscircuit/autorouting-dataset-01"
8
+ import type { SimpleRouteJson } from "../../lib/types/srj-types"
9
+ import type {
10
+ BenchmarkTask,
11
+ WorkerResult,
12
+ WorkerResultMessage,
13
+ WorkerTaskMessage,
14
+ } from "./benchmark-types"
15
+
16
+ type BenchmarkOptions = {
17
+ concurrency: number
18
+ limit?: number
19
+ }
20
+
21
+ type WorkerSlot = {
22
+ id: number
23
+ child: ChildProcessWithoutNullStreams
24
+ stdoutReader: readline.Interface
25
+ stderrReader: readline.Interface
26
+ }
27
+
28
+ type WorkerExecutionResult = {
29
+ result: WorkerResult
30
+ restartWorker: boolean
31
+ }
32
+
33
+ const SAMPLE_TIMEOUT_MS = 120_000
34
+
35
+ const formatDuration = (timeMs: number | null) => {
36
+ if (timeMs === null) {
37
+ return "n/a"
38
+ }
39
+ if (timeMs < 1000) {
40
+ return `${timeMs}ms`
41
+ }
42
+ return `${(timeMs / 1000).toFixed(1)}s`
43
+ }
44
+
45
+ const getPercentileMs = (
46
+ values: number[],
47
+ percentile: number,
48
+ ): number | null => {
49
+ if (values.length === 0) {
50
+ return null
51
+ }
52
+
53
+ const sorted = [...values].sort((a, b) => a - b)
54
+ const index = (sorted.length - 1) * percentile
55
+ const lower = Math.floor(index)
56
+ const upper = Math.ceil(index)
57
+
58
+ if (lower === upper) {
59
+ return sorted[lower] ?? null
60
+ }
61
+
62
+ const weight = index - lower
63
+ const lowerValue = sorted[lower]
64
+ const upperValue = sorted[upper]
65
+ if (lowerValue === undefined || upperValue === undefined) {
66
+ return null
67
+ }
68
+ return lowerValue + (upperValue - lowerValue) * weight
69
+ }
70
+
71
+ const parseArgs = (): BenchmarkOptions => {
72
+ const args = process.argv.slice(2)
73
+ const defaultConcurrency =
74
+ typeof os.availableParallelism === "function"
75
+ ? os.availableParallelism()
76
+ : os.cpus().length
77
+
78
+ const options: BenchmarkOptions = {
79
+ concurrency: defaultConcurrency,
80
+ }
81
+
82
+ for (let i = 0; i < args.length; i += 1) {
83
+ const arg = args[i]
84
+ if (arg === "--concurrent") {
85
+ options.concurrency = Number.parseInt(args[i + 1] ?? "", 10)
86
+ i += 1
87
+ continue
88
+ }
89
+ if (arg === "--limit") {
90
+ options.limit = Number.parseInt(args[i + 1] ?? "", 10)
91
+ i += 1
92
+ continue
93
+ }
94
+ throw new Error(`Unknown argument: ${arg}`)
95
+ }
96
+
97
+ if (!Number.isFinite(options.concurrency) || options.concurrency < 1) {
98
+ throw new Error("--concurrent must be a positive integer")
99
+ }
100
+
101
+ if (
102
+ options.limit !== undefined &&
103
+ (!Number.isFinite(options.limit) || options.limit < 1)
104
+ ) {
105
+ throw new Error("--limit must be a positive integer")
106
+ }
107
+
108
+ return options
109
+ }
110
+
111
+ const loadScenarios = (limit?: number): BenchmarkTask[] => {
112
+ const scenarios = Object.entries(dataset)
113
+ .map(([scenarioName, scenario]) => ({
114
+ scenarioName,
115
+ scenario: scenario as SimpleRouteJson,
116
+ }))
117
+ .sort((a, b) => a.scenarioName.localeCompare(b.scenarioName))
118
+
119
+ if (limit === undefined) {
120
+ return scenarios
121
+ }
122
+
123
+ return scenarios.slice(0, limit)
124
+ }
125
+
126
+ const createChildProcess = () =>
127
+ spawn(
128
+ process.execPath,
129
+ [path.join("scripts", "benchmark", "benchmark.child.ts")],
130
+ {
131
+ cwd: process.cwd(),
132
+ stdio: ["pipe", "pipe", "pipe"],
133
+ env: process.env,
134
+ },
135
+ )
136
+
137
+ const createWorkerSlot = (id: number): WorkerSlot => {
138
+ const child = createChildProcess()
139
+ child.stdout.setEncoding("utf8")
140
+ child.stderr.setEncoding("utf8")
141
+
142
+ return {
143
+ id,
144
+ child,
145
+ stdoutReader: readline.createInterface({
146
+ input: child.stdout,
147
+ crlfDelay: Infinity,
148
+ }),
149
+ stderrReader: readline.createInterface({
150
+ input: child.stderr,
151
+ crlfDelay: Infinity,
152
+ }),
153
+ }
154
+ }
155
+
156
+ const terminateWorker = async (slot: WorkerSlot) => {
157
+ if (slot.child.killed || slot.child.exitCode !== null) {
158
+ slot.stdoutReader.close()
159
+ slot.stderrReader.close()
160
+ return
161
+ }
162
+
163
+ await new Promise<void>((resolve) => {
164
+ const finish = () => {
165
+ slot.stdoutReader.close()
166
+ slot.stderrReader.close()
167
+ resolve()
168
+ }
169
+
170
+ slot.child.once("close", finish)
171
+ try {
172
+ slot.child.kill("SIGKILL")
173
+ } catch {
174
+ finish()
175
+ }
176
+ })
177
+ }
178
+
179
+ const replaceWorker = async (slot: WorkerSlot) => {
180
+ const previousWorker: WorkerSlot = {
181
+ id: slot.id,
182
+ child: slot.child,
183
+ stdoutReader: slot.stdoutReader,
184
+ stderrReader: slot.stderrReader,
185
+ }
186
+
187
+ const nextWorker = createWorkerSlot(slot.id)
188
+ slot.child = nextWorker.child
189
+ slot.stdoutReader = nextWorker.stdoutReader
190
+ slot.stderrReader = nextWorker.stderrReader
191
+ await terminateWorker(previousWorker)
192
+ }
193
+
194
+ const executeTaskOnWorker = (
195
+ slot: WorkerSlot,
196
+ request: WorkerTaskMessage,
197
+ ): Promise<WorkerExecutionResult> =>
198
+ new Promise((resolve) => {
199
+ const startedAt = performance.now()
200
+ let settled = false
201
+ let stderrOutput = ""
202
+
203
+ const finish = (result: WorkerResult, restartWorker: boolean) => {
204
+ if (settled) {
205
+ return
206
+ }
207
+ settled = true
208
+ clearTimeout(timeout)
209
+ slot.stdoutReader.removeListener("line", onLine)
210
+ slot.stderrReader.removeListener("line", onStderrLine)
211
+ slot.child.removeListener("error", onError)
212
+ slot.child.removeListener("exit", onExit)
213
+ resolve({ result, restartWorker })
214
+ }
215
+
216
+ const fail = (
217
+ error: string,
218
+ didTimeout: boolean,
219
+ restartWorker: boolean,
220
+ ) => {
221
+ finish(
222
+ {
223
+ scenarioName: request.task.scenarioName,
224
+ elapsedTimeMs: Math.max(0, Math.round(performance.now() - startedAt)),
225
+ didSolve: false,
226
+ didTimeout,
227
+ error,
228
+ },
229
+ restartWorker,
230
+ )
231
+ }
232
+
233
+ const onLine = (line: string) => {
234
+ let message: WorkerResultMessage
235
+ try {
236
+ message = JSON.parse(line) as WorkerResultMessage
237
+ } catch (error) {
238
+ fail(`Failed to parse worker output: ${String(error)}`, false, true)
239
+ return
240
+ }
241
+
242
+ finish(message.result, false)
243
+ }
244
+
245
+ const onStderrLine = (line: string) => {
246
+ stderrOutput = stderrOutput ? `${stderrOutput}\n${line}` : line
247
+ }
248
+
249
+ const onError = (error: Error) => {
250
+ fail(`Worker error: ${error.message}`, false, true)
251
+ }
252
+
253
+ const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
254
+ fail(
255
+ stderrOutput ||
256
+ `Worker exited before responding (code=${code}, signal=${signal})`,
257
+ false,
258
+ true,
259
+ )
260
+ }
261
+
262
+ const timeout = setTimeout(() => {
263
+ fail(`Timed out after ${formatDuration(SAMPLE_TIMEOUT_MS)}`, true, true)
264
+ }, SAMPLE_TIMEOUT_MS)
265
+
266
+ slot.stdoutReader.on("line", onLine)
267
+ slot.stderrReader.on("line", onStderrLine)
268
+ slot.child.once("error", onError)
269
+ slot.child.once("exit", onExit)
270
+ slot.child.stdin.write(`${JSON.stringify(request)}\n`)
271
+ })
272
+
273
+ const formatTable = (
274
+ scenarioCount: number,
275
+ solvedCount: number,
276
+ failedCount: number,
277
+ timedOutCount: number,
278
+ avgTimeMs: number,
279
+ p50TimeMs: number | null,
280
+ p95TimeMs: number | null,
281
+ totalTimeMs: number,
282
+ ) => {
283
+ const headers = [
284
+ "Solver",
285
+ "Scenarios",
286
+ "Solved",
287
+ "Failed",
288
+ "Timed Out",
289
+ "Success %",
290
+ "Avg Time",
291
+ "P50 Time",
292
+ "P95 Time",
293
+ "Total Time",
294
+ ]
295
+
296
+ const rows = [
297
+ [
298
+ "Pipeline4->PortPointPathing",
299
+ String(scenarioCount),
300
+ String(solvedCount),
301
+ String(failedCount),
302
+ String(timedOutCount),
303
+ `${((solvedCount / scenarioCount) * 100).toFixed(1)}%`,
304
+ formatDuration(avgTimeMs),
305
+ formatDuration(p50TimeMs),
306
+ formatDuration(p95TimeMs),
307
+ formatDuration(totalTimeMs),
308
+ ],
309
+ ]
310
+
311
+ const widths = headers.map((header, columnIndex) =>
312
+ Math.max(
313
+ header.length,
314
+ ...rows.map((row) => row[columnIndex]?.length ?? 0),
315
+ ),
316
+ )
317
+ const paddedWidths = widths.map((width) => width ?? 0)
318
+
319
+ const separator = `+${paddedWidths.map((width) => "-".repeat(width + 2)).join("+")}+`
320
+ const headerLine = `| ${headers.map((header, index) => header.padEnd(paddedWidths[index] ?? header.length)).join(" | ")} |`
321
+ const bodyLines = rows.map(
322
+ (row) =>
323
+ `| ${row.map((cell, index) => cell.padEnd(paddedWidths[index] ?? cell.length)).join(" | ")} |`,
324
+ )
325
+
326
+ return [separator, headerLine, separator, ...bodyLines, separator].join("\n")
327
+ }
328
+
329
+ const main = async () => {
330
+ const options = parseArgs()
331
+ const tasks = loadScenarios(options.limit)
332
+
333
+ console.log(
334
+ `Running ${tasks.length} dataset scenarios with concurrency ${options.concurrency}`,
335
+ )
336
+
337
+ const startedAt = performance.now()
338
+ const results: WorkerResult[] = []
339
+ const workers = Array.from(
340
+ { length: Math.min(options.concurrency, tasks.length) },
341
+ (_, index) => createWorkerSlot(index + 1),
342
+ )
343
+
344
+ let nextTaskIndex = 0
345
+
346
+ const runWorkerLoop = async (slot: WorkerSlot) => {
347
+ while (nextTaskIndex < tasks.length) {
348
+ const task = tasks[nextTaskIndex]
349
+ if (!task) {
350
+ break
351
+ }
352
+ nextTaskIndex += 1
353
+
354
+ const execution = await executeTaskOnWorker(slot, { task })
355
+ results.push(execution.result)
356
+
357
+ const progressLabel = `[${results.length}/${tasks.length}]`
358
+ const status = execution.result.didSolve
359
+ ? "solved"
360
+ : execution.result.didTimeout
361
+ ? "timed out"
362
+ : "failed"
363
+ console.log(
364
+ `${progressLabel} ${execution.result.scenarioName} ${status} in ${formatDuration(execution.result.elapsedTimeMs)}`,
365
+ )
366
+
367
+ if (execution.restartWorker) {
368
+ await replaceWorker(slot)
369
+ }
370
+ }
371
+ }
372
+
373
+ await Promise.all(workers.map((slot) => runWorkerLoop(slot)))
374
+ await Promise.all(workers.map((slot) => terminateWorker(slot)))
375
+
376
+ const totalTimeMs = Math.max(0, Math.round(performance.now() - startedAt))
377
+ const solvedCount = results.filter((result) => result.didSolve).length
378
+ const failedCount = results.length - solvedCount
379
+ const timedOutCount = results.filter((result) => result.didTimeout).length
380
+ const elapsedTimes = results.map((result) => result.elapsedTimeMs)
381
+ const avgTimeMs =
382
+ elapsedTimes.reduce((sum, value) => sum + value, 0) / elapsedTimes.length
383
+
384
+ console.log("")
385
+ console.log(
386
+ formatTable(
387
+ results.length,
388
+ solvedCount,
389
+ failedCount,
390
+ timedOutCount,
391
+ Math.round(avgTimeMs),
392
+ getPercentileMs(elapsedTimes, 0.5),
393
+ getPercentileMs(elapsedTimes, 0.95),
394
+ totalTimeMs,
395
+ ),
396
+ )
397
+
398
+ const failures = results.filter((result) => !result.didSolve)
399
+ if (failures.length > 0) {
400
+ console.log("")
401
+ console.log("Failed scenarios:")
402
+ for (const failure of failures.slice(0, 20)) {
403
+ console.log(
404
+ `- ${failure.scenarioName}: ${failure.error ?? (failure.didTimeout ? "Timed out" : "Unknown failure")}`,
405
+ )
406
+ }
407
+ if (failures.length > 20) {
408
+ console.log(`- ... and ${failures.length - 20} more`)
409
+ }
410
+ }
411
+ }
412
+
413
+ await main()
@@ -0,0 +1,41 @@
1
+ import { expect, test } from "bun:test"
2
+ import { RectDiffPipeline } from "../lib/RectDiffPipeline"
3
+ import { createSolver } from "../scripts/benchmark/benchmark-child/createSolver"
4
+ import type { SimpleRouteJson } from "../lib/types/srj-types"
5
+
6
+ const SENTINEL_ERROR = "FAKE_RECTDIFF_PIPELINE_USED"
7
+
8
+ class FakeRectDiffPipeline extends RectDiffPipeline {
9
+ constructor(...args: ConstructorParameters<typeof RectDiffPipeline>) {
10
+ super(...args)
11
+ throw new Error(SENTINEL_ERROR)
12
+ }
13
+ }
14
+
15
+ test("benchmark createSolver uses an injected fake RectDiff override", async () => {
16
+ const simpleRouteJson: SimpleRouteJson = {
17
+ bounds: {
18
+ minX: 0,
19
+ maxX: 10,
20
+ minY: 0,
21
+ maxY: 10,
22
+ },
23
+ obstacles: [],
24
+ connections: [],
25
+ layerCount: 2,
26
+ minTraceWidth: 0.15,
27
+ }
28
+
29
+ const solver = await createSolver(simpleRouteJson, FakeRectDiffPipeline)
30
+
31
+ while (!solver.failed && solver.getCurrentPhase() !== "nodeSolver") {
32
+ solver.step()
33
+ }
34
+
35
+ expect(solver.getCurrentPhase()).toBe("nodeSolver")
36
+ expect(solver.failed).toBe(false)
37
+
38
+ expect(() => solver.step()).toThrow(SENTINEL_ERROR)
39
+ expect(solver.failed).toBe(true)
40
+ expect(solver.error).toContain(SENTINEL_ERROR)
41
+ }, 60_000)
@@ -0,0 +1,30 @@
1
+ import { expect, test } from "bun:test"
2
+
3
+ test("benchmark prints result output for one scenario", async () => {
4
+ const benchmarkProcess = Bun.spawn({
5
+ cmd: [
6
+ process.execPath,
7
+ "scripts/benchmark/index.ts",
8
+ "--limit",
9
+ "1",
10
+ "--concurrent",
11
+ "1",
12
+ ],
13
+ cwd: process.cwd(),
14
+ stdout: "pipe",
15
+ stderr: "pipe",
16
+ env: process.env,
17
+ })
18
+
19
+ const [exitCode, stdout, stderr] = await Promise.all([
20
+ benchmarkProcess.exited,
21
+ new Response(benchmarkProcess.stdout).text(),
22
+ new Response(benchmarkProcess.stderr).text(),
23
+ ])
24
+
25
+ expect(stderr).toBe("")
26
+ expect(exitCode).toBe(0)
27
+ expect(stdout).toContain("Running 1 dataset scenarios with concurrency 1")
28
+ expect(stdout).toContain("[1/1]")
29
+ expect(stdout).toContain("Pipeline4->PortPointPathing")
30
+ }, 180_000)