Skip to content

Commit 20a6e91

Browse files
authored
Merge branch 'tscircuit:main' into main
2 parents 83ed4eb + d0b1d4a commit 20a6e91

File tree

77 files changed

+1246
-1131
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1246
-1131
lines changed

.github/workflows/benchmark.yml

Lines changed: 157 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,32 @@ jobs:
3030
\`\`\`
3131
3232
**Examples:**
33-
- \`/benchmark\` - Run with default solver, all scenarios
33+
- \`/benchmark\` - Run all solvers, all scenarios
3434
- \`/benchmark AutoroutingPipelineSolver3_HgPortPointPathing\` - Run with specific solver
3535
- \`/benchmark AutoroutingPipelineSolver3_HgPortPointPathing 10\` - Run with specific solver, 10 scenarios
36-
- \`/benchmark _ 20\` - Run with default solver, 20 scenarios`
36+
- \`/benchmark _ 20\` - Run all solvers, 20 scenarios`
3737
});
3838
39-
benchmark:
40-
name: Run autorouting benchmark
41-
# Only run on PR comments with /benchmark command from maintainers (not bots)
39+
setup:
40+
name: Setup benchmark
4241
if: |
4342
github.event_name == 'issue_comment' &&
4443
github.event.issue.pull_request &&
4544
github.event.comment.user.type != 'Bot' &&
46-
contains(github.event.comment.body, '/benchmark') &&
45+
startsWith(github.event.comment.body, '/benchmark') &&
4746
(
4847
github.event.comment.author_association == 'OWNER' ||
4948
github.event.comment.author_association == 'MEMBER' ||
5049
github.event.comment.author_association == 'COLLABORATOR'
5150
)
5251
runs-on: ubuntu-latest
53-
timeout-minutes: 30
52+
timeout-minutes: 360
5453

54+
outputs:
55+
solvers: ${{ steps.discover.outputs.solvers }}
56+
scenario_limit: ${{ steps.parse.outputs.scenario_limit }}
57+
comment_id: ${{ steps.running-comment.outputs.comment_id }}
58+
sha: ${{ steps.parse.outputs.sha }}
5559
steps:
5660
- name: React to comment and post running status
5761
id: running-comment
@@ -74,8 +78,8 @@ jobs:
7478
});
7579
core.setOutput('comment_id', comment.data.id);
7680
77-
- name: Get PR branch and parse solver name
78-
id: pr-info
81+
- name: Parse command and get PR info
82+
id: parse
7983
uses: actions/github-script@v7
8084
with:
8185
script: |
@@ -84,11 +88,8 @@ jobs:
8488
repo: context.repo.repo,
8589
pull_number: context.issue.number
8690
});
87-
core.setOutput('ref', pr.data.head.ref);
8891
core.setOutput('sha', pr.data.head.sha);
8992
90-
// Parse solver name and scenario limit from comment: /benchmark [SolverName] [limit]
91-
// Examples: /benchmark, /benchmark SolverName, /benchmark SolverName 50, /benchmark _ 50
9293
const comment = context.payload.comment.body;
9394
const match = comment.match(/\/benchmark(?:\s+(\S+))?(?:\s+(\d+))?/);
9495
const solverName = match && match[1] && match[1] !== '_' ? match[1] : '';
@@ -101,6 +102,48 @@ jobs:
101102
with:
102103
ref: refs/pull/${{ github.event.issue.number }}/head
103104

