Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat local freerouting autorouter #30

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
82 changes: 82 additions & 0 deletions cli/run/process-circuit-file-locally.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { routeUsingLocalFreerouting } from "freerouting"
import { readFile, writeFile, mkdir, unlink } from "fs/promises"
import { join, dirname } from "path/posix"
import { convertAndSaveCircuitToDsn } from "../utils/circuit-json-to dsn-file-converter"
import {
parseDsnToDsnJson,
convertDsnSessionToCircuitJson,
type DsnPcb,
type DsnSession,
} from "dsn-converter"
import { glob } from "glob"
import Debug from "debug"
import type { AnyCircuitElement } from "circuit-json"

const debug = Debug("autorouting:cli/run/local-freerouting")

export async function processCircuitFileLocally(inputPath: string) {
debug(`Processing circuit file locally: ${inputPath}`)

const sampleDir = dirname(inputPath)
let dsnPath: string
let circuitWithoutTraces: AnyCircuitElement[] | undefined

// Check if input is DSN or JSON
if (inputPath.toLowerCase().endsWith(".dsn")) {
dsnPath = inputPath
// For DSN files, we don't need to do initial conversion
} else if (inputPath.toLowerCase().endsWith(".json")) {
// Assume JSON file
// Read and parse the input circuit
const circuitJson = await readFile(inputPath, "utf8")
const circuit: AnyCircuitElement[] = JSON.parse(circuitJson)

// Filter out any existing pcb_traces from the original circuit
circuitWithoutTraces = circuit.filter((item) => item.type !== "pcb_trace")

// Convert circuit to DSN and save to temp file
dsnPath = await convertAndSaveCircuitToDsn(circuitWithoutTraces)
} else {
throw new Error(`Unsupported file format for input file: ${inputPath}`)
}

try {
// Run local freerouting
debug(`Running freerouting on DSN file: ${dsnPath}`)
const routedDsn = await routeUsingLocalFreerouting({ inputPath: dsnPath })

// Read the original DSN file to get the PCB data
const originalDsnContent = await readFile(dsnPath, "utf8")
const pcbJson = parseDsnToDsnJson(originalDsnContent) as DsnPcb

// Parse the routed DSN to get the session data
const sessionJson = parseDsnToDsnJson(routedDsn) as DsnSession

// Convert the routed DSN back to circuit JSON
const routedTraces = convertDsnSessionToCircuitJson(pcbJson, sessionJson)

// Combine original circuit (without traces) with new traces
const outputCircuit = circuitWithoutTraces
? [...circuitWithoutTraces, ...routedTraces]
: routedTraces

// Write the routed circuit JSON to the outputs directory
const outputsDir = join(sampleDir, "outputs")
await mkdir(outputsDir, { recursive: true })
const outputPath = join(outputsDir, "freerouting_routed_circuit.json")
await writeFile(outputPath, JSON.stringify(outputCircuit, null, 2))

debug(`Wrote routed circuit to: ${outputPath}`)

// Clean up temporary DSN file
if (!inputPath.toLowerCase().endsWith(".dsn")) {
await unlink(dsnPath)
debug(`Deleted temporary DSN file: ${dsnPath}`)
}

return outputPath
} catch (error) {
debug(`Error processing file ${inputPath}:`, error)
throw error
}
}
23 changes: 14 additions & 9 deletions cli/run/process-circuit-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ import type { AnyCircuitElement } from "circuit-json"

const debug = Debug("autorouting:cli/run/process-circuit-file")

export async function processCircuitFile(
circuitPath: string,
autorouter: string,
serverUrl: string,
) {
debug(`Processing circuit file: ${circuitPath}`)
export async function processCircuitFile({
inputPath,
autorouter,
serverUrl,
}: {
inputPath: string
autorouter: string
serverUrl: string
}) {
debug(`Processing circuit file: ${inputPath}`)

const fetchWithDebug = (url: string, options: RequestInit) => {
debug("fetching", url)
return fetch(url, options)
}
const circuitJson = await readFile(circuitPath, "utf8")
const circuitJson = await readFile(inputPath, "utf8")
const circuit: AnyCircuitElement[] = JSON.parse(circuitJson)

// Create autorouting job
Expand All @@ -28,7 +33,7 @@ export async function processCircuitFile(
input_circuit_json: circuit,
provider: autorouter,
autostart: true,
display_name: circuitPath,
display_name: inputPath,
}),
headers: { "Content-Type": "application/json" },
},
Expand Down Expand Up @@ -59,7 +64,7 @@ export async function processCircuitFile(
},
).then((r) => r.json())

const sampleDir = dirname(circuitPath)
const sampleDir = dirname(inputPath)
const outputsDir = join(sampleDir, "outputs")
await mkdir(outputsDir, { recursive: true })

Expand Down
6 changes: 6 additions & 0 deletions cli/run/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const autorouterRunCommand = (program: Command) => {
"freerouting",
)
.option("-d, --dataset", "Treat input path as a dataset directory", false)
.option(
"-l, --local",
"Run freerouting locally instead of using the server",
false,
)
.action(async (inputPath, options) => {
try {
if (!options.dataset && !inputPath.endsWith(".json")) {
Expand All @@ -28,6 +33,7 @@ export const autorouterRunCommand = (program: Command) => {
inputPath,
autorouter: options.autorouter,
isDataset: options.dataset,
isLocal: options.local,
})
console.log("Successfully completed autorouting")
} catch (error) {
Expand Down
24 changes: 18 additions & 6 deletions cli/run/run-autorouter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { KyInstance } from "ky"
import { readFile, writeFile, mkdir } from "fs/promises"
import { join, dirname } from "path/posix"
import { stat } from "fs/promises"
import { join } from "path/posix"
import { glob } from "glob"
import Debug from "debug"
import { stat } from "fs/promises"
import type { AnyCircuitElement } from "circuit-json"
import { processCircuitFile } from "./process-circuit-file"
import { processCircuitFileLocally } from "./process-circuit-file-locally"

const debug = Debug("autorouting:cli/run/run-autorouter")

Expand All @@ -15,17 +14,24 @@ interface RunAutorouterOptions {
isDataset?: boolean
ky?: KyInstance
serverUrl?: string
isLocal?: boolean
}

export async function runAutorouter({
inputPath,
autorouter,
isDataset = false,
serverUrl = "https://registry-api.tscircuit.com",
isLocal = false,
}: RunAutorouterOptions) {
if (autorouter !== "freerouting") {
throw new Error(`Unsupported autorouter: ${autorouter}`)
}
if (!isDataset) {
debug(`Processing single file: ${inputPath}`)
await processCircuitFile(inputPath, autorouter, serverUrl)
isLocal
? await processCircuitFileLocally(inputPath)
: await processCircuitFile({ inputPath, autorouter, serverUrl })
return
}
debug(`Processing dataset: ${inputPath}`)
Expand All @@ -50,7 +56,13 @@ export async function runAutorouter({
debug(`Processing sample folder: ${sampleFolder}`)

// Process the circuit file
await processCircuitFile(unroutedCircuitPath, autorouter, serverUrl)
isLocal
? await processCircuitFileLocally(unroutedCircuitPath)
: await processCircuitFile({
inputPath: unroutedCircuitPath,
autorouter,
serverUrl,
})
}
} catch (error) {
debug(`Skipping ${sampleFolder} - no unrouted_circuit.json found`)
Expand Down
19 changes: 19 additions & 0 deletions cli/utils/circuit-json-to dsn-file-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { convertCircuitJsonToDsnString } from "dsn-converter"
import type { AnyCircuitElement } from "circuit-json"
import { writeFile } from "fs/promises"
import { temporaryFile as tempyFile } from "tempy"

export async function convertAndSaveCircuitToDsn(
circuit: AnyCircuitElement[],
): Promise<string> {
// Convert circuit JSON to DSN format
const dsnContent = convertCircuitJsonToDsnString(circuit)

// Create temporary file with .dsn extension
const tempPath = tempyFile({ extension: "dsn" })

// Write DSN file to temp location
await writeFile(tempPath, dsnContent)

return tempPath
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"embla-carousel-react": "^8.5.1",
"eventemitter3": "^5.0.1",
"events": "^3.3.0",
"freerouting": "^0.0.19",
"immer": "^10.1.1",
"input-otp": "^1.4.1",
"javascript-time-ago": "^2.5.11",
Expand Down
50 changes: 50 additions & 0 deletions tests/cli/run/run-autorouter-local1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "bun:test"
import { temporaryDirectory } from "tempy"
import { runAutorouter } from "@/cli/run/run-autorouter"
import { existsSync, readFileSync } from "fs"
import { join } from "path/posix"
import { writeFile, mkdir } from "fs/promises"
import type { AnyCircuitElement } from "circuit-json"
import unroutedCircuit from "tests/fixtures/unrouted_circuit.json"

const createMockFiles = async (dir: string) => {
// Create unrouted circuit JSON
const mockCircuit = unroutedCircuit
await writeFile(
join(dir, "unrouted_circuit.json"),
JSON.stringify(mockCircuit, null, 2),
)
}

test("should run local autorouter on a single circuit file", async () => {
// Create a temporary directory with a sample folder structure
const tempDir = temporaryDirectory()
const sampleDir = join(tempDir, "sample1")
await mkdir(sampleDir, { recursive: true })
await createMockFiles(sampleDir)
const inputPath = join(sampleDir, "unrouted_circuit.json")
console.log("inputPath", inputPath)
// Run command
await runAutorouter({
inputPath,
autorouter: "freerouting",
serverUrl: "http://localhost:3000",
isLocal: true,
})

// Check routed file was created in sample's outputs directory
const outputPath = join(
sampleDir,
"outputs",
"freerouting_routed_circuit.json",
)
expect(existsSync(outputPath)).toBe(true)

// Verify output content
const outputJson: AnyCircuitElement[] = JSON.parse(
readFileSync(outputPath, "utf8"),
)
expect(Array.isArray(outputJson)).toBe(true)
expect(outputJson.some((item) => item.type !== "pcb_trace")).toBe(true)
expect(outputJson.some((item) => item.type === "pcb_trace")).toBe(true)
}, 30000) // Increase timeout to 30 seconds
57 changes: 57 additions & 0 deletions tests/cli/run/run-autorouter-local2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from "bun:test"
import { temporaryDirectory } from "tempy"
import { runAutorouter } from "@/cli/run/run-autorouter"
import { existsSync, readFileSync } from "fs"
import { join } from "path/posix"
import { writeFile, mkdir } from "fs/promises"
import type { AnyCircuitElement } from "circuit-json"
import unroutedCircuit from "tests/fixtures/unrouted_circuit.json"

const createMockCircuitFile = async (dir: string) => {
const mockCircuit = unroutedCircuit
await writeFile(
join(dir, "unrouted_circuit.json"),
JSON.stringify(mockCircuit, null, 2),
)
}

test("should run local autorouter on a dataset directory", async () => {
// Create a temporary dataset directory structure
const datasetDir = temporaryDirectory()

// Create multiple sample folders
const sampleDirs = ["sample1", "sample2"] // Reduced to 2 samples to speed up test
for (const sampleDir of sampleDirs) {
const fullSamplePath = join(datasetDir, sampleDir)
await mkdir(fullSamplePath, { recursive: true })
await createMockCircuitFile(fullSamplePath)
}

console.log("Running local autorouter with dataset directory:", datasetDir)
// Run command
await runAutorouter({
inputPath: datasetDir,
autorouter: "freerouting",
isDataset: true,
serverUrl: "http://localhost:3000",
isLocal: true,
})

// Check each sample folder
for (const sampleDir of sampleDirs) {
const outputPath = join(
datasetDir,
sampleDir,
"outputs",
"freerouting_routed_circuit.json",
)
expect(existsSync(outputPath)).toBe(true)

const outputJson: AnyCircuitElement[] = JSON.parse(
readFileSync(outputPath, "utf8"),
)
expect(Array.isArray(outputJson)).toBe(true)
expect(outputJson.some((item) => item.type !== "pcb_trace")).toBe(true)
expect(outputJson.some((item) => item.type === "pcb_trace")).toBe(true)
}
}, 30000) // Increased timeout to 30 seconds
Loading