0hmX/dual-usb-peltier-assembly

A tscircuit package created by 0hmX.

Version
0.4.12
License
unset
Stars
0

tests/routing.test.tsx

import { beforeAll, describe, expect, test } from "bun:test"
import { createHash } from "node:crypto"
import type { PcbTraceRoutePoint } from "circuit-json"
import * as checks from "@tscircuit/checks"
import routingData from "../src/board/routing-data.json"
import type { RoutedCopperData } from "../src/board/routed-copper"
import { assertCopperAssociations } from "./helpers/copper-associations"
import { analyzePhysicalCopper, drillSmtClearanceViolations, sourceConnectivity } from "./helpers/physical-copper"
import { inspectThermalVias } from "./helpers/thermal-vias"
import { boardFixture, FIXTURE_RENDER_TIMEOUT } from "./fixtures"

let circuit: Awaited<ReturnType<typeof boardFixture>>
let physical: ReturnType<typeof analyzePhysicalCopper>
const routeKey = (sourceTraceId: string | undefined, route: PcbTraceRoutePoint[]) => JSON.stringify([
  sourceTraceId, route.map(point => point.route_type === "wire"
    ? ["wire", point.x, point.y, point.width, point.layer]
    : point.route_type === "via" ? ["via", point.x, point.y, point.from_layer, point.to_layer]
      : ["through_pad", point.start, point.end, point.width, point.start_layer, point.end_layer]),
])
beforeAll(async () => {
  circuit = await boardFixture()
  physical = analyzePhysicalCopper(circuit.getCircuitJson())
}, FIXTURE_RENDER_TIMEOUT)