105+
- name: Discover solvers from exports
106+
id: discover
107+
uses: actions/github-script@v7
108+
with:
109+
script: |
110+
const fs = require('fs');
111+
112+
// Read the pipeline solvers index - this is the source of truth for benchmarkable solvers
113+
const pipelineIndex = fs.readFileSync('lib/autorouter-pipelines/index.ts', 'utf8');
114+
const pipelineNames = [...pipelineIndex.matchAll(/export\s*\{\s*(\w+)\s*\}/g)].map(m => m[1]);
115+
116+
// Read lib/index.ts to find aliases (e.g. "X as Y")
117+
const libIndex = fs.readFileSync('lib/index.ts', 'utf8');
118+
119+
// Build a map of original name -> exported name (alias or original)
120+
const solvers = [];
121+
for (const name of pipelineNames) {
122+
const aliasMatch = libIndex.match(new RegExp(`${name}\\s+as\\s+(\\w+)`));
123+
solvers.push(aliasMatch ? aliasMatch[1] : name);
124+
}
125+
126+
const solverName = '${{ steps.parse.outputs.solver_name }}';
127+
const finalSolvers = solverName ? [solverName] : solvers;
128+
129+
console.log('Discovered solvers:', finalSolvers);
130+
core.setOutput('solvers', JSON.stringify(finalSolvers));
131+
132+
benchmark:
133+
name: Benchmark ${{ matrix.solver }}
134+
needs: setup
135+
runs-on: ubuntu-latest
136+
timeout-minutes: 360
137+
strategy:
138+
fail-fast: false
139+
matrix:
140+
solver: ${{ fromJson(needs.setup.outputs.solvers) }}
141+
steps:
142+
- name: Checkout PR branch
143+
uses: actions/checkout@v4
144+
with:
145+
ref: refs/pull/${{ github.event.issue.number }}/head
146+
104147
- name: Setup bun
105148
uses: oven-sh/setup-bun@v2
106149
with:
@@ -121,75 +164,138 @@ jobs:
121164
export * from "./lib"
122165
EOF
123166
124-
- name: Run benchmark
167+
- name: Run benchmark for ${{ matrix.solver }}
125168
id: benchmark
126169
run: |
127-
SOLVER_NAME="${{ steps.pr-info.outputs.solver_name }}"
128-
SCENARIO_LIMIT="${{ steps.pr-info.outputs.scenario_limit }}"
129-
130-
# Build command with optional solver name and scenario limit
131-
CMD="autorouting-dataset-runner benchmark-solver.ts"
132-
133-
if [ -n "$SOLVER_NAME" ]; then
134-
CMD="$CMD $SOLVER_NAME"
135-
echo "Running benchmark with solver: $SOLVER_NAME"
136-
else
137-
echo "Running benchmark with default solver"
138-
fi
170+
SCENARIO_LIMIT="${{ needs.setup.outputs.scenario_limit }}"
171+
CMD="autorouting-dataset-runner benchmark-solver.ts ${{ matrix.solver }}"
139172
140173
if [ -n "$SCENARIO_LIMIT" ]; then
141174
CMD="$CMD --scenario-limit $SCENARIO_LIMIT"
142-
echo "Scenario limit: $SCENARIO_LIMIT"
143-
else
144-
echo "Running all scenarios"
145175
fi
146176
147-
# Run and capture output while showing live progress
148177
$CMD 2>&1 | tee /tmp/benchmark_output.txt || true
149178
150-
# Extract only key info for the comment (to avoid "Argument list too long" error)
151-
{
152-
echo "BENCHMARK_OUTPUT<<EOFMARKER"
153-
# Get loading/using autorouter lines
154-
grep -E "^(Loading|✓ Using|✓ From)" /tmp/benchmark_output.txt || true
155-
echo ""
156-
# Get the results table (lines with + or |)
157-
grep -E "^[+|]" /tmp/benchmark_output.txt || true
158-
echo ""
159-
# Get summary lines
160-
grep -E "^(Scenarios:|✓)" /tmp/benchmark_output.txt | tail -3 || true
161-
echo "EOFMARKER"
162-
} >> $GITHUB_OUTPUT
179+
# Extract the data row from the results table and save to file
180+
mkdir -p /tmp/benchmark-row
181+
grep -E "^\|" /tmp/benchmark_output.txt | grep -v "Completed %" > /tmp/benchmark-row/row.txt || true
163182
164183
- name: Upload benchmark results
165184
uses: actions/upload-artifact@v4
166185
with:
167-
name: benchmark-results
186+
name: results-${{ matrix.solver }}
168187
path: results/
169188
retention-days: 30
170189

