tscircuit/ti
ti tscircuit library
- Version
- 1.0.198
- License
- unset
- Stars
- 0
system-block-ui/src/model/resolve-connection.ts
import { SUBCIRCUIT_CATALOG } from "./catalog";
import type {
BlockInstance,
ConnectionKind,
ConnectionKindInput,
ConnectionPortReference,
ConnectionProtocol,
LogicalConnection,
PortDefinition,
PortRole,
PortSignal,
ResolveConnectionRequest,
ResolvedConnection,
ResolvedTrace,
SubcircuitDefinition,
TryResolutionResult,
VoltageRange,
VoltageRequirement,
} from "./types";
import { ConnectionResolutionError } from "./types";
const compareStrings = (left: string, right: string): number =>
left < right ? -1 : left > right ? 1 : 0;
const COMPLEMENTARY_ROLES: ReadonlySet<string> = new Set([
"provider:consumer",
"consumer:provider",
"source:sink",
"sink:source",
"host:device",
"device:host",
"controller:peripheral",
"peripheral:controller",
"peer:peer",
]);
const normalizeKind = (kind: ConnectionKindInput): ConnectionKind => {
const normalized = kind.toLowerCase();
if (normalized === "power" || normalized === "data") return normalized;
throw new ConnectionResolutionError(
"INVALID_CONNECTION_KIND",
`Unsupported connection kind: ${kind}`,
);
};
const rolesAreComplementary = (a: PortRole, b: PortRole): boolean =>
COMPLEMENTARY_ROLES.has(`${a}:${b}`);
const toVoltageRange = (voltage: VoltageRequirement): VoltageRange =>
typeof voltage === "number"
? { min: voltage, max: voltage, nominal: voltage }
: voltage;
const voltageCompatibilityScore = (
kind: ConnectionKind,
a: PortDefinition,
b: PortDefinition,
): number | undefined => {
if (kind !== "power") return 0;
if (a.voltage === undefined || b.voltage === undefined) return undefined;
const provider = a.role === "provider" ? a : b;
const consumer = a.role === "consumer" ? a : b;
if (provider.role !== "provider" || consumer.role !== "consumer") {
return undefined;
}
const output = toVoltageRange(provider.voltage!);
const input = toVoltageRange(consumer.voltage!);
// The entire possible source range must be safe for the consumer. Merely
// overlapping would, for example, connect a 3.0-4.4 V battery rail to a
// 3.6 V-maximum sensor.
if (output.min < input.min || output.max > input.max) return undefined;
let score = 20;
if (
output.nominal !== undefined &&
output.nominal >= input.min &&
output.nominal <= input.max
) {
score += 8;
}
if (
input.nominal !== undefined &&
input.nominal >= output.min &&
input.nominal <= output.max
) {
score += 8;
}
if (
output.nominal !== undefined &&
input.nominal !== undefined &&
Math.abs(output.nominal - input.nominal) < 0.001
) {
score += 12;
}
return score;
};
const counterpartName = (
protocol: ConnectionProtocol | undefined,
signalName: string,
): string => {
if (protocol === "uart" || protocol === "hci-uart") {
const uartPairs: Readonly<Record<string, string>> = {
tx: "rx",
rx: "tx",
rts: "cts",
cts: "rts",
};
return uartPairs[signalName] ?? signalName;
}
// Power, I2C and I2S use exact logical signal names. This makes a missing or
// mislabeled clock/data rail fail instead of silently choosing a pin.
return signalName;
};
const directionsAreCompatible = (
a: PortSignal["direction"],
b: PortSignal["direction"],
): boolean => {
if (a === "passive" || b === "passive") return a === b;
if (a === "bidirectional" || b === "bidirectional") return true;
return (a === "output" && b === "input") || (a === "input" && b === "output");
};
interface MatchedSignal {
a: PortSignal;
b: PortSignal;
}
const matchSignals = (
a: PortDefinition,
b: PortDefinition,
protocol: ConnectionProtocol | undefined,
): readonly MatchedSignal[] | undefined => {
const matches: MatchedSignal[] = [];
const usedB = new Set<PortSignal>();
for (const aSignal of a.signals) {
const expected = counterpartName(protocol, aSignal.name);
const bSignal = b.signals.find(
(candidate) =>
!usedB.has(candidate) &&
candidate.name === expected &&
directionsAreCompatible(aSignal.direction, candidate.direction),
);
if (!bSignal) continue;
usedB.add(bSignal);
matches.push({ a: aSignal, b: bSignal });
}
const requiredA = new Set([
...(a.requiredSignals ?? []),
...a.signals.filter((item) => item.required).map((item) => item.name),
]);
const requiredB = new Set([
...(b.requiredSignals ?? []),
...b.signals.filter((item) => item.required).map((item) => item.name),
]);
const matchedA = new Set(matches.map((match) => match.a.name));
const matchedB = new Set(matches.map((match) => match.b.name));
if ([...requiredA].some((name) => !matchedA.has(name))) return undefined;
if ([...requiredB].some((name) => !matchedB.has(name))) return undefined;
return matches.length > 0 ? matches : undefined;
};
const isOutputDirection = (signal: PortSignal): boolean =>
signal.direction === "output";
const providerLike = (role: PortRole): boolean =>
role === "provider" ||
role === "source" ||
role === "host" ||
role === "controller";
const expandSignalTraces = ({
match,
aBlockId,
bBlockId,
aRole,
bRole,
}: {
match: MatchedSignal;
aBlockId: string;
bBlockId: string;
aRole: PortRole;
bRole: PortRole;
}): readonly ResolvedTrace[] => {
let fromSignal = match.a;
let toSignal = match.b;
let fromBlockId = aBlockId;
let toBlockId = bBlockId;
const bIsOutput = isOutputDirection(match.b);
if (
(bIsOutput && !isOutputDirection(match.a)) ||
(match.a.direction === match.b.direction &&
providerLike(bRole) &&
!providerLike(aRole))
) {
fromSignal = match.b;
toSignal = match.a;
fromBlockId = bBlockId;
toBlockId = aBlockId;
}
const fromSelectors = [...fromSignal.selectors];
const toSelectors = [...toSignal.selectors];
if (fromSelectors.length === 0 || toSelectors.length === 0) return [];
const signalName =
fromSignal.name === toSignal.name
? fromSignal.name
: `${fromSignal.name}-${toSignal.name}`;
const traces: ResolvedTrace[] = [];
// A star connects every selector into one electrical net without emitting a
// needless cartesian product. This covers supply fanout such as one 1.8 V
// output feeding AVDD, DVDD, IOVDD and an I2C pull-up rail.
for (const toSelector of toSelectors) {
traces.push({
signal: signalName,
fromBlockId,
toBlockId,
fromSelector: fromSelectors[0],
toSelector,
});
}
for (const fromSelector of fromSelectors.slice(1)) {
traces.push({
signal: signalName,
fromBlockId,
toBlockId,
fromSelector,
toSelector: toSelectors[0],
});
}
return traces;
};
interface Candidate {
fromPort: PortDefinition;
toPort: PortDefinition;
protocol?: ConnectionProtocol;
score: number;
traces: readonly ResolvedTrace[];
}
const candidateForPorts = (
request: ResolveConnectionRequest,
kind: ConnectionKind,
fromPort: PortDefinition,
toPort: PortDefinition,
): Candidate | undefined => {
if (fromPort.kind !== kind || toPort.kind !== kind) return undefined;
if (!rolesAreComplementary(fromPort.role, toPort.role)) return undefined;
if (
request.unavailablePorts?.some(
(port) =>
(port.blockId === request.from.block.id &&
port.portId === fromPort.id) ||
(port.blockId === request.to.block.id && port.portId === toPort.id),
)
) {
return undefined;
}
const protocol = fromPort.protocol ?? toPort.protocol;
if (
fromPort.protocol !== undefined &&
toPort.protocol !== undefined &&
fromPort.protocol !== toPort.protocol
) {
return undefined;
}
if (request.protocol !== undefined && protocol !== request.protocol) {
return undefined;
}
const voltageScore = voltageCompatibilityScore(kind, fromPort, toPort);
if (voltageScore === undefined) return undefined;
const matches = matchSignals(fromPort, toPort, protocol);
if (!matches) return undefined;
const traces = matches.flatMap((match) =>
expandSignalTraces({
match,
aBlockId: request.from.block.id,
bBlockId: request.to.block.id,
aRole: fromPort.role,
bRole: toPort.role,
}),
);
if (traces.length === 0) return undefined;
return {
fromPort,
toPort,
protocol,
score:
100 +
(protocol ? 40 : 0) +
voltageScore +
matches.length * 10 +
traces.length,
traces,
};
};
export const resolveConnection = (
request: ResolveConnectionRequest,
): ResolvedConnection => {
const kind = normalizeKind(request.kind);
if (request.from.block.id === request.to.block.id) {
throw new ConnectionResolutionError(
"SAME_BLOCK",
"A system block cannot be connected to itself.",
{ blockId: request.from.block.id },
);
}
const candidates = request.from.definition.ports.flatMap((fromPort) =>
request.to.definition.ports.flatMap((toPort) => {
const candidate = candidateForPorts(request, kind, fromPort, toPort);
return candidate ? [candidate] : [];
}),
);
candidates.sort(
(a, b) =>
b.score - a.score ||
compareStrings(a.fromPort.id, b.fromPort.id) ||
compareStrings(a.toPort.id, b.toPort.id),
);
if (candidates.length === 0) {
if (request.unavailablePorts?.length) {
try {
const occupiedMatch = resolveConnection({
...request,
unavailablePorts: undefined,
});
const occupiedEndpoints = [
{
blockId: occupiedMatch.fromBlockId,
portId: occupiedMatch.fromPortId,
},
{ blockId: occupiedMatch.toBlockId, portId: occupiedMatch.toPortId },
].filter((endpoint) =>
request.unavailablePorts?.some(
(port) =>
port.blockId === endpoint.blockId &&
port.portId === endpoint.portId,
),
);
if (occupiedEndpoints.length > 0) {
throw new ConnectionResolutionError(
"PORT_IN_USE",
`Every compatible ${kind} interface between ${request.from.definition.title} and ${request.to.definition.title} is already connected.`,
{ occupiedPorts: occupiedEndpoints },
);
}
} catch (error) {
if (
error instanceof ConnectionResolutionError &&
error.code === "PORT_IN_USE"
) {
throw error;
}
}
}
throw new ConnectionResolutionError(
"NO_COMPATIBLE_PORTS",
`No compatible ${kind} connection exists between ${request.from.definition.title} and ${request.to.definition.title}.`,
{
kind,
protocol: request.protocol,
fromDefinitionId: request.from.definition.id,
toDefinitionId: request.to.definition.id,
},
);
}
const best = candidates[0];
const tied = candidates.filter((candidate) => candidate.score === best.score);
if (tied.length > 1) {
throw new ConnectionResolutionError(
"AMBIGUOUS_CONNECTION",
`More than one equally good ${kind} connection exists between ${request.from.definition.title} and ${request.to.definition.title}.`,
{
candidates: tied.map((candidate) => ({
fromPortId: candidate.fromPort.id,
toPortId: candidate.toPort.id,
protocol: candidate.protocol,
score: candidate.score,
})),
},
);
}
return {
id:
request.connectionId ??
`${request.from.block.id}:${kind}:${request.to.block.id}`,
kind,
protocol: best.protocol,
fromBlockId: request.from.block.id,
toBlockId: request.to.block.id,
fromPortId: best.fromPort.id,
toPortId: best.toPort.id,
score: best.score,
traces: best.traces,
};
};
export const tryResolveConnection = (
request: ResolveConnectionRequest,
): TryResolutionResult => {
try {
return { ok: true, value: resolveConnection(request) };
} catch (error) {
if (error instanceof ConnectionResolutionError) {
return { ok: false, error };
}
throw error;
}
};
const resolvedPortEndpoints = (
connection: ResolvedConnection,
): readonly ConnectionPortReference[] => [
{ blockId: connection.fromBlockId, portId: connection.fromPortId },
{ blockId: connection.toBlockId, portId: connection.toPortId },
];
/** Return occupied single-use ports so the resolver can try another rail. */
export const getUnavailableConnectionPorts = (
existing: readonly ResolvedConnection[],
blocks: readonly BlockInstance[],
catalog: readonly SubcircuitDefinition[] = SUBCIRCUIT_CATALOG,
): readonly ConnectionPortReference[] => {
const blockById = new Map(blocks.map((block) => [block.id, block]));
const definitionById = new Map(catalog.map((item) => [item.id, item]));
const unavailable: ConnectionPortReference[] = [];
for (const connection of existing) {
for (const endpoint of resolvedPortEndpoints(connection)) {
const block = blockById.get(endpoint.blockId);
const definition = block
? definitionById.get(block.definitionId)
: undefined;
const port = definition?.ports.find(
(item) => item.id === endpoint.portId,
);
if (port && port.allowMultiple !== true) unavailable.push(endpoint);
}
}
return unavailable;
};
/**
* Prevent two generated edges from tying together a single-use interface.
* Fanout is permitted only when the curated port itself opts into it.
*/
export const assertResolvedConnectionPortsAvailable = (
candidate: ResolvedConnection,
existing: readonly ResolvedConnection[],
blocks: readonly BlockInstance[],
catalog: readonly SubcircuitDefinition[] = SUBCIRCUIT_CATALOG,
): void => {
const blockById = new Map(blocks.map((block) => [block.id, block]));
const definitionById = new Map(catalog.map((item) => [item.id, item]));
for (const endpoint of resolvedPortEndpoints(candidate)) {
const block = blockById.get(endpoint.blockId);
const definition = block
? definitionById.get(block.definitionId)
: undefined;
const port = definition?.ports.find((item) => item.id === endpoint.portId);
if (!block || !definition || !port) {
throw new ConnectionResolutionError(
block ? "UNKNOWN_SUBCIRCUIT" : "UNKNOWN_BLOCK",
`Cannot validate resolved port ${endpoint.blockId}.${endpoint.portId}.`,
{ ...endpoint },
);
}
if (port.allowMultiple === true) continue;
const conflict = existing.find((connection) =>
resolvedPortEndpoints(connection).some(
(used) =>
used.blockId === endpoint.blockId && used.portId === endpoint.portId,
),
);
if (!conflict) continue;
throw new ConnectionResolutionError(
"PORT_IN_USE",
`${definition.title} interface "${port.label}" is already connected by ${conflict.id}.`,
{
blockId: endpoint.blockId,
portId: endpoint.portId,
connectionId: candidate.id,
conflictingConnectionId: conflict.id,
},
);
}
};
export const resolveDesignConnections = (
blocks: readonly BlockInstance[],
connections: readonly LogicalConnection[],
catalog: readonly SubcircuitDefinition[] = SUBCIRCUIT_CATALOG,
): readonly ResolvedConnection[] => {
const blockById = new Map(blocks.map((block) => [block.id, block]));
const definitionById = new Map(catalog.map((item) => [item.id, item]));
const resolved: ResolvedConnection[] = [];
for (const connection of [...connections].sort((a, b) =>
compareStrings(a.id, b.id),
)) {
const fromBlock = blockById.get(connection.fromBlockId);
const toBlock = blockById.get(connection.toBlockId);
if (!fromBlock || !toBlock) {
throw new ConnectionResolutionError(
"UNKNOWN_BLOCK",
`Connection ${connection.id} references an unknown block.`,
{
fromBlockId: connection.fromBlockId,
toBlockId: connection.toBlockId,
},
);
}
const fromDefinition = definitionById.get(fromBlock.definitionId);
const toDefinition = definitionById.get(toBlock.definitionId);
if (!fromDefinition || !toDefinition) {
throw new ConnectionResolutionError(
"UNKNOWN_SUBCIRCUIT",
`Connection ${connection.id} references an unknown subcircuit definition.`,
{
fromDefinitionId: fromBlock.definitionId,
toDefinitionId: toBlock.definitionId,
},
);
}
const candidate = resolveConnection({
connectionId: connection.id,
kind: connection.kind,
protocol: connection.protocol,
from: { block: fromBlock, definition: fromDefinition },
to: { block: toBlock, definition: toDefinition },
unavailablePorts: getUnavailableConnectionPorts(
resolved,
blocks,
catalog,
),
});
assertResolvedConnectionPortsAvailable(
candidate,
resolved,
blocks,
catalog,
);
resolved.push(candidate);
}
return resolved;
};