diff --git a/lib/HyperGraphSolver.ts b/lib/HyperGraphSolver.ts index 3d5f489..0d05db2 100644 --- a/lib/HyperGraphSolver.ts +++ b/lib/HyperGraphSolver.ts @@ -1,23 +1,24 @@ import { BaseSolver } from "@tscircuit/solver-utils" -import { convertSerializedHyperGraphToHyperGraph } from "./convertSerializedHyperGraphToHyperGraph" -import { convertHyperGraphToSerializedHyperGraph } from "./convertHyperGraphToSerializedHyperGraph" import { convertConnectionsToSerializedConnections } from "./convertConnectionsToSerializedConnections" +import { convertHyperGraphToSerializedHyperGraph } from "./convertHyperGraphToSerializedHyperGraph" +import { convertSerializedConnectionsToConnections } from "./convertSerializedConnectionsToConnections" +import { convertSerializedHyperGraphToHyperGraph } from "./convertSerializedHyperGraphToHyperGraph" +import { PriorityQueue } from "./PriorityQueue" import type { Candidate, Connection, - RegionPort, - PortId, + GScore, HyperGraph, - SerializedConnection, - SerializedHyperGraph, + NetworkId, + PortId, Region, RegionId, - SolvedRoute, + RegionPort, RegionPortAssignment, - GScore, + SerializedConnection, + SerializedHyperGraph, + SolvedRoute, } from "./types" -import { convertSerializedConnectionsToConnections } from "./convertSerializedConnectionsToConnections" -import { PriorityQueue } from "./PriorityQueue" export class HyperGraphSolver< RegionType extends Region = Region, @@ -61,6 +62,7 @@ export class HyperGraphSolver< ) { super() this.graph = convertSerializedHyperGraphToHyperGraph(input.inputGraph) + this.validateRegionCapacities() for (const region of this.graph.regions) { region.assignments = [] } @@ -183,12 +185,81 @@ export class HyperGraphSolver< computeRoutesToRip(newlySolvedRoute: SolvedRoute): Set { const crossingRoutesToRip = this.computeCrossingRoutes(newlySolvedRoute) const portReuseRoutesToRip = this.computePortOverlapRoutes(newlySolvedRoute) + const regionCapacityRoutesToRip = + this.computeRegionCapacityOverlapRoutes(newlySolvedRoute) return new Set([ ...crossingRoutesToRip, ...portReuseRoutesToRip, + ...regionCapacityRoutesToRip, ]) } + private validateRegionCapacities() { + for (const region of this.graph.regions) { + if (region.capacity === undefined) continue + const { capacity } = region + if ( + capacity !== Infinity && + (!Number.isFinite(capacity) || + capacity < 1 || + !Number.isInteger(capacity)) + ) { + throw new Error( + `Region ${region.regionId} has invalid capacity ${capacity}. Capacity must be a positive integer or Infinity.`, + ) + } + } + } + + protected getRegionCapacity(region: RegionType): number { + return region.capacity ?? Infinity + } + + protected getRegionAssignmentsByNetwork( + region: RegionType, + ): Map { + const assignmentsByNetwork = new Map() + const assignments = region.assignments ?? [] + for (const assignment of assignments) { + const networkId = assignment.connection.mutuallyConnectedNetworkId + assignmentsByNetwork.set(networkId, [ + ...(assignmentsByNetwork.get(networkId) ?? []), + assignment, + ]) + } + return assignmentsByNetwork + } + + protected getRegionCapacityOverflowIfUsed(region: RegionType): number { + const capacity = this.getRegionCapacity(region) + if (!Number.isFinite(capacity)) return 0 + + const assignmentsByNetwork = this.getRegionAssignmentsByNetwork(region) + const currentNetworkId = this.currentConnection!.mutuallyConnectedNetworkId + const nextDistinctNetworkCount = + assignmentsByNetwork.size + + (assignmentsByNetwork.has(currentNetworkId) ? 0 : 1) + + return Math.max(0, nextDistinctNetworkCount - capacity) + } + + getRipsRequiredForRegionUsage(region: RegionType): RegionPortAssignment[] { + const overflow = this.getRegionCapacityOverflowIfUsed(region) + if (overflow <= 0) return [] + + const assignmentsByNetwork = this.getRegionAssignmentsByNetwork(region) + const currentNetworkId = this.currentConnection!.mutuallyConnectedNetworkId + const candidateNetworksToRip = [...assignmentsByNetwork.entries()] + .filter(([networkId]) => networkId !== currentNetworkId) + .sort((a, b) => a[1].length - b[1].length) + + if (candidateNetworksToRip.length === 0) return [] + + return candidateNetworksToRip + .slice(0, overflow) + .flatMap(([, assignments]) => assignments) + } + /** * Returns solved routes that overlap ports with the newly solved route. * Use this in computeRoutesToRip overrides to include port reuse rips. @@ -223,16 +294,36 @@ export class HyperGraphSolver< return crossingRoutesToRip } + computeRegionCapacityOverlapRoutes( + newlySolvedRoute: SolvedRoute, + ): Set { + const regionCapacityRoutesToRip: Set = new Set() + for (const candidate of newlySolvedRoute.path) { + if (!candidate.lastRegion) continue + const ripsRequired = this.getRipsRequiredForRegionUsage( + candidate.lastRegion as RegionType, + ) + for (const assignment of ripsRequired) { + regionCapacityRoutesToRip.add(assignment.solvedRoute) + } + } + return regionCapacityRoutesToRip + } + getNextCandidates(currentCandidate: CandidateType): CandidateType[] { const currentRegion = currentCandidate.nextRegion! const currentPort = currentCandidate.port + const regionUsageRipRequired = + this.getRipsRequiredForRegionUsage(currentRegion as RegionType).length > 0 const nextCandidatesByRegion: Record = {} for (const port of currentRegion.ports) { if (port === currentCandidate.port) continue const ripRequired = - port.assignment && - port.assignment.connection.mutuallyConnectedNetworkId !== - this.currentConnection!.mutuallyConnectedNetworkId + !!( + port.assignment && + port.assignment.connection.mutuallyConnectedNetworkId !== + this.currentConnection!.mutuallyConnectedNetworkId + ) || regionUsageRipRequired const newCandidate: Partial = { port, hops: currentCandidate.hops + 1, diff --git a/lib/JumperGraphSolver/JumperGraphSolver.ts b/lib/JumperGraphSolver/JumperGraphSolver.ts index a358a6d..52bdebd 100644 --- a/lib/JumperGraphSolver/JumperGraphSolver.ts +++ b/lib/JumperGraphSolver/JumperGraphSolver.ts @@ -1,3 +1,4 @@ +import { distance } from "@tscircuit/math-utils" import type { GraphicsObject } from "graphics-debug" import { HyperGraphSolver } from "../HyperGraphSolver" import type { @@ -8,12 +9,11 @@ import type { SerializedHyperGraph, SolvedRoute, } from "../types" -import type { JPort, JRegion } from "./jumper-types" -import { visualizeJumperGraphSolver } from "./visualizeJumperGraphSolver" -import { distance } from "@tscircuit/math-utils" -import { computeDifferentNetCrossings } from "./computeDifferentNetCrossings" import { computeCrossingAssignments } from "./computeCrossingAssignments" +import { computeDifferentNetCrossings } from "./computeDifferentNetCrossings" import { countInputConnectionCrossings } from "./countInputConnectionCrossings" +import type { JPort, JRegion } from "./jumper-types" +import { visualizeJumperGraphSolver } from "./visualizeJumperGraphSolver" export const JUMPER_GRAPH_SOLVER_DEFAULTS = { portUsagePenalty: 0.034685181009478865, @@ -130,8 +130,16 @@ export class JumperGraphSolver extends HyperGraphSolver { port1: JPort, port2: JPort, ): number { + const capacityOverflow = this.getRegionCapacityOverflowIfUsed(region) + const capacityPenalty = + capacityOverflow * this.crossingPenalty + + capacityOverflow * this.crossingPenaltySq const crossings = computeDifferentNetCrossings(region, port1, port2) - return crossings * this.crossingPenalty + crossings * this.crossingPenaltySq + return ( + capacityPenalty + + crossings * this.crossingPenalty + + crossings * this.crossingPenaltySq + ) } override getRipsRequiredForPortUsage( diff --git a/lib/JumperGraphSolver/jumper-graph-generator/generateJumperGrid.ts b/lib/JumperGraphSolver/jumper-graph-generator/generateJumperGrid.ts index 4f0d67a..7cd21e9 100644 --- a/lib/JumperGraphSolver/jumper-graph-generator/generateJumperGrid.ts +++ b/lib/JumperGraphSolver/jumper-graph-generator/generateJumperGrid.ts @@ -1,9 +1,9 @@ -import type { JPort, JRegion, JumperGraph } from "../jumper-types" +import { compose, rotate, translate } from "transformation-matrix" +import { applyTransformToGraph } from "../geometry/applyTransformToGraph" import { computeBoundsCenter } from "../geometry/getBoundsCenter" -import { dims0603 } from "./generateSingleJumperRegions" +import type { JPort, JRegion, JumperGraph } from "../jumper-types" import { calculateGraphBounds } from "./calculateGraphBounds" -import { applyTransformToGraph } from "../geometry/applyTransformToGraph" -import { compose, translate, rotate } from "transformation-matrix" +import { dims0603 } from "./generateSingleJumperRegions" export const generateJumperGrid = ({ cols, @@ -68,6 +68,7 @@ export const generateJumperGrid = ({ ): JRegion => ({ regionId: id, ports: [], + capacity: isPad || isThroughJumper ? 1 : undefined, d: { bounds, center: computeBoundsCenter(bounds), isPad, isThroughJumper }, }) diff --git a/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX2Grid.ts b/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX2Grid.ts index 9f60fa5..0ae7f87 100644 --- a/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX2Grid.ts +++ b/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX2Grid.ts @@ -1,5 +1,5 @@ -import type { JPort, JRegion } from "../jumper-types" import { computeBoundsCenter } from "../geometry/getBoundsCenter" +import type { JPort, JRegion } from "../jumper-types" import { dims0606x2 } from "./generateSingleJumperX2Regions" export const generateJumperX2Grid = ({ @@ -79,6 +79,7 @@ export const generateJumperX2Grid = ({ ): JRegion => ({ regionId: id, ports: [], + capacity: isPad || isThroughJumper ? 1 : undefined, d: { bounds, center: computeBoundsCenter(bounds), isPad, isThroughJumper }, }) diff --git a/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid.ts b/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid.ts index d94d80c..17ee668 100644 --- a/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid.ts +++ b/lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid.ts @@ -1,9 +1,9 @@ -import type { JPort, JRegion, JumperGraph } from "../jumper-types" +import { compose, rotate, translate } from "transformation-matrix" +import { applyTransformToGraph } from "../geometry/applyTransformToGraph" import { computeBoundsCenter } from "../geometry/getBoundsCenter" -import { dims1206x4 } from "./generateSingleJumperX4Regions" +import type { JPort, JRegion, JumperGraph } from "../jumper-types" import { calculateGraphBounds } from "./calculateGraphBounds" -import { applyTransformToGraph } from "../geometry/applyTransformToGraph" -import { compose, translate, rotate } from "transformation-matrix" +import { dims1206x4 } from "./generateSingleJumperX4Regions" export const generateJumperX4Grid = ({ cols, @@ -136,6 +136,7 @@ export const generateJumperX4Grid = ({ ): JRegion => ({ regionId: id, ports: [], + capacity: isPad || isThroughJumper ? 1 : undefined, d: { bounds, center: computeBoundsCenter(bounds), isPad, isThroughJumper }, }) diff --git a/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperRegions.ts b/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperRegions.ts index 7fc3d45..b2c58cd 100644 --- a/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperRegions.ts +++ b/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperRegions.ts @@ -1,5 +1,5 @@ -import type { JPort, JRegion } from "../jumper-types" import { computeBoundsCenter } from "../geometry/getBoundsCenter" +import type { JPort, JRegion } from "../jumper-types" export const dims0603 = { padToPad: 1.65, @@ -79,6 +79,7 @@ export const generateSingleJumperRegions = ({ ): JRegion => ({ regionId: `${idPrefix}:${id}`, ports: [], + capacity: isPad || isThroughJumper ? 1 : undefined, d: { bounds, center: computeBoundsCenter(bounds), isPad, isThroughJumper }, }) diff --git a/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX2Regions.ts b/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX2Regions.ts index b68e5c7..a9b024b 100644 --- a/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX2Regions.ts +++ b/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX2Regions.ts @@ -1,5 +1,5 @@ -import type { JPort, JRegion } from "../jumper-types" import { computeBoundsCenter } from "../geometry/getBoundsCenter" +import type { JPort, JRegion } from "../jumper-types" // 0606x2 resistor chip array dimensions // This is a 2-element array with 4 pads total (2 per resistor) @@ -107,6 +107,7 @@ export const generateSingleJumperX2Regions = ({ ): JRegion => ({ regionId: `${idPrefix}:${id}`, ports: [], + capacity: isPad || isThroughJumper ? 1 : undefined, d: { bounds, center: computeBoundsCenter(bounds), isPad, isThroughJumper }, }) diff --git a/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX4Regions.ts b/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX4Regions.ts index 49d9ea7..4bc6532 100644 --- a/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX4Regions.ts +++ b/lib/JumperGraphSolver/jumper-graph-generator/generateSingleJumperX4Regions.ts @@ -1,5 +1,5 @@ -import type { JPort, JRegion } from "../jumper-types" import { computeBoundsCenter } from "../geometry/getBoundsCenter" +import type { JPort, JRegion } from "../jumper-types" // EXB-38VR000V (Panasonic) 4x 0Ω isolated jumpers dimensions // This is a 4-element array with 8 pads total (4 per side) @@ -156,6 +156,7 @@ export const generateSingleJumperX4Regions = ({ ): JRegion => ({ regionId: `${idPrefix}:${id}`, ports: [], + capacity: isPad || isThroughJumper ? 1 : undefined, d: { bounds, center: computeBoundsCenter(bounds), isPad, isThroughJumper }, }) diff --git a/lib/convertHyperGraphToSerializedHyperGraph.ts b/lib/convertHyperGraphToSerializedHyperGraph.ts index b011028..4da2bbe 100644 --- a/lib/convertHyperGraphToSerializedHyperGraph.ts +++ b/lib/convertHyperGraphToSerializedHyperGraph.ts @@ -1,8 +1,8 @@ import type { HyperGraph, - SerializedHyperGraph, SerializedGraphPort, SerializedGraphRegion, + SerializedHyperGraph, } from "./types" export const convertHyperGraphToSerializedHyperGraph = ( @@ -20,6 +20,7 @@ export const convertHyperGraphToSerializedHyperGraph = ( regionId: region.regionId, pointIds: region.ports.map((port) => port.portId), d: region.d, + capacity: region.capacity, }), ) diff --git a/lib/types.ts b/lib/types.ts index 02234dc..32b9eea 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -28,6 +28,11 @@ export type Region = { regionId: RegionId ports: RegionPort[] d: any + /** + * Maximum number of distinct networks allowed to occupy this region. + * If undefined, the region has no capacity limit. + */ + capacity?: number assignments?: RegionPortAssignment[] } @@ -82,6 +87,7 @@ export type SerializedGraphRegion = { regionId: RegionId pointIds: PortId[] d: any + capacity?: number assignments?: SerializedRegionPortAssignment[] } export type SerializedRegionPortAssignment = { diff --git a/tests/jumper-graph-solver/jumper-graph-solver06.test.ts b/tests/jumper-graph-solver/jumper-graph-solver06.test.ts new file mode 100644 index 0000000..47dcf13 --- /dev/null +++ b/tests/jumper-graph-solver/jumper-graph-solver06.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from "bun:test" +import { convertHyperGraphToSerializedHyperGraph } from "lib/convertHyperGraphToSerializedHyperGraph" +import { convertSerializedHyperGraphToHyperGraph } from "lib/convertSerializedHyperGraphToHyperGraph" +import { JumperGraphSolver } from "lib/JumperGraphSolver/JumperGraphSolver" +import { createGraphWithConnectionsFromBaseGraph } from "lib/JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph" +import { generateJumperX4Grid } from "lib/JumperGraphSolver/jumper-graph-generator/generateJumperX4Grid" + +const createSampleGraph = () => { + const baseGraph = generateJumperX4Grid({ + cols: 1, + rows: 1, + marginX: 0.5, + marginY: 0.5, + outerPaddingX: 0.8, + outerPaddingY: 0.8, + regionsBetweenPads: true, + }) + + return createGraphWithConnectionsFromBaseGraph(baseGraph, [ + { + start: { x: -2.55, y: 1.0 }, + end: { x: 2.55, y: -1.0 }, + connectionId: "A", + }, + { + start: { x: 0, y: 2.955 }, + end: { x: -2.55, y: -1.0 }, + connectionId: "B", + }, + { + start: { x: 0, y: -2.955 }, + end: { x: 2.55, y: 1.0 }, + connectionId: "C", + }, + ]) +} + +test("jumper-graph-solver06: enforce region capacity on pads and through-jumpers", () => { + const graphWithConnections = createSampleGraph() + + const solver = new JumperGraphSolver({ + inputGraph: { + regions: graphWithConnections.regions, + ports: graphWithConnections.ports, + }, + inputConnections: graphWithConnections.connections, + }) + + solver.solve() + expect(solver.solved).toBe(true) + + for (const region of solver.graph.regions) { + if (region.capacity === undefined) continue + + const usedNetworkIds = new Set( + (region.assignments ?? []).map( + (a) => a.connection.mutuallyConnectedNetworkId, + ), + ) + expect(usedNetworkIds.size).toBeLessThanOrEqual(region.capacity) + } +}) + +test("jumper-graph-solver06: rejects invalid region capacities", () => { + const graphWithConnections = createSampleGraph() + graphWithConnections.regions[0].capacity = 0 + + expect( + () => + new JumperGraphSolver({ + inputGraph: { + regions: graphWithConnections.regions, + ports: graphWithConnections.ports, + }, + inputConnections: graphWithConnections.connections, + }), + ).toThrow("invalid capacity") +}) + +test("jumper-graph-solver06: preserves region capacity through serialization", () => { + const graphWithConnections = createSampleGraph() + + const serialized = convertHyperGraphToSerializedHyperGraph({ + regions: graphWithConnections.regions, + ports: graphWithConnections.ports, + }) + + const hydrated = convertSerializedHyperGraphToHyperGraph(serialized) + + const originalCapacityByRegion = new Map( + graphWithConnections.regions.map((region) => [ + region.regionId, + region.capacity, + ]), + ) + + for (const region of hydrated.regions) { + expect(region.capacity).toBe(originalCapacityByRegion.get(region.regionId)) + } +})