171-
- name: Update comment with results
190+
- name: Upload benchmark row
191+
uses: actions/upload-artifact@v4
192+
with:
193+
name: benchmark-row-${{ matrix.solver }}
194+
path: /tmp/benchmark-row/row.txt
195+
retention-days: 1
196+
197+
report:
198+
name: Post benchmark results
199+
needs: [setup, benchmark]
200+
if: always() && needs.setup.result == 'success'
201+
runs-on: ubuntu-latest
202+
steps:
203+
- name: Download benchmark row artifacts
204+
uses: actions/download-artifact@v4
205+
with:
206+
path: benchmark-rows/
207+
pattern: benchmark-row-*
208+
209+
- name: Download all benchmark result artifacts
210+
uses: actions/download-artifact@v4
211+
with:
212+
path: all-results/
213+
pattern: results-*
214+
merge-multiple: true
215+
216+
- name: Upload combined benchmark results
217+
uses: actions/upload-artifact@v4
218+
with:
219+
name: benchmark-results
220+
path: all-results/
221+
retention-days: 30
222+
223+
- name: Delete per-solver artifacts
224+
uses: actions/github-script@v7
225+
with:
226+
script: |
227+
const artifacts = await github.rest.actions.listArtifactsForRepo({
228+
owner: context.repo.owner,
229+
repo: context.repo.repo,
230+
per_page: 100
231+
});
232+
const runArtifacts = artifacts.data.artifacts.filter(a =>
233+
a.workflow_run?.id === ${{ github.run_id }} &&
234+
(a.name.startsWith('results-') || a.name.startsWith('benchmark-row-'))
235+
);
236+
for (const artifact of runArtifacts) {
237+
await github.rest.actions.deleteArtifact({
238+
owner: context.repo.owner,
239+
repo: context.repo.repo,
240+
artifact_id: artifact.id
241+
});
242+
}
243+
244+
- name: Post combined results
172245
uses: actions/github-script@v7
173246
with:
174247
github-token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
175248
script: |
176-
const output = `${{ steps.benchmark.outputs.BENCHMARK_OUTPUT }}`;
177-
const solverName = '${{ steps.pr-info.outputs.solver_name }}' || 'default';
178-
const scenarioLimit = '${{ steps.pr-info.outputs.scenario_limit }}' || 'all';
249+
const fs = require('fs');
250+
const solvers = ${{ needs.setup.outputs.solvers }};
251+
252+
// Parse each solver's result row into columns
253+
const results = [];
254+
for (const solver of solvers) {
255+
const rowFile = `benchmark-rows/benchmark-row-${solver}/row.txt`;
256+
let cols = null;
257+
if (fs.existsSync(rowFile)) {
258+
const row = fs.readFileSync(rowFile, 'utf8').trim();
259+
if (row) {
260+
cols = row.split('|').filter(c => c.trim()).map(c => c.trim());
261+
}
262+
}
263+
if (cols && cols.length >= 5) {
264+
results.push(cols);
265+
} else {
266+
results.push([solver, 'failed', 'n/a', 'n/a', 'n/a']);
267+
}
268+
}
269+
270+
// Calculate max width for each column
271+
const headers = ['Solver', 'Completed %', 'Relaxed DRC Pass %', 'P50 Time', 'P95 Time'];
272+
const widths = headers.map((h, i) =>
273+
Math.max(h.length, ...results.map(r => (r[i] || '').length))
274+
);
275+
276+
const sep = '+' + widths.map(w => '-'.repeat(w + 2)).join('+') + '+';
277+
const headerRow = '|' + headers.map((h, i) => ` ${h.padEnd(widths[i])} `).join('|') + '|';
278+
const dataRows = results.map(r =>
279+
'|' + r.map((c, i) => ` ${c.padEnd(widths[i])} `).join('|') + '|'
280+
).join('\n');
281+
282+
const table = [sep, headerRow, sep, dataRows, sep].join('\n');
283+
284+
const scenarioLimit = '${{ needs.setup.outputs.scenario_limit }}' || 'all';
179285
180286
await github.rest.issues.updateComment({
181287
owner: context.repo.owner,
182288
repo: context.repo.repo,
183-
comment_id: ${{ steps.running-comment.outputs.comment_id }},
289+
comment_id: ${{ needs.setup.outputs.comment_id }},
184290
body: `## 🏃 Autorouting Benchmark Results
185291
186-
**Solver:** ${solverName} | **Scenarios:** ${scenarioLimit}
292+
**Solvers:** ${solvers.length} | **Scenarios:** ${scenarioLimit}
187293
188294
\`\`\`
189-
${output}
295+
${table}
190296
\`\`\`
191297
192298
📊 [Download HTML visualization and bundle](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)
193299
194-
<sub>Triggered by @${{ github.event.comment.user.login }} • Commit: ${{ steps.pr-info.outputs.sha }}</sub>`
300+
<sub>Triggered by @${{ github.event.comment.user.login }} • Commit: ${{ needs.setup.outputs.sha }}</sub>`
195301
});

lib/autorouter-pipelines/AssignableAutoroutingPipeline2/AssignableAutoroutingPipeline2.ts

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,58 @@
1-
import type { GraphicsObject, Line } from "graphics-debug"
2-
import { combineVisualizations } from "../../utils/combineVisualizations"
3-
import type {
4-
CapacityMeshEdge,
5-
CapacityMeshNode,
6-
ObstacleId,
7-
RootConnectionName,
8-
SimpleRouteJson,
9-
SimplifiedPcbTrace,
10-
SimplifiedPcbTraces,
11-
TraceId,
12-
} from "../../types"
13-
import { BaseSolver } from "../../solvers/BaseSolver"
14-
import { CapacityMeshEdgeSolver } from "../../solvers/CapacityMeshSolver/CapacityMeshEdgeSolver"
15-
import { getColorMap } from "../../solvers/colors"
16-
import { HighDensitySolver as LegacyHighDensitySolver } from "../../solvers/HighDensitySolver/HighDensitySolver"
17-
import { SimpleHighDensitySolver } from "./SimpleHighDensitySolver"
18-
import { JumperHighDensitySolver } from "./JumperHighDensitySolver"
1+
import { RectDiffPipeline } from "@tscircuit/rectdiff"
192
import { ConnectivityMap } from "circuit-json-to-connectivity-map"
20-
import { getConnectivityMapFromSimpleRouteJson } from "lib/utils/getConnectivityMapFromSimpleRouteJson"
21-
import { CapacityNodeTargetMerger } from "../../solvers/CapacityNodeTargetMerger/CapacityNodeTargetMerger"
22-
import { calculateOptimalCapacityDepth } from "../../utils/getTunedTotalCapacity1"
23-
import { NetToPointPairsSolver } from "../../solvers/NetToPointPairsSolver/NetToPointPairsSolver"
24-
import { convertHdRouteToSimplifiedRoute } from "lib/utils/convertHdRouteToSimplifiedRoute"
25-
import { MultipleHighDensityRouteStitchSolver } from "../../solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver"
26-
import { convertSrjToGraphicsObject } from "lib/utils/convertSrjToGraphicsObject"
27-
import { StrawSolver } from "../../solvers/StrawSolver/StrawSolver"
28-
import { SingleLayerNodeMergerSolver } from "../../solvers/SingleLayerNodeMerger/SingleLayerNodeMergerSolver"
3+
import type { GraphicsObject, Line } from "graphics-debug"
4+
import { getGlobalInMemoryCache } from "lib/cache/setupGlobalCaches"
5+
import { CacheProvider } from "lib/cache/types"
6+
import {
7+
HyperPortPointPathingSolver,
8+
HyperPortPointPathingSolverParams,
9+
} from "lib/solvers/PortPointPathingSolver/HyperPortPointPathingSolver"
2910
import {
3011
HighDensityIntraNodeRoute,
3112
HighDensityRoute,
3213
} from "lib/types/high-density-types"
14+
import { convertHdRouteToSimplifiedRoute } from "lib/utils/convertHdRouteToSimplifiedRoute"
15+
import { convertSrjToGraphicsObject } from "lib/utils/convertSrjToGraphicsObject"
16+
import { getConnectivityMapFromSimpleRouteJson } from "lib/utils/getConnectivityMapFromSimpleRouteJson"
17+
import { AvailableSegmentPointSolver } from "../../solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver"
18+
import { BaseSolver } from "../../solvers/BaseSolver"
19+
import { CapacityMeshEdgeSolver } from "../../solvers/CapacityMeshSolver/CapacityMeshEdgeSolver"
3320
import { CapacityMeshEdgeSolver2_NodeTreeOptimization } from "../../solvers/CapacityMeshSolver/CapacityMeshEdgeSolver2_NodeTreeOptimization"
21+
import { CapacityMeshNodeSolver2_NodeUnderObstacle } from "../../solvers/CapacityMeshSolver/CapacityMeshNodeSolver2_NodesUnderObstacles"
22+
import { CapacityNodeTargetMerger } from "../../solvers/CapacityNodeTargetMerger/CapacityNodeTargetMerger"
3423
import { DeadEndSolver } from "../../solvers/DeadEndSolver/DeadEndSolver"
35-
import { CacheProvider } from "lib/cache/types"
36-
import { getGlobalInMemoryCache } from "lib/cache/setupGlobalCaches"
24+
import { HighDensitySolver as LegacyHighDensitySolver } from "../../solvers/HighDensitySolver/HighDensitySolver"
25+
import { MultiSectionPortPointOptimizer } from "../../solvers/MultiSectionPortPointOptimizer"
26+
import { NetToPointPairsSolver } from "../../solvers/NetToPointPairsSolver/NetToPointPairsSolver"
3727
import { NetToPointPairsSolver2_OffBoardConnection } from "../../solvers/NetToPointPairsSolver2_OffBoardConnection/NetToPointPairsSolver2_OffBoardConnection"
38-
import { RectDiffPipeline } from "@tscircuit/rectdiff"
39-
import { TraceSimplificationSolver } from "../../solvers/TraceSimplificationSolver/TraceSimplificationSolver"
40-
import { TraceKeepoutSolver } from "../../solvers/TraceKeepoutSolver/TraceKeepoutSolver"
41-
import { TraceWidthSolver } from "../../solvers/TraceWidthSolver/TraceWidthSolver"
42-
import { AvailableSegmentPointSolver } from "../../solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver"
4328
import {
44-
PortPointPathingSolver,
4529
InputNodeWithPortPoints,
4630
InputPortPoint,
31+
PortPointPathingSolver,
4732
} from "../../solvers/PortPointPathingSolver/PortPointPathingSolver"
48-
import { CapacityMeshNodeSolver2_NodeUnderObstacle } from "../../solvers/CapacityMeshSolver/CapacityMeshNodeSolver2_NodesUnderObstacles"
49-
import { MultiSectionPortPointOptimizer } from "../../solvers/MultiSectionPortPointOptimizer"
33+
import { MultipleHighDensityRouteStitchSolver } from "../../solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver"
34+
import { SingleLayerNodeMergerSolver } from "../../solvers/SingleLayerNodeMerger/SingleLayerNodeMergerSolver"
35+
import { StrawSolver } from "../../solvers/StrawSolver/StrawSolver"
36+
import { TraceKeepoutSolver } from "../../solvers/TraceKeepoutSolver/TraceKeepoutSolver"
37+
import { TraceSimplificationSolver } from "../../solvers/TraceSimplificationSolver/TraceSimplificationSolver"
38+
import { TraceWidthSolver } from "../../solvers/TraceWidthSolver/TraceWidthSolver"
39+
import { getColorMap } from "../../solvers/colors"
40+
import type {
41+
CapacityMeshEdge,
42+
CapacityMeshNode,
43+
ObstacleId,
44+
RootConnectionName,
45+
SimpleRouteJson,
46+
SimplifiedPcbTrace,
47+
SimplifiedPcbTraces,
48+
TraceId,
49+
} from "../../types"
50+
import { combineVisualizations } from "../../utils/combineVisualizations"
51+
import { calculateOptimalCapacityDepth } from "../../utils/getTunedTotalCapacity1"
52+
import { JumperHighDensitySolver } from "./JumperHighDensitySolver"
5053
import { PortPointOffboardPathFragmentSolver } from "./PortPointOffboardPathFragmentSolver"
51-
import {
52-
HyperPortPointPathingSolver,
53-
HyperPortPointPathingSolverParams,
54-
} from "lib/solvers/PortPointPathingSolver/HyperPortPointPathingSolver"
5554
import { RelateNodesToOffBoardConnectionsSolver } from "./RelateNodesToOffBoardConnectionsSolver"
55+
import { SimpleHighDensitySolver } from "./SimpleHighDensitySolver"
5656
import { updateConnMapWithOffboardObstacleConnections } from "./updateConnMapWithOffboardObstacleConnections"
5757

5858
interface CapacityMeshSolverOptions {
@@ -400,7 +400,7 @@ export class AssignableAutoroutingPipeline2 extends BaseSolver {
400400
this.srj = srj
401401
this.opts = { ...opts }
402402
this.MAX_ITERATIONS = 100e6
403-
this.viaDiameter = srj.minViaDiameter ?? 0.6
403+
this.viaDiameter = srj.minViaDiameter ?? 0.3
404404
this.minTraceWidth = srj.minTraceWidth
405405
const mutableOpts = this.opts
406406
this.effort = mutableOpts.effort ?? 1

0 commit comments

Comments
 (0)