describe("complete PCB routing", () => {
  test("routing preserves the original named pins, NC pins and electrical net partition", () => {
    // Hash of canonical {net names, component.pin groups} from the original
    // unrouted board. Copper IDs, placements and generated via ports are absent.
    const originalNetlist = "2df111c39b7d3294a0155048451ab575d62202339edb9e6157b2c3a0de41d77b"
    expect(createHash("sha256").update(sourceConnectivity(circuit.getCircuitJson()).signature).digest("hex"))
      .toBe(originalNetlist)
    expect(physical.requiredPortCount).toBe(430)
    expect(physical.groups).toHaveLength(76)
    expect(physical.ignoredNcPortCount).toBe(39)
    // Physical package metal is limited to the original connector shells and
    // verified USBLC6 flow-through conductors; it cannot stand in for PCB wires.
    const packageMetal = [
      ...["J_USB", "J_PROGRAM"].map(name => [17, "pin17_internal_1", "pin17_internal_2", "pin17_internal_3"].map(pin => `${name}.${pin}`)),
      ...["U_PROGRAM_ESD", "U_SENSOR_ESD"].flatMap(name => [[1, 6], [3, 4]].map(pins => pins.map(pin => `${name}.${pin}`))),
    ].map(pins => pins.sort().join()).sort()
    expect([...new Set(sourceConnectivity(circuit.getCircuitJson()).internalKeys.map(pins => pins.sort().join()))].sort())
      .toEqual(packageMetal)
  })

  test("actual copper joins every required pin with no shorts, floating copper or invalid pours", () => {
    expect(physical.missingPorts).toEqual([])
    expect(physical.disconnected).toEqual([])
    expect(physical.shorts).toEqual([])
    expect(physical.invalid).toEqual([])
    expect(physical.unanchoredCopper).toEqual([])
    expect(physical.clearanceViolations).toEqual([])
    const ground = circuit.db.source_net.list().find(net => net.name === "GND")!
    expect(circuit.db.pcb_copper_pour.list().some(pour => pour.source_net_id === ground.source_net_id)).toBe(true)
  })

  test("saved copper renders with finite coordinates, legal widths and physical through vias", () => {
    assertCopperAssociations(circuit.getCircuitJson(), routingData as RoutedCopperData)
    const board = circuit.db.pcb_board.list()[0]!
    const traces = circuit.db.pcb_trace.list()
    expect(board.min_trace_width).toBeGreaterThanOrEqual(0.2)
    expect(board.min_trace_to_pad_edge_clearance).toBeGreaterThanOrEqual(0.17)
    expect(board.min_pad_edge_to_pad_edge_clearance).toBeGreaterThanOrEqual(0.17)
    expect(board.min_via_edge_to_pad_edge_clearance).toBeGreaterThanOrEqual(0.17)
    expect(board.min_via_hole_diameter).toBeGreaterThanOrEqual(0.3)
    expect(board.min_via_pad_diameter).toBeGreaterThanOrEqual(0.6)
    expect(board.min_trace_to_hole_edge_clearance).toBeGreaterThanOrEqual(0.25)
    expect(board.min_via_hole_edge_to_via_hole_edge_clearance).toBeGreaterThanOrEqual(0.25)
    expect(board.min_board_edge_clearance).toBeGreaterThanOrEqual(0.3)
    expect(routingData.routes.length).toBeGreaterThan(0)
    expect(traces.length).toBeGreaterThanOrEqual(routingData.routes.length)
    const rendered = new Map<string, number>()
    for (const trace of traces) {
      const key = routeKey(trace.source_trace_id, trace.route)
      rendered.set(key, (rendered.get(key) ?? 0) + 1)
    }
    const missingRoutes: string[] = []
    for (const [index, route] of (routingData as RoutedCopperData).routes.entries()) {
      const key = routeKey(route.sourceTraceId, route.route), count = rendered.get(key) ?? 0
      if (!count) missingRoutes.push(`${index}: ${route.net}`)
      else rendered.set(key, count - 1)
    }
    expect(missingRoutes).toEqual([])
    for (const trace of traces) {
      expect(trace.route.filter(point => point.route_type === "wire").length).toBeGreaterThanOrEqual(2)
      for (const point of trace.route) {
        const coordinates = point.route_type === "through_pad" ? [point.start, point.end] : [point]
        expect(coordinates.every(p => Number.isFinite(p.x) && Number.isFinite(p.y))).toBe(true)
        if (point.route_type === "wire") expect(point.width).toBeGreaterThanOrEqual(board.min_trace_width! - 1e-9)
      }
    }
    const layers = ["top", ...Array.from({ length: board.num_layers - 2 }, (_, i) => `inner${i + 1}`), "bottom"].sort()
    expect(circuit.db.pcb_via.list().length).toBeGreaterThanOrEqual(routingData.vias.length)
    for (const via of circuit.db.pcb_via.list()) {
      expect(via.layers.map(String).sort()).toEqual(layers)
      expect(via.hole_diameter).toBeGreaterThanOrEqual(board.min_via_hole_diameter!)
      expect(via.outer_diameter).toBeGreaterThanOrEqual(board.min_via_pad_diameter!)
      expect(via.outer_diameter).toBeGreaterThan(via.hole_diameter)
    }
  })

  test("original crystal limits hold even for copper without source-trace associations", () => {
    for (const net of ["XIN", "XOUT_CRYSTAL"]) {
      const metric = physical.metricsForNet(net)
      expect(metric.traceLengthMm).toBeGreaterThan(0)
      expect(metric.traceLengthMm).toBeLessThanOrEqual(10 + 1e-6)
      expect(metric.viaCount).toBe(0)
    }
  })

  test("full-board native connectivity and geometry checks pass", async () => {
    const json = circuit.getCircuitJson()
    const strictJson = structuredClone(json)
    // The fabrication process may permit filled/capped via-in-pad globally;
    // validation always restores the strict rule before the exact-pair waiver.
    const board = strictJson.find(item => item.type === "pcb_board")!
    board.is_via_in_pad_allowed = false
    const native = [
      ...await checks.runAllRoutingChecks(structuredClone(strictJson)),
      ...await checks.runAllPlacementChecks(structuredClone(strictJson)),
    ]
    // Style/underspecified-pin warnings are distinct from physical failures.
    // Widths are checked directly above; the library width warning applies the
    // widest requested branch to every branch of a shared net, including GND.
    const thermal = inspectThermalVias(json)
    expect(thermal.missing).toEqual([])
    expect(thermal.invalid).toEqual([])
    expect(thermal.allowedErrorIds.size).toBe(13)
    expect(drillSmtClearanceViolations(json, thermal.allowedErrorIds)).toEqual([])
    // Keep original diagnostics intact. Native rechecks identify exact pad/via
    // pairs; core renumbers embedded duplicates, so match only their exact text.
    // Every fresh native error is still checked by ID (not rounded coordinates).
    const allowedEmbeddedMessages = new Set(native.filter(item => item.type === "pcb_placement_error" &&
      thermal.allowedErrorIds.has(item.pcb_placement_error_id)).map(item => item.message))
    const errors = [
      ...native.filter(item => item.type.endsWith("_error") &&
        !(item.type === "pcb_placement_error" && thermal.allowedErrorIds.has(item.pcb_placement_error_id))),
      ...json.filter(item => item.type.endsWith("_error") &&
        !(item.type === "pcb_placement_error" && allowedEmbeddedMessages.has(item.message))),
    ]
    expect(errors).toEqual([])
  }, 180_000)
})