diff --git a/bun.lockb b/bun.lockb index 074f9c6..94d2e7e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cli/run/process-circuit-file-locally.ts b/cli/run/process-circuit-file-locally.ts new file mode 100644 index 0000000..bd9ba94 --- /dev/null +++ b/cli/run/process-circuit-file-locally.ts @@ -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 + } +} diff --git a/cli/run/process-circuit-file.ts b/cli/run/process-circuit-file.ts index 39f2edd..7271171 100644 --- a/cli/run/process-circuit-file.ts +++ b/cli/run/process-circuit-file.ts @@ -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 @@ -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" }, }, @@ -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 }) diff --git a/cli/run/register.ts b/cli/run/register.ts index b29bf3d..6681694 100644 --- a/cli/run/register.ts +++ b/cli/run/register.ts @@ -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")) { @@ -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) { diff --git a/cli/run/run-autorouter.ts b/cli/run/run-autorouter.ts index de9b38e..41fb594 100644 --- a/cli/run/run-autorouter.ts +++ b/cli/run/run-autorouter.ts @@ -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") @@ -15,6 +14,7 @@ interface RunAutorouterOptions { isDataset?: boolean ky?: KyInstance serverUrl?: string + isLocal?: boolean } export async function runAutorouter({ @@ -22,10 +22,16 @@ export async function runAutorouter({ 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}`) @@ -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`) diff --git a/cli/utils/circuit-json-to dsn-file-converter.ts b/cli/utils/circuit-json-to dsn-file-converter.ts new file mode 100644 index 0000000..f017480 --- /dev/null +++ b/cli/utils/circuit-json-to dsn-file-converter.ts @@ -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 { + // 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 +} diff --git a/package.json b/package.json index d7380ec..1ce54d9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/cli/run/run-autorouter-local1.test.ts b/tests/cli/run/run-autorouter-local1.test.ts new file mode 100644 index 0000000..31c8036 --- /dev/null +++ b/tests/cli/run/run-autorouter-local1.test.ts @@ -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 diff --git a/tests/cli/run/run-autorouter-local2.test.ts b/tests/cli/run/run-autorouter-local2.test.ts new file mode 100644 index 0000000..40dabec --- /dev/null +++ b/tests/cli/run/run-autorouter-local2.test.ts @@ -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 diff --git a/tests/cli/run/run-autorouter-local3.test.ts b/tests/cli/run/run-autorouter-local3.test.ts new file mode 100644 index 0000000..eeca559 --- /dev/null +++ b/tests/cli/run/run-autorouter-local3.test.ts @@ -0,0 +1,69 @@ +import { expect, test } from "bun:test" +import { temporaryDirectory } from "tempy" +import { runAutorouter } from "@/cli/run/run-autorouter" +import { existsSync } from "fs" +import { join } from "path/posix" +import { writeFile, mkdir } from "fs/promises" +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 handle dataset with missing circuit files using local server", async () => { + expect.assertions(4) + // Create a temporary dataset directory structure + const datasetDir = temporaryDirectory() + + // Create sample folders - some with circuit files, some without + const samplesWithCircuit = ["sample1", "sample3"] + const samplesWithoutCircuit = ["sample2", "sample4"] + + // Create folders with circuit files + for (const sampleDir of samplesWithCircuit) { + const fullSamplePath = join(datasetDir, sampleDir) + await mkdir(fullSamplePath, { recursive: true }) + await createMockCircuitFile(fullSamplePath) + } + + // Create folders without circuit files + for (const sampleDir of samplesWithoutCircuit) { + const fullSamplePath = join(datasetDir, sampleDir) + await mkdir(fullSamplePath, { recursive: true }) + } + + // Run autorouter + await runAutorouter({ + inputPath: datasetDir, + autorouter: "freerouting", + isDataset: true, + serverUrl: "http://localhost:3000", + isLocal: true, + }) + + // Check folders that should have output + for (const sampleDir of samplesWithCircuit) { + const outputPath = join( + datasetDir, + sampleDir, + "outputs", + "freerouting_routed_circuit.json", + ) + expect(existsSync(outputPath)).toBe(true) + } + + // Check folders that should not have output + for (const sampleDir of samplesWithoutCircuit) { + const outputPath = join( + datasetDir, + sampleDir, + "outputs", + "freerouting_routed_circuit.json", + ) + expect(existsSync(outputPath)).toBe(false) + } +}, 30000) // Increase timeout to 30 seconds diff --git a/tests/cli/run/run-autorouter-local4.test.ts b/tests/cli/run/run-autorouter-local4.test.ts new file mode 100644 index 0000000..b3ab630 --- /dev/null +++ b/tests/cli/run/run-autorouter-local4.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test" +import { temporaryDirectory } from "tempy" +import { runAutorouter } from "@/cli/run/run-autorouter" +import { join } from "path/posix" +import { writeFile, mkdir } from "fs/promises" +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 handle local autorouter errors gracefully", async () => { + const tempDir = temporaryDirectory() + await createMockCircuitFile(tempDir) + const inputPath = join(tempDir, "unrouted_circuit.json") + + await expect( + runAutorouter({ + inputPath, + autorouter: "invalid_autorouter", + serverUrl: "http://localhost:3000", + isLocal: true, + }), + ).rejects.toThrow() +}) diff --git a/tests/cli/run/run-autorouter3.test.ts b/tests/cli/run/run-autorouter3.test.ts index 9ec4fb3..be9fb6a 100644 --- a/tests/cli/run/run-autorouter3.test.ts +++ b/tests/cli/run/run-autorouter3.test.ts @@ -64,4 +64,4 @@ test("should handle dataset with missing circuit files", async () => { ) expect(existsSync(outputPath)).toBe(false) } -}) +}, 30000) diff --git a/tests/cli/run/run-autorouter4.test.ts b/tests/cli/run/run-autorouter4.test.ts index 9309143..503a2fe 100644 --- a/tests/cli/run/run-autorouter4.test.ts +++ b/tests/cli/run/run-autorouter4.test.ts @@ -1,10 +1,8 @@ -import { describe, expect, test } from "bun:test" +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) => { @@ -14,143 +12,17 @@ const createMockCircuitFile = async (dir: string) => { JSON.stringify(mockCircuit, null, 2), ) } - -describe("autorouter run", () => { - test("should run 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 createMockCircuitFile(sampleDir) - const inputPath = join(sampleDir, "unrouted_circuit.json") - - // Run command - await runAutorouter({ +test("should handle autorouter errors gracefully", async () => { + const tempDir = temporaryDirectory() + await createMockCircuitFile(tempDir) + const inputPath = join(tempDir, "unrouted_circuit.json") + + // Test with invalid autorouter name + await expect( + runAutorouter({ inputPath, - autorouter: "freerouting", - serverUrl: "https://registry-api.tscircuit.com", - }) - - // 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) - }, 10000) // Increase timeout to 10 seconds - - test("should run 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) - } - - // Run command - await runAutorouter({ - inputPath: datasetDir, - autorouter: "freerouting", - isDataset: true, - serverUrl: "https://registry-api.tscircuit.com", - }) - - // 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) - } - }, 15000) - - test("should handle dataset with missing circuit files", async () => { - // Create a temporary dataset directory structure - const datasetDir = temporaryDirectory() - - // Create sample folders - some with circuit files, some without - const samplesWithCircuit = ["sample1", "sample3"] - const samplesWithoutCircuit = ["sample2", "sample4"] - - // Create folders with circuit files - for (const sampleDir of samplesWithCircuit) { - const fullSamplePath = join(datasetDir, sampleDir) - await mkdir(fullSamplePath, { recursive: true }) - await createMockCircuitFile(fullSamplePath) - } - - // Create folders without circuit files - for (const sampleDir of samplesWithoutCircuit) { - const fullSamplePath = join(datasetDir, sampleDir) - await mkdir(fullSamplePath, { recursive: true }) - } - - // Run autorouter - await runAutorouter({ - inputPath: datasetDir, - autorouter: "freerouting", - isDataset: true, + autorouter: "invalid_autorouter", serverUrl: "https://registry-api.tscircuit.com", - }) - - // Check folders that should have output - for (const sampleDir of samplesWithCircuit) { - const outputPath = join( - datasetDir, - sampleDir, - "outputs", - "freerouting_routed_circuit.json", - ) - expect(existsSync(outputPath)).toBe(true) - } - - // Check folders that should not have output - for (const sampleDir of samplesWithoutCircuit) { - const outputPath = join( - datasetDir, - sampleDir, - "outputs", - "freerouting_routed_circuit.json", - ) - expect(existsSync(outputPath)).toBe(false) - } - }) - - test("should handle autorouter errors gracefully", async () => { - const tempDir = temporaryDirectory() - await createMockCircuitFile(tempDir) - const inputPath = join(tempDir, "unrouted_circuit.json") - - // Test with invalid autorouter name - await expect( - runAutorouter({ - inputPath, - autorouter: "invalid_autorouter", - serverUrl: "https://registry-api.tscircuit.com", - }), - ).rejects.toThrow() - }) + }), + ).rejects.toThrow() })