Skip to content
5 changes: 5 additions & 0 deletions lib/components/primitive-components/DifferentialPair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type BaseComponentConfig,
PrimitiveComponent,
} from "../base-components/PrimitiveComponent"
import { DifferentialPair_doInitialSourceDesignRuleChecks } from "./DifferentialPair_doInitialSourceDesignRuleChecks"

/**
* Declares the routing constraints for a positive and negative trace pair.
Expand All @@ -16,4 +17,8 @@ export class DifferentialPair extends PrimitiveComponent<
zodProps: differentialPairProps,
}
}

doInitialSourceDesignRuleChecks(): void {
DifferentialPair_doInitialSourceDesignRuleChecks(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import type { SourcePort } from "circuit-json"
import type { DifferentialPair } from "./DifferentialPair"
import type { Port } from "./Port/Port"

type ConnectionPolarity = "positive" | "negative"
type SourceComponentId = NonNullable<SourcePort["source_component_id"]>

type ResolvedPointToPointConnection = {
sourcePorts: SourcePort[]
sourceNetName?: string
}

const resolvePointToPointConnection = (
differentialPair: DifferentialPair,
connectionSelector: string,
): ResolvedPointToPointConnection | undefined => {
const { db } = differentialPair.root!
const subcircuit = differentialPair.getSubcircuit()
const subcircuitSourceTraces = db.source_trace
.list()
.filter(
(sourceTrace) => sourceTrace.subcircuit_id === subcircuit.subcircuit_id,
)
const matchingSourceTraces = subcircuitSourceTraces.filter(
(sourceTrace) => sourceTrace.name === connectionSelector,
)
const connectivityMapKeys = new Set<string>()

if (matchingSourceTraces.length > 0) {
for (const sourceTrace of matchingSourceTraces) {
if (sourceTrace.subcircuit_connectivity_map_key) {
connectivityMapKeys.add(sourceTrace.subcircuit_connectivity_map_key)
}
}
} else {
const selectedPort = subcircuit.selectOne<Port>(connectionSelector, {
type: "port",
})
if (selectedPort?.source_port_id) {
const sourcePort = db.source_port.get(selectedPort.source_port_id)
if (sourcePort?.subcircuit_connectivity_map_key) {
connectivityMapKeys.add(sourcePort.subcircuit_connectivity_map_key)
}
}
}

if (connectivityMapKeys.size !== 1) return undefined
const connectivityMapKey = connectivityMapKeys.values().next().value
if (!connectivityMapKey) return undefined

const sourcePorts = db.source_port
.list()
.filter(
(sourcePort) =>
sourcePort.subcircuit_connectivity_map_key === connectivityMapKey,
)
const sourceNet = db.source_net
.list()
.find(
(sourceNet) =>
sourceNet.subcircuit_connectivity_map_key === connectivityMapKey,
)

return {
sourcePorts,
sourceNetName: sourceNet?.name,
}
}

const formatTerminalPinList = (terminalPinSelectors: string[]): string => {
if (terminalPinSelectors.length <= 1) return terminalPinSelectors.join("")
if (terminalPinSelectors.length === 2) {
return `${terminalPinSelectors[0]} and ${terminalPinSelectors[1]}`
}
return `${terminalPinSelectors.slice(0, -1).join(", ")}, and ${terminalPinSelectors.at(-1)}`
}

const getTerminalPinSelector = (
sourcePort: SourcePort,
sourceComponentsById: Map<SourceComponentId, { name: string }>,
): string => {
let sourceComponent: { name: string } | undefined
if (sourcePort.source_component_id) {
sourceComponent = sourceComponentsById.get(sourcePort.source_component_id)
}
if (!sourceComponent?.name) return sourcePort.source_port_id

const portName =
sourcePort.most_frequently_referenced_by_name ?? sourcePort.name
return `.${sourceComponent.name} > .${portName}`
}

const getPointToPointWarningMessage = ({
differentialPairName,
connectionPolarity,
connectionSelector,
sourceNetName,
terminalPinSelectors,
}: {
differentialPairName: string
connectionPolarity: ConnectionPolarity
connectionSelector: string
sourceNetName?: string
terminalPinSelectors: string[]
}): string => {
let resolvedConnectionName = `"${connectionSelector}"`
if (sourceNetName) {
resolvedConnectionName = `net.${sourceNetName}`
}
const terminalCount = terminalPinSelectors.length
let pinOrPins = "pins"
if (terminalCount === 1) {
pinOrPins = "pin"
}
const terminalList = formatTerminalPinList(terminalPinSelectors)
const suggestedSelector = terminalPinSelectors[0]
let correction = "Connect exactly two terminal pins"
if (terminalCount > 2) {
correction = "Remove the extra connection"
}
let selectorRecommendation = ""
if (suggestedSelector) {
selectorRecommendation = ` and prefer a pin selector such as ${connectionPolarity}Connection="${suggestedSelector}"`
}
let terminalListDescription = ""
if (terminalList) {
terminalListDescription = `: ${terminalList}`
}

return (
`Differential pair "${differentialPairName}" ${connectionPolarity}Connection resolves to ${resolvedConnectionName}, which is not point-to-point. ` +
`It connects to ${terminalCount} ${pinOrPins}${terminalListDescription}. ` +
`${correction}${selectorRecommendation}.`
)
}

export const DifferentialPair_doInitialSourceDesignRuleChecks = (
differentialPair: DifferentialPair,
): void => {
const { db } = differentialPair.root!

const sourceComponentsById = new Map<SourceComponentId, { name: string }>()
for (const sourceComponent of db.source_component.list()) {
sourceComponentsById.set(sourceComponent.source_component_id, {
name: sourceComponent.name,
})
}

for (const connectionPolarity of ["positive", "negative"] as const) {
let connectionSelector = differentialPair._parsedProps.negativeConnection
if (connectionPolarity === "positive") {
connectionSelector = differentialPair._parsedProps.positiveConnection
}

const resolvedConnection = resolvePointToPointConnection(
differentialPair,
connectionSelector,
)
if (!resolvedConnection) continue

const terminalSourcePorts = resolvedConnection.sourcePorts
if (terminalSourcePorts.length === 2) continue
let warningSourceComponentId = ""
const firstTerminalSourcePort = terminalSourcePorts[0]
if (firstTerminalSourcePort) {
if (!firstTerminalSourcePort.source_component_id) {
throw new Error(
`Differential pair "${differentialPair.name}" resolved terminal port "${firstTerminalSourcePort.source_port_id}" without a source_component_id`,
)
}
warningSourceComponentId = firstTerminalSourcePort.source_component_id
}

const terminalPinSelectors = terminalSourcePorts
.map((sourcePort) =>
getTerminalPinSelector(sourcePort, sourceComponentsById),
)
.sort((selectorA, selectorB) => selectorA.localeCompare(selectorB))
db.source_property_ignored_warning.insert({
source_component_id: warningSourceComponentId,
property_name: `${connectionPolarity}Connection`,
error_type: "source_property_ignored_warning",
message: getPointToPointWarningMessage({
differentialPairName: differentialPair.name,
connectionPolarity,
connectionSelector,
sourceNetName: resolvedConnection.sourceNetName,
terminalPinSelectors,
}),
subcircuit_id:
differentialPair.getSubcircuit().subcircuit_id ?? undefined,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from "bun:test"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("stores a warning when a differential pair port selector matches multiple traces", (): void => {
const { circuit } = getTestFixture()

circuit.add(
<board width="20mm" height="14mm" routingDisabled>
<differentialpair
name="USB"
positiveConnection=".R1 > .pin1"
negativeConnection="USB_N"
/>
<resistor name="R1" resistance="1k" footprint="0402" pcbX={-6} pcbY={2} />
<resistor
name="R2"
resistance="1k"
footprint="0402"
pcbX={-6}
pcbY={-3}
/>
<led name="LED1" footprint="0402" pcbX={6} pcbY={3} />
<capacitor name="C1" capacitance="1uF" footprint="0402" pcbX={6} />
<led name="LED2" footprint="0402" pcbX={6} pcbY={-3} />
<trace name="USB_P_PRIMARY" from=".R1 > .pin1" to=".LED1 > .anode" />
<trace name="USB_P_BRANCH" from=".R1 > .pin1" to=".C1 > .pin1" />
<trace name="USB_N" from=".R2 > .pin1" to=".LED2 > .anode" />
</board>,
)

circuit.render()

const pointToPointWarning =
circuit.db.source_property_ignored_warning.getWhere({
property_name: "positiveConnection",
})
expect(pointToPointWarning).toMatchObject({
type: "source_property_ignored_warning",
error_type: "source_property_ignored_warning",
property_name: "positiveConnection",
})
expect(pointToPointWarning?.message).toContain(
'positiveConnection resolves to ".R1 > .pin1", which is not point-to-point',
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from "bun:test"
import type { DifferentialPair } from "lib/components/primitive-components/DifferentialPair"
import { DifferentialPair_doInitialSourceDesignRuleChecks } from "lib/components/primitive-components/DifferentialPair_doInitialSourceDesignRuleChecks"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("throws when a differential-pair terminal has no source component ID", (): void => {
const { circuit } = getTestFixture()

circuit.add(
<board width="20mm" height="14mm" routingDisabled>
<differentialpair
name="USB_DATA"
positiveConnection="DP_SINGLE"
negativeConnection="DM"
/>
<chip name="J1" footprint="soic8" pcbX={-6} />
<chip name="U1" footprint="soic8" pcbX={6} />
<trace name="DP_SINGLE" from=".J1 > .pin1" to="net.DP" />
<trace name="DM" from=".J1 > .pin2" to=".U1 > .pin2" />
</board>,
)

circuit.render()

const j1 = circuit.db.source_component.getWhere({ name: "J1" })!
const j1Pin1 = circuit.db.source_port
.list()
.find(
(sourcePort) =>
sourcePort.source_component_id === j1.source_component_id &&
sourcePort.name === "pin1",
)!
circuit.db.source_port.update(j1Pin1.source_port_id, {
source_component_id: null as never,
})

const differentialPair = circuit.selectOne(
"differentialpair",
) as DifferentialPair
expect(() =>
DifferentialPair_doInitialSourceDesignRuleChecks(differentialPair),
).toThrow(
`Differential pair "USB_DATA" resolved terminal port "${j1Pin1.source_port_id}" without a source_component_id`,
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test } from "bun:test"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("does not warn for a two-terminal conductor split through a source net", (): void => {
const { circuit } = getTestFixture()

circuit.add(
<board width="20mm" height="14mm" routingDisabled>
<differentialpair
name="USB_DATA"
positiveConnection=".J1 > .pin1"
negativeConnection="DM"
/>
<chip name="J1" footprint="soic8" pcbX={-6} />
<chip name="U1" footprint="soic8" pcbX={6} />
<trace name="DP_FROM_J1" from=".J1 > .pin1" to="net.DP" />
<trace name="DP_TO_U1" from="net.DP" to=".U1 > .pin1" />
<trace name="DM" from=".J1 > .pin2" to=".U1 > .pin2" />
</board>,
)

circuit.render()

expect(
circuit.db.source_property_ignored_warning
.list()
.filter(
(warning) =>
warning.property_name === "positiveConnection" ||
warning.property_name === "negativeConnection",
),
).toEqual([])
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from "bun:test"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("stores a property warning for a branched differential pair", (): void => {
const { circuit } = getTestFixture()

circuit.add(
<board width="20mm" height="14mm" routingDisabled>
<differentialpair
name="USB_DATA"
positiveConnection="DP_FROM_J1"
negativeConnection="DM"
/>
<chip name="J1" footprint="soic8" pcbX={-7} />
<chip name="U1" footprint="soic8" pcbX={2} />
<testpoint name="TP1" footprintVariant="pad" pcbX={7} />
<trace name="DP_FROM_J1" from=".J1 > .pin1" to="net.DP" />
<trace from="net.DP" to=".U1 > .pin1" />
<trace from="net.DP" to=".TP1 > .pin1" />
<trace name="DM" from=".J1 > .pin2" to=".U1 > .pin2" />
</board>,
)

circuit.render()

const pointToPointWarnings = circuit.db.source_property_ignored_warning.list()
expect(pointToPointWarnings).toHaveLength(1)
expect(pointToPointWarnings[0]).toMatchObject({
type: "source_property_ignored_warning",
error_type: "source_property_ignored_warning",
property_name: "positiveConnection",
})
expect(pointToPointWarnings[0]?.source_component_id).toBeDefined()
expect(pointToPointWarnings[0]?.message).toBe(
'Differential pair "USB_DATA" positiveConnection resolves to net.DP, which is not point-to-point. It connects to 3 pins: .J1 > .pin1, .TP1 > .pin1, and .U1 > .pin1. Remove the extra connection and prefer a pin selector such as positiveConnection=".J1 > .pin1".',
)
})
Loading
Loading