diff --git a/generate_preview.ts b/generate_preview.ts new file mode 100644 index 0000000..7ead802 --- /dev/null +++ b/generate_preview.ts @@ -0,0 +1,174 @@ +import { SingleInnerPartitionPackingSolver } from "./lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "./lib/types/InputProblem" + +// 构造包含 5 个解耦电容的 Mock Input +const problem: PartitionInputProblem = { + isPartition: true, + partitionType: "decoupling_caps", + chipMap: { + C1: { + chipId: "C1", + pins: ["C1_P1", "C1_P2"], + size: { x: 1.0, y: 0.5 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2_P1", "C2_P2"], + size: { x: 1.2, y: 0.6 }, + availableRotations: [0, 180], + }, + C3: { + chipId: "C3", + pins: ["C3_P1", "C3_P2"], + size: { x: 0.8, y: 0.4 }, + availableRotations: [0, 180], + }, + C4: { + chipId: "C4", + pins: ["C4_P1", "C4_P2"], + size: { x: 1.5, y: 0.7 }, + availableRotations: [0, 180], + }, + C5: { + chipId: "C5", + pins: ["C5_P1", "C5_P2"], + size: { x: 0.9, y: 0.45 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + C1_P1: { pinId: "C1_P1", offset: { x: 0, y: 0.25 }, side: "y+" }, + C1_P2: { pinId: "C1_P2", offset: { x: 0, y: -0.25 }, side: "y-" }, + C2_P1: { pinId: "C2_P1", offset: { x: 0, y: 0.3 }, side: "y+" }, + C2_P2: { pinId: "C2_P2", offset: { x: 0, y: -0.3 }, side: "y-" }, + C3_P1: { pinId: "C3_P1", offset: { x: 0, y: 0.2 }, side: "y+" }, + C3_P2: { pinId: "C3_P2", offset: { x: 0, y: -0.2 }, side: "y-" }, + C4_P1: { pinId: "C4_P1", offset: { x: 0, y: 0.35 }, side: "y+" }, + C4_P2: { pinId: "C4_P2", offset: { x: 0, y: -0.35 }, side: "y-" }, + C5_P1: { pinId: "C5_P1", offset: { x: 0, y: 0.225 }, side: "y+" }, + C5_P2: { pinId: "C5_P2", offset: { x: 0, y: -0.225 }, side: "y-" }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C1_P1-VCC": true, + "C1_P2-GND": true, + "C2_P1-VCC": true, + "C2_P2-GND": true, + "C3_P1-VCC": true, + "C3_P2-GND": true, + "C4_P1-VCC": true, + "C4_P2-GND": true, + "C5_P1-VCC": true, + "C5_P2-GND": true, + }, + chipGap: 0.2, + partitionGap: 2, + decouplingCapsGap: 0.3, +} + +const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, +}) + +solver.step() + +const layout = solver.layout! +const placements = layout.chipPlacements + +// 计算总宽度用于 SVG 视图框 +const chipIds = Object.keys(problem.chipMap).sort() +let totalWidth = 0 +let maxHeight = 0 +for (const id of chipIds) { + const chip = problem.chipMap[id]! + totalWidth += chip.size.x + totalWidth += problem.decouplingCapsGap! + maxHeight = Math.max(maxHeight, chip.size.y) +} +totalWidth -= problem.decouplingCapsGap! + +const viewBoxWidth = totalWidth + 2 +const viewBoxHeight = maxHeight + 1 + +// 生成 SVG +const svgParts: string[] = [] +svgParts.push( + ``, +) +svgParts.push( + ` `, +) + +// 绘制每个电容 +for (const id of chipIds) { + const placement = placements[id]! + const chip = problem.chipMap[id]! + const halfWidth = chip.size.x / 2 + const halfHeight = chip.size.y / 2 + + const x = placement.x - halfWidth + const y = placement.y - halfHeight + + svgParts.push( + ` `, + ) + svgParts.push( + ` ${id}`, + ) + svgParts.push( + ` x:${placement.x.toFixed(2)}, y:${placement.y.toFixed(2)}`, + ) +} + +// 添加标题 +svgParts.push( + ` Decoupling Capacitors Linear Layout`, +) +svgParts.push( + ` 5 caps arranged horizontally with 0.3 gap`, +) + +svgParts.push(``) + +const htmlContent = ` + + + Decoupling Caps Layout Preview + + + +
+

Decoupling Capacitors Linear Layout Preview

+
+

Algorithm: _layoutDecouplingCapsLinear()

+

Gap: 0.3 | Coordinate System: Center-based

+

Sorting: chipId localeCompare (deterministic)

+
+
${svgParts.join("\n")}
+

Layout JSON

+
${JSON.stringify(layout, null, 2)}
+
+ +` + +await Bun.write("preview.html", htmlContent) +console.log("✅ preview.html generated successfully!") +console.log("📐 Layout Summary:") +for (const id of chipIds) { + const p = placements[id]! + console.log( + ` ${id}: x=${p.x.toFixed(2)}, y=${p.y.toFixed(2)}, rotation=${p.ccwRotationDegrees}°`, + ) +} diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..d895f88 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -38,6 +38,15 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + // Injection point: if decoupling caps, intercept and use linear layout + if ( + this.partitionInputProblem.partitionType === "decoupling_caps" && + !this.solved + ) { + this._layoutDecouplingCapsLinear() + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +73,52 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private _layoutDecouplingCapsLinear() { + const input = this.partitionInputProblem + + // Step 1: Deterministic sort by chipId for consistent results across environments + const chips = Object.values(input.chipMap).sort((a, b) => + a.chipId.localeCompare(b.chipId), + ) + const gap = input.decouplingCapsGap ?? input.chipGap ?? 0.2 + + const chipPlacements: Record = {} + + // Step 2: Pre-calculate total width for center alignment + let totalWidth = 0 + let maxHeight = 0 + for (let i = 0; i < chips.length; i++) { + totalWidth += chips[i]!.size.x + if (i < chips.length - 1) { + totalWidth += gap + } + maxHeight = Math.max(maxHeight, chips[i]!.size.y) + } + + // Step 3: Linear coordinate assignment (center-based coordinate system) + let currentX = -(totalWidth / 2) + for (const chip of chips) { + const halfWidth = chip.size.x / 2 + currentX += halfWidth + + // Move to chip center + chipPlacements[chip.chipId] = { + x: currentX, + y: 0, // horizontal alignment + ccwRotationDegrees: chip.availableRotations?.[0] ?? 0, + } + + currentX += halfWidth + gap + } + + // Step 4: Package output + this.layout = { + chipPlacements, + groupPlacements: {}, + } + this.solved = true + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/package.json b/package.json index 1fd506e..cb83a2d 100644 --- a/package.json +++ b/package.json @@ -31,5 +31,8 @@ }, "peerDependencies": { "typescript": "^5" + }, + "dependencies": { + "circuit-to-svg": "^0.0.332" } } diff --git a/preview.html b/preview.html new file mode 100644 index 0000000..eb7215d --- /dev/null +++ b/preview.html @@ -0,0 +1,74 @@ + + + + Decoupling Caps Layout Preview + + + +
+

Decoupling Capacitors Linear Layout Preview

+
+

Algorithm: _layoutDecouplingCapsLinear()

+

Gap: 0.3 | Coordinate System: Center-based

+

Sorting: chipId localeCompare (deterministic)

+
+
+ + + C1 + x:-2.80, y:0.00 + + C2 + x:-1.40, y:0.00 + + C3 + x:-0.10, y:0.00 + + C4 + x:1.35, y:0.00 + + C5 + x:2.85, y:0.00 + Decoupling Capacitors Linear Layout + 5 caps arranged horizontally with 0.3 gap +
+

Layout JSON

+
{
+  "chipPlacements": {
+    "C1": {
+      "x": -2.8,
+      "y": 0,
+      "ccwRotationDegrees": 0
+    },
+    "C2": {
+      "x": -1.4,
+      "y": 0,
+      "ccwRotationDegrees": 0
+    },
+    "C3": {
+      "x": -0.09999999999999998,
+      "y": 0,
+      "ccwRotationDegrees": 0
+    },
+    "C4": {
+      "x": 1.35,
+      "y": 0,
+      "ccwRotationDegrees": 0
+    },
+    "C5": {
+      "x": 2.8500000000000005,
+      "y": 0,
+      "ccwRotationDegrees": 0
+    }
+  },
+  "groupPlacements": {}
+}
+
+ + \ No newline at end of file diff --git a/tests/LinearDecouplingLayout.test.ts b/tests/LinearDecouplingLayout.test.ts new file mode 100644 index 0000000..3b134db --- /dev/null +++ b/tests/LinearDecouplingLayout.test.ts @@ -0,0 +1,91 @@ +import { test, expect } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "../lib/types/InputProblem" + +test("LinearDecouplingLayout - 3 chips horizontal alignment", () => { + // 构造包含 3 个不同尺寸芯片的 PartitionInputProblem + const problem: PartitionInputProblem = { + isPartition: true, + partitionType: "decoupling_caps", + chipMap: { + C1: { + chipId: "C1", + pins: ["C1_P1", "C1_P2"], + size: { x: 1.0, y: 0.5 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2_P1", "C2_P2"], + size: { x: 1.2, y: 0.6 }, + availableRotations: [0, 180], + }, + C3: { + chipId: "C3", + pins: ["C3_P1", "C3_P2"], + size: { x: 0.8, y: 0.4 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + C1_P1: { pinId: "C1_P1", offset: { x: 0, y: 0.25 }, side: "y+" }, + C1_P2: { pinId: "C1_P2", offset: { x: 0, y: -0.25 }, side: "y-" }, + C2_P1: { pinId: "C2_P1", offset: { x: 0, y: 0.3 }, side: "y+" }, + C2_P2: { pinId: "C2_P2", offset: { x: 0, y: -0.3 }, side: "y-" }, + C3_P1: { pinId: "C3_P1", offset: { x: 0, y: 0.2 }, side: "y+" }, + C3_P2: { pinId: "C3_P2", offset: { x: 0, y: -0.2 }, side: "y-" }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C1_P1-VCC": true, + "C1_P2-GND": true, + "C2_P1-VCC": true, + "C2_P2-GND": true, + "C3_P1-VCC": true, + "C3_P2-GND": true, + }, + chipGap: 0.2, + partitionGap: 2, + decouplingCapsGap: 0.3, + } + + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, + }) + + // 执行布局 + solver.step() + + // 断言:布局已解决 + expect(solver.solved).toBe(true) + expect(solver.layout).not.toBeNull() + + const placements = solver.layout!.chipPlacements + + // 断言:所有芯片的 y 坐标必须为 0 + expect(placements.C1!.y).toBe(0) + expect(placements.C2!.y).toBe(0) + expect(placements.C3!.y).toBe(0) + + // 断言:芯片按 chipId 排序 (C1, C2, C3) + // C1 在左,C2 在中,C3 在右 + expect(placements.C1!.x).toBeLessThan(placements.C2!.x) + expect(placements.C2!.x).toBeLessThan(placements.C3!.x) + + // 断言:芯片间的间距必须精确等于 gap (0.3) + // C1 和 C2 之间的间距 + const gap1 = placements.C2!.x - placements.C1!.x - 1.0 / 2 - 1.2 / 2 + expect(Math.abs(gap1 - 0.3)).toBeLessThan(0.0001) + + // C2 和 C3 之间的间距 + const gap2 = placements.C3!.x - placements.C2!.x - 1.2 / 2 - 0.8 / 2 + expect(Math.abs(gap2 - 0.3)).toBeLessThan(0.0001) + + // 输出布局快照 + console.log("Layout Snapshot:", JSON.stringify(solver.layout, null, 2)) +})