From a53355b2b43148d35d610ecc7d874c0faa466263 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Fri, 24 Jul 2026 19:04:30 +0900 Subject: [PATCH] fix: use declared pin side when grouping same-side passives Progress on #15. findSameSidePassiveGroups inferred a main-chip pin's side from its offset (|x| vs |y|), which misclassifies corner pins on tall chips: U3.44 at offset (-1.9, 2.0) is declared side x- but reads as y+ from the offset. On the LayoutPipelineSolver06 board this split U3's decoupling caps into an x- group and a phantom y+ group, so C10/C11 (and rail siblings C7/C18) were never grouped with the left-edge rows and packed as loose singles. The declared ChipPin.side is now authoritative, rotated by the chip's fixed rotation; offset inference remains as a fallback for pins without a declared side. All caps on the 06 board now group on x-, and the RP2040 boards keep their existing rows (snapshots unchanged). --- .../findSameSidePassiveGroups.ts | 26 +++++++++++++++---- ...dSameSidePassiveGroups-corner-pins.test.ts | 24 +++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tests/PackInnerPartitionsSolver/findSameSidePassiveGroups-corner-pins.test.ts diff --git a/lib/solvers/PackInnerPartitionsSolver/findSameSidePassiveGroups.ts b/lib/solvers/PackInnerPartitionsSolver/findSameSidePassiveGroups.ts index c9930288..eabd49ca 100644 --- a/lib/solvers/PackInnerPartitionsSolver/findSameSidePassiveGroups.ts +++ b/lib/solvers/PackInnerPartitionsSolver/findSameSidePassiveGroups.ts @@ -69,12 +69,28 @@ const getNetForPin = (problem: InputProblem, pinId: PinId): NetId | null => { return null } -/** Main-chip side a pin offset points to once the chip's rotation is applied. */ +/** Rotate a declared side by a CCW quarter-turn rotation. */ +const SIDE_ROTATION_ORDER: Side[] = ["x+", "y+", "x-", "y-"] +const rotateSide = (side: Side, ccwRotationDegrees: number): Side => { + const turns = ((Math.round(ccwRotationDegrees / 90) % 4) + 4) % 4 + const index = SIDE_ROTATION_ORDER.indexOf(side) + return SIDE_ROTATION_ORDER[(index + turns) % 4]! +} + +/** + * Main-chip side a pin sits on once the chip's rotation is applied. + * + * Prefers the pin's declared `side` — offset inference misclassifies corner + * pins (e.g. a left-edge pin near the top of a tall chip has |y| > |x|, which + * would wrongly read as "y+") — and falls back to offset inference when no + * side is declared. + */ const getMainChipPinSide = ( - offset: { x: number; y: number }, + pin: { offset: { x: number; y: number }; side?: Side }, mainChipRotation: number, ): Side => { - const o = rotatePinOffset(offset, mainChipRotation) + if (pin.side) return rotateSide(pin.side, mainChipRotation) + const o = rotatePinOffset(pin.offset, mainChipRotation) if (Math.abs(o.x) >= Math.abs(o.y)) { if (o.x >= 0) return "x+" return "x-" @@ -164,7 +180,7 @@ export const findSameSidePassiveGroups = ( commonNodeGroups.push({ mainChipId, side: getMainChipPinSide( - mainChipPin.offset, + mainChipPin, mainChip.availableRotations?.[0] ?? 0, ), passiveChipIds, @@ -215,7 +231,7 @@ export const findSameSidePassiveGroups = ( mainChipRotation, ) const side = getMainChipPinSide( - problem.chipPinMap[mainChipPinId]!.offset, + problem.chipPinMap[mainChipPinId]!, mainChipRotation, ) candidates.push({ diff --git a/tests/PackInnerPartitionsSolver/findSameSidePassiveGroups-corner-pins.test.ts b/tests/PackInnerPartitionsSolver/findSameSidePassiveGroups-corner-pins.test.ts new file mode 100644 index 00000000..7e7abff8 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/findSameSidePassiveGroups-corner-pins.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "bun:test" +import { findSameSidePassiveGroups } from "lib/solvers/PackInnerPartitionsSolver/findSameSidePassiveGroups" +import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page" + +// https://github.com/tscircuit/matchpack/issues/15 +// +// U3's decoupling caps all hang off its LEFT edge (every rail pin declares +// side "x-"), but side inference from pin offsets misclassified the corner +// pins: U3.44 sits near the top of the tall chip at offset (-1.9, 2.0), where +// |y| > |x| reads as "y+". That split the caps into an x- group and a phantom +// y+ group, so C10/C11/C7/C18 never joined the left-edge rows. +// +// The declared pin side is authoritative; offset inference is only a fallback. +test("corner pins keep their declared side when grouping same-side passives", () => { + const groups = findSameSidePassiveGroups(problem as any) + + // All caps must land in x- groups — no phantom y+ group from corner pins + expect(groups.every((group) => group.side === "x-")).toBe(true) + + const groupedCaps = new Set(groups.flatMap((group) => group.passiveChipIds)) + for (const capId of ["C10", "C11", "C7", "C18"]) { + expect(groupedCaps.has(capId)).toBe(true) + } +})