@tscircuit/schematic-trace-solver 0.0.165 → 0.0.167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,5 +9,8 @@ test("board 648 ESP-12F power and boot section", () => {
9
9
 
10
10
  solver.solve()
11
11
 
12
+ expect(solver.sameNetJunctionAlignmentSolver?.stats.collapsedCycleCount).toBe(
13
+ 0,
14
+ )
12
15
  expect(solver).toMatchSolverSnapshot(import.meta.path)
13
16
  })
@@ -1,9 +1,18 @@
1
1
  import { expect, test } from "bun:test"
2
2
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
- import type { InputProblem } from "lib/types/InputProblem"
3
+ import type { InputProblem, PinId } from "lib/types/InputProblem"
4
4
  import "tests/fixtures/matcher"
5
5
  import inputProblemJson from "./assets/repro-isolated-rs485-isow7841.input.json"
6
6
 
7
+ const EXPECTED_D_P_RAIL_Y = 2.2
8
+ const REDUNDANT_D_P_RAIL_Y = 2.3
9
+ const EXPECTED_GND1_RAIL_Y = 1.76
10
+ const GND1_RAIL_PIN_IDS = ["schematic_port_35", "schematic_port_33"] satisfies [
11
+ PinId,
12
+ PinId,
13
+ ]
14
+ const POINT_EPSILON = 1e-6
15
+
7
16
  test("repro isolated RS-485 ISOW7841 schematic traces", () => {
8
17
  const inputProblem: InputProblem = JSON.parse(
9
18
  JSON.stringify(inputProblemJson),
@@ -17,5 +26,28 @@ test("repro isolated RS-485 ISOW7841 schematic traces", () => {
17
26
  expect(solver.schematicTraceLinesSolver?.failedConnectionPairs).toHaveLength(
18
27
  0,
19
28
  )
29
+ expect(solver.sameNetJunctionAlignmentSolver?.stats.collapsedCycleCount).toBe(
30
+ 2,
31
+ )
32
+ const dPLabel =
33
+ solver.sameNetJunctionAlignmentSolver?.outputNetLabelPlacements.find(
34
+ (label) => label.netId === "D_P",
35
+ )
36
+ expect(dPLabel?.anchorPoint.y).toBeCloseTo(EXPECTED_D_P_RAIL_Y)
37
+ const dPLabelHostTrace =
38
+ solver.sameNetJunctionAlignmentSolver?.outputTraces.find((trace) =>
39
+ dPLabel?.mspConnectionPairIds.includes(trace.mspPairId),
40
+ )
41
+ expect(
42
+ dPLabelHostTrace?.tracePath.some(
43
+ (point) => Math.abs(point.y - REDUNDANT_D_P_RAIL_Y) < POINT_EPSILON,
44
+ ),
45
+ ).toBe(false)
46
+ const gnd1RailTrace =
47
+ solver.sameNetJunctionAlignmentSolver?.outputTraces.find((trace) =>
48
+ GND1_RAIL_PIN_IDS.every((pinId) => trace.pinIds.includes(pinId)),
49
+ )
50
+ expect(gnd1RailTrace?.tracePath[1]?.y).toBeCloseTo(EXPECTED_GND1_RAIL_Y)
51
+ expect(gnd1RailTrace?.tracePath[2]?.y).toBeCloseTo(EXPECTED_GND1_RAIL_Y)
20
52
  expect(solver).toMatchSolverSnapshot(import.meta.path)
21
53
  })
@@ -1,13 +1,22 @@
1
1
  import { expect, test } from "bun:test"
2
2
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
3
4
  import "tests/fixtures/matcher"
4
5
  import inputProblem from "./assets/repro-nrf52810-clock-routing.input.json"
5
6
 
7
+ const EXPECTED_CRYSTAL_RAIL_Y_BY_TRACE_ID = new Map([
8
+ ["schematic_port_42-schematic_port_175", 4.99],
9
+ ["schematic_port_46-schematic_port_181", 5],
10
+ ])
11
+ const REDUNDANT_UPPER_RAIL_Y = 5.1
12
+ const POINT_EPSILON = 1e-6
13
+
6
14
  // Extracted from the nRF52810 clock-source schematic shown in the reproduction:
7
15
  // a four-pin 32 MHz crystal and a two-pin 32.768 kHz crystal, each connected
8
16
  // to an MCU schematic box, two load capacitors, and section-local GND labels.
9
17
  test("repro nRF52810 HF and LF crystal trace/net-label routing", () => {
10
- const solver = new SchematicTracePipelineSolver(inputProblem as any)
18
+ const solverInput: InputProblem = JSON.parse(JSON.stringify(inputProblem))
19
+ const solver = new SchematicTracePipelineSolver(solverInput)
11
20
 
12
21
  solver.solve()
13
22
 
@@ -24,5 +33,21 @@ test("repro nRF52810 HF and LF crystal trace/net-label routing", () => {
24
33
  expect(
25
34
  solver.sameNetJunctionAlignmentSolver?.getOutput().netLabelPlacements,
26
35
  ).toHaveLength(10)
36
+ expect(solver.sameNetJunctionAlignmentSolver?.stats.collapsedCycleCount).toBe(
37
+ 2,
38
+ )
39
+ for (const [traceId, expectedRailY] of EXPECTED_CRYSTAL_RAIL_Y_BY_TRACE_ID) {
40
+ const crystalLoadTrace =
41
+ solver.sameNetJunctionAlignmentSolver?.outputTraces.find(
42
+ (trace) => trace.mspPairId === traceId,
43
+ )
44
+ expect(crystalLoadTrace?.tracePath[0]?.y).toBeCloseTo(expectedRailY)
45
+ expect(crystalLoadTrace?.tracePath[1]?.y).toBeCloseTo(expectedRailY)
46
+ expect(
47
+ crystalLoadTrace?.tracePath.some(
48
+ (point) => Math.abs(point.y - REDUNDANT_UPPER_RAIL_Y) < POINT_EPSILON,
49
+ ),
50
+ ).toBe(false)
51
+ }
27
52
  expect(solver).toMatchSolverSnapshot(import.meta.path)
28
53
  })
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test"
2
+ import { ConnectivityMap } from "connectivity-map"
2
3
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
4
  import type { InputProblem } from "lib/types/InputProblem"
4
5
  import "tests/fixtures/matcher"
@@ -12,22 +13,49 @@ const GND = "schematic_port_7"
12
13
  const C3_GND = "schematic_port_32"
13
14
  const R1_GND = "schematic_port_38"
14
15
 
15
- test("repro: PGA300 adds redundant ground branches at COMP", async () => {
16
+ test("PGA300 preserves COMP to R1 without redundant ground branches", async () => {
16
17
  const solver = new SchematicTracePipelineSolver(
17
18
  inputProblem as unknown as InputProblem,
18
19
  )
19
20
  solver.solve()
20
21
 
21
22
  expect(solver.solved).toBe(true)
22
- const { traces } = solver.netLabelToTraceSolver!.getOutput()
23
+ const { traces, netLabelPlacements } =
24
+ solver.netLabelToTraceSolver!.getOutput()
23
25
  const hasPair = (a: string, b: string) =>
24
26
  traces.some((trace) => trace.pinIds.includes(a) && trace.pinIds.includes(b))
25
27
 
26
- // Current bug: the global GND spanning tree substitutes these two branches
27
- // for the explicitly requested COMP -> R1 connection in the source circuit.
28
- expect(hasPair(COMP, GND)).toBe(true)
29
- expect(hasPair(COMP, C3_GND)).toBe(true)
30
- expect(hasPair(COMP, R1_GND)).toBe(false)
28
+ // GND labels provide global connectivity; they must not replace the
29
+ // explicitly requested COMP -> R1 connection with unrelated ground branches.
30
+ expect(hasPair(COMP, GND)).toBe(false)
31
+ expect(hasPair(COMP, C3_GND)).toBe(false)
32
+ expect(hasPair(COMP, R1_GND)).toBe(true)
33
+
34
+ // Removing physical branches must leave every original GND pin connected
35
+ // through a routed island and a ground label, including the capacitor pins.
36
+ const renderedConnectivity = new ConnectivityMap({})
37
+ for (const trace of traces)
38
+ renderedConnectivity.addConnections([trace.pinIds])
39
+ for (const label of netLabelPlacements) {
40
+ if (label.netId) {
41
+ renderedConnectivity.addConnections([[label.netId, ...label.pinIds]])
42
+ }
43
+ }
44
+ const groundNet = renderedConnectivity.getNetConnectedToId("GND")
45
+ expect(groundNet).toBeDefined()
46
+ for (const pinId of inputProblem.netConnections.find((net) => net.isGround)!
47
+ .pinIds) {
48
+ expect(renderedConnectivity.getNetConnectedToId(pinId)).toBe(groundNet)
49
+ }
50
+
51
+ // Keep the already-fixed vertical Q1 emitter/R1 connection.
52
+ const emitterTrace = traces.find(
53
+ (trace) =>
54
+ trace.pinIds.includes("schematic_port_34") &&
55
+ trace.pinIds.includes("schematic_port_37"),
56
+ )!
57
+ expect(emitterTrace).toBeDefined()
58
+ for (const point of emitterTrace.tracePath) expect(point.x).toBeCloseTo(5.49)
31
59
 
32
60
  await expect(solver).toMatchSolverSnapshot(import.meta.path)
33
61
  })
@@ -0,0 +1,120 @@
1
+ import { expect, test } from "bun:test"
2
+ import { LongDistancePairSolver } from "lib/solvers/LongDistancePairSolver/LongDistancePairSolver"
3
+ import { MspConnectionPairSolver } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
4
+ import { getGroundConnectionPolicy } from "lib/solvers/MspConnectionPairSolver/getGroundConnectionPolicy"
5
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+
8
+ // The explicit A-B and C-D wires are farther apart than their neighbors on
9
+ // the same named net. E is label-only, with no requested physical connection.
10
+ const createInput = (): InputProblem => ({
11
+ chips: [0, 4, 1, 5, 2].map((x, index) => {
12
+ const chipId = "ABCDE"[index]!
13
+ return {
14
+ chipId,
15
+ center: { x, y: 0 },
16
+ width: 0.2,
17
+ height: 0.2,
18
+ pins: [{ pinId: chipId, x, y: 0.1, _facingDirection: "y+" }],
19
+ }
20
+ }),
21
+ directConnections: [
22
+ { pinIds: ["A", "B"], netId: "AGND" },
23
+ { pinIds: ["C", "D"], netId: "AGND" },
24
+ ],
25
+ netConnections: [
26
+ { netId: "AGND", pinIds: ["A", "B", "C", "D", "E"], isGround: true },
27
+ ],
28
+ availableNetLabelOrientations: { AGND: ["y+"] },
29
+ maxMspPairDistance: 100,
30
+ })
31
+
32
+ const pairKeys = (solver: MspConnectionPairSolver) =>
33
+ solver.mspConnectionPairs
34
+ .map((pair) =>
35
+ pair.pins
36
+ .map((pin) => pin.pinId)
37
+ .sort()
38
+ .join("-"),
39
+ )
40
+ .sort()
41
+
42
+ test("marked ground nets retain explicit islands even with identical netIds", () => {
43
+ const inputProblem = createInput()
44
+ const canRoute = getGroundConnectionPolicy(inputProblem)
45
+ expect(canRoute("A", "B")).toBe(true)
46
+ expect(canRoute("C", "D")).toBe(true)
47
+ expect(canRoute("A", "C")).toBe(false)
48
+ expect(canRoute("A", "E")).toBe(false)
49
+
50
+ const solver = new MspConnectionPairSolver({ inputProblem })
51
+ solver.solve()
52
+ expect(pairKeys(solver)).toEqual(["A-B", "C-D"])
53
+ // Physical partitioning must not split the electrical net.
54
+ expect(solver.globalConnMap.getNetConnectedToId("A")).toBe(
55
+ solver.globalConnMap.getNetConnectedToId("E"),
56
+ )
57
+ })
58
+
59
+ test.each([false, undefined])(
60
+ "isGround=%s preserves automatic net routing",
61
+ (isGround) => {
62
+ const inputProblem = createInput()
63
+ inputProblem.netConnections[0]!.isGround = isGround
64
+ const solver = new MspConnectionPairSolver({ inputProblem })
65
+ solver.solve()
66
+ expect(getGroundConnectionPolicy(inputProblem)("A", "C")).toBe(true)
67
+ expect(solver.mspConnectionPairs).toHaveLength(4)
68
+ },
69
+ )
70
+
71
+ test("ground nets without explicit wires retain automatic shared buses", () => {
72
+ const inputProblem = createInput()
73
+ inputProblem.directConnections = []
74
+ const solver = new MspConnectionPairSolver({ inputProblem })
75
+ solver.solve()
76
+ expect(getGroundConnectionPolicy(inputProblem)("A", "C")).toBe(true)
77
+ expect(solver.mspConnectionPairs).toHaveLength(4)
78
+ })
79
+
80
+ test("long-distance recovery cannot reconnect separate ground islands", () => {
81
+ const inputProblem = createInput()
82
+ const solver = new LongDistancePairSolver({
83
+ inputProblem,
84
+ alreadySolvedTraces: [],
85
+ primaryMspConnectionPairs: [],
86
+ failedConnectionPairs: [],
87
+ })
88
+ solver.solve()
89
+ expect(solver.solvedLongDistanceTraces.length).toBeGreaterThan(0)
90
+ for (const trace of solver.solvedLongDistanceTraces) {
91
+ expect(["A-B", "C-D"]).toContain([...trace.pinIds].sort().join("-"))
92
+ }
93
+ })
94
+
95
+ test("explicit ground connections still respect distance and section limits", () => {
96
+ const inputProblem = createInput()
97
+ inputProblem.maxMspPairDistance = 0.5
98
+ const solver = new MspConnectionPairSolver({ inputProblem })
99
+ solver.solve()
100
+ expect(solver.mspConnectionPairs).toHaveLength(0)
101
+
102
+ inputProblem.maxMspPairDistance = 100
103
+ inputProblem.chips[0]!.sectionId = "left"
104
+ inputProblem.chips[1]!.sectionId = "right"
105
+ const sectionSolver = new MspConnectionPairSolver({ inputProblem })
106
+ sectionSolver.solve()
107
+ expect(pairKeys(sectionSolver)).toEqual(["C-D"])
108
+ })
109
+
110
+ test("label-only ground pins remain connected in the pipeline output", () => {
111
+ const solver = new SchematicTracePipelineSolver(createInput())
112
+ solver.solve()
113
+ const output = solver.netLabelToTraceSolver!.getOutput()
114
+ expect(solver.solved).toBe(true)
115
+ expect(
116
+ output.netLabelPlacements.some(
117
+ (label) => label.netId === "AGND" && label.pinIds.includes("E"),
118
+ ),
119
+ ).toBe(true)
120
+ })
@@ -0,0 +1,154 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { Point } from "@tscircuit/math-utils"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { collapseSameNetCycles } from "lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles"
5
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import {
7
+ getVisibleTraceLength,
8
+ getVisibleTraceSegmentCount,
9
+ } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
10
+ import type { PinId } from "lib/types/InputProblem"
11
+
12
+ interface TraceFixtureInput {
13
+ mspPairId: string
14
+ pinIds: [PinId, PinId]
15
+ tracePath: Point[]
16
+ }
17
+
18
+ const createTraceFixture = ({
19
+ mspPairId,
20
+ pinIds,
21
+ tracePath,
22
+ }: TraceFixtureInput): SolvedTracePath => ({
23
+ mspPairId,
24
+ dcConnNetId: "same-net",
25
+ globalConnNetId: "same-net",
26
+ pins: [
27
+ {
28
+ pinId: pinIds[0],
29
+ chipId: `chip-${pinIds[0]}`,
30
+ ...tracePath[0]!,
31
+ },
32
+ {
33
+ pinId: pinIds[1],
34
+ chipId: `chip-${pinIds[1]}`,
35
+ ...tracePath.at(-1)!,
36
+ },
37
+ ],
38
+ tracePath,
39
+ mspConnectionPairIds: [mspPairId],
40
+ pinIds,
41
+ })
42
+
43
+ test("collapses a same-net cycle between a shared pin and crossing", () => {
44
+ const traces = [
45
+ createTraceFixture({
46
+ mspPairId: "upper-to-shared",
47
+ pinIds: ["upper", "shared"],
48
+ tracePath: [
49
+ { x: 8, y: 2 },
50
+ { x: 0, y: 2 },
51
+ { x: 0, y: 0 },
52
+ ],
53
+ }),
54
+ createTraceFixture({
55
+ mspPairId: "shared-to-lower",
56
+ pinIds: ["shared", "lower"],
57
+ tracePath: [
58
+ { x: 0, y: 0 },
59
+ { x: 6, y: 0 },
60
+ { x: 6, y: 4 },
61
+ { x: 8, y: 4 },
62
+ ],
63
+ }),
64
+ ]
65
+ const netLabelPlacement: NetLabelPlacement = {
66
+ globalConnNetId: "same-net",
67
+ dcConnNetId: "same-net",
68
+ mspConnectionPairIds: ["upper-to-shared"],
69
+ pinIds: ["upper", "shared"],
70
+ orientation: "x-",
71
+ anchorPoint: { x: 0, y: 2 },
72
+ width: 0.5,
73
+ height: 0.2,
74
+ center: { x: -0.25, y: 2 },
75
+ }
76
+
77
+ const result = collapseSameNetCycles({
78
+ traces,
79
+ netLabelPlacements: [netLabelPlacement],
80
+ })
81
+
82
+ expect(result.collapsedCycleCount).toBe(1)
83
+ expect(getVisibleTraceLength(traces)).toBe(22)
84
+ expect(getVisibleTraceLength(result.traces)).toBe(14)
85
+ expect(getVisibleTraceSegmentCount(result.traces)).toBe(4)
86
+ expect(result.traces[0]?.tracePath.at(-1)).toEqual({ x: 0, y: 0 })
87
+ expect(result.netLabelPlacements[0]?.anchorPoint).toEqual({ x: 0, y: 0 })
88
+ })
89
+
90
+ test("keeps a valid shared same-net branch", () => {
91
+ const traces = [
92
+ createTraceFixture({
93
+ mspPairId: "shared-to-branch",
94
+ pinIds: ["shared", "branch"],
95
+ tracePath: [
96
+ { x: 0, y: 0 },
97
+ { x: 4, y: 0 },
98
+ { x: 4, y: 2 },
99
+ ],
100
+ }),
101
+ createTraceFixture({
102
+ mspPairId: "shared-to-trunk",
103
+ pinIds: ["shared", "trunk"],
104
+ tracePath: [
105
+ { x: 0, y: 0 },
106
+ { x: 4, y: 0 },
107
+ { x: 8, y: 0 },
108
+ ],
109
+ }),
110
+ ]
111
+
112
+ const result = collapseSameNetCycles({
113
+ traces,
114
+ netLabelPlacements: [],
115
+ })
116
+
117
+ expect(result.collapsedCycleCount).toBe(0)
118
+ expect(result.traces).toEqual(traces)
119
+ })
120
+
121
+ test("does not lengthen a trace to reuse shared donor geometry", () => {
122
+ const traces = [
123
+ createTraceFixture({
124
+ mspPairId: "short-trace",
125
+ pinIds: ["short-end", "shared"],
126
+ tracePath: [
127
+ { x: 0, y: 0 },
128
+ { x: 0, y: -1 },
129
+ { x: 2, y: -1 },
130
+ { x: 2, y: 0 },
131
+ ],
132
+ }),
133
+ createTraceFixture({
134
+ mspPairId: "long-donor",
135
+ pinIds: ["shared", "donor-end"],
136
+ tracePath: [
137
+ { x: 2, y: 0 },
138
+ { x: 2, y: -1 },
139
+ { x: 4, y: -1 },
140
+ { x: 4, y: -2 },
141
+ { x: 1, y: -2 },
142
+ { x: 1, y: 1 },
143
+ { x: 5, y: 1 },
144
+ ],
145
+ }),
146
+ ]
147
+
148
+ const result = collapseSameNetCycles({
149
+ traces,
150
+ netLabelPlacements: [],
151
+ })
152
+
153
+ expect(result.traces[0]).toEqual(traces[0])
154
+ })