Skip to content

Commit

Permalink
feat local freerouting autorouter (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Abse2001 authored Jan 29, 2025
1 parent a4631b8 commit cae9347
Show file tree
Hide file tree
Showing 13 changed files with 358 additions and 156 deletions.
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

0 comments on commit cae9347

Please sign in to comment.