diff --git a/lib/DuplicateCongestedPortSolver.ts b/lib/DuplicateCongestedPortSolver.ts index 0e00d4d..43a5073 100644 --- a/lib/DuplicateCongestedPortSolver.ts +++ b/lib/DuplicateCongestedPortSolver.ts @@ -11,11 +11,14 @@ import type { PortId, RouteId } from "./types" type SerializedPort = SerializedHyperGraph["ports"][number] type SerializedRegion = SerializedHyperGraph["regions"][number] +type SerializedRegionId = SerializedRegion["regionId"] +type LayerBoundaryKey = string export const DUPLICATE_PORT_PROXIMITY = 0.05 export interface DuplicateCongestedPortSolverOptions { duplicatePortProximity?: number + minPortSpacing?: number routeSolveOptions?: TinyHyperGraphSolverOptions /** Ignore serialized port penalties while estimating shared-port use. */ useSerializedPortPenalties?: boolean @@ -166,6 +169,34 @@ const getRegionCenter = (region: SerializedRegion | undefined): Point => { } } +const getSharedBoundaryLength = ( + source: SerializedPort, + regions: Map, +): number => { + const first = getRegionBounds(regions.get(source.region1Id)) + const second = getRegionBounds(regions.get(source.region2Id)) + const tolerance = 0.001 + if ( + Math.abs(first.maxX - second.minX) <= tolerance || + Math.abs(second.maxX - first.minX) <= tolerance + ) { + return Math.max( + 0, + Math.min(first.maxY, second.maxY) - Math.max(first.minY, second.minY), + ) + } + if ( + Math.abs(first.maxY - second.minY) <= tolerance || + Math.abs(second.maxY - first.minY) <= tolerance + ) { + return Math.max( + 0, + Math.min(first.maxX, second.maxX) - Math.max(first.minX, second.minX), + ) + } + return 0 +} + const findNearestPortOnSameBoundary = ( sourcePort: SerializedPort, boundaryPorts: SerializedPort[], @@ -187,10 +218,15 @@ const findNearestPortOnSameBoundary = ( return nearestPort } -const getFallbackBoundaryDirection = ( - sourcePort: SerializedPort, - regionById: Map, -): Point => { +const getFallbackBoundaryDirection = ({ + sourcePort, + regionById, + minPortSpacing, +}: { + sourcePort: SerializedPort + regionById: Map + minPortSpacing?: number +}): Point => { const region1Center = getRegionCenter(regionById.get(sourcePort.region1Id)) const region2Center = getRegionCenter(regionById.get(sourcePort.region2Id)) const perpendicular = normalize({ @@ -198,14 +234,34 @@ const getFallbackBoundaryDirection = ( y: region2Center.x - region1Center.x, }) - return perpendicular ?? { x: 1, y: 0 } + if (!perpendicular) return { x: 1, y: 0 } + const fixedNetIds = sourcePort.d?._preloadedFixedNetIds + if ( + minPortSpacing === undefined && + (!Array.isArray(fixedNetIds) || fixedNetIds.length === 0) + ) { + return perpendicular + } + if ( + perpendicular.y < -EPSILON || + (Math.abs(perpendicular.y) <= EPSILON && perpendicular.x < 0) + ) { + return { x: -perpendicular.x, y: -perpendicular.y } + } + return perpendicular } -const getDuplicateDirection = ( - sourcePort: SerializedPort, - nearestBoundaryPort: SerializedPort | undefined, - regionById: Map, -): Point => { +const getDuplicateDirection = ({ + sourcePort, + nearestBoundaryPort, + regionById, + minPortSpacing, +}: { + sourcePort: SerializedPort + nearestBoundaryPort: SerializedPort | undefined + regionById: Map + minPortSpacing?: number +}): Point => { const sourcePoint = getPortPoint(sourcePort) if (nearestBoundaryPort) { @@ -217,7 +273,11 @@ const getDuplicateDirection = ( if (awayFromNearest) return awayFromNearest } - return getFallbackBoundaryDirection(sourcePort, regionById) + return getFallbackBoundaryDirection({ + sourcePort, + regionById, + minPortSpacing, + }) } const createDuplicatePortId = ( @@ -412,6 +472,7 @@ export class DuplicateCongestedPortSolver extends BaseSolver { ) const usedPortIds = new Set(ports.map((port) => port.portId)) const duplicatedPorts: DuplicatedPortSummary[] = [] + const portCountByLayerBoundary = new Map() for (const [sourcePortId, useCount] of [...portUseCounts.entries()].sort( ([leftPortId], [rightPortId]) => leftPortId.localeCompare(rightPortId), @@ -421,20 +482,45 @@ export class DuplicateCongestedPortSolver extends BaseSolver { const sourcePort = sourcePortById.get(sourcePortId) if (!sourcePort) continue - const duplicateCount = useCount - 1 + let duplicateCount = useCount - 1 const boundaryPorts = portsByBoundary.get(getBoundaryKey(sourcePort)) if (!boundaryPorts) { throw new Error(`No boundary ports found for "${sourcePortId}"`) } + if (this.options.minPortSpacing !== undefined) { + const spacing = this.options.minPortSpacing + if (!Number.isFinite(spacing) || spacing <= 0) { + throw new Error("minPortSpacing must be finite and greater than zero") + } + const z = getNumber(sourcePort.d?.z) + const layerBoundaryKey: LayerBoundaryKey = `${getBoundaryKey(sourcePort)}:${z}` + const existing = + portCountByLayerBoundary.get(layerBoundaryKey) ?? + boundaryPorts.filter((port) => getNumber(port.d?.z) === z).length + const capacity = Math.max( + 1, + Math.floor(getSharedBoundaryLength(sourcePort, regionById) / spacing), + ) + duplicateCount = Math.min( + duplicateCount, + Math.max(0, capacity - existing), + ) + if (duplicateCount === 0) continue + portCountByLayerBoundary.set( + layerBoundaryKey, + existing + duplicateCount, + ) + } const nearestBoundaryPort = findNearestPortOnSameBoundary( sourcePort, boundaryPorts, ) - const duplicateDirection = getDuplicateDirection( + const duplicateDirection = getDuplicateDirection({ sourcePort, nearestBoundaryPort, regionById, - ) + minPortSpacing: this.options.minPortSpacing, + }) const sourcePoint = getPortPoint(sourcePort) const duplicatePortIds: string[] = [] diff --git a/lib/core.ts b/lib/core.ts index 8bd0e56..e6d1fb0 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -256,6 +256,10 @@ export interface TinyHyperGraphWorkingState { export interface TinyHyperGraphSolverOptions { minViaPadDiameter?: number + /** Constrain layer changes to physical regions that can fit the via pad. */ + ENFORCE_VIA_SPACE?: boolean + /** Minimum distance between crossings assigned to different nets on one layer. */ + minPortSpacing?: number DISTANCE_TO_COST?: number RIP_THRESHOLD_START?: number RIP_THRESHOLD_END?: number @@ -320,6 +324,8 @@ export interface TinyHyperGraphSolverOptions { export interface TinyHyperGraphSolverOptionTarget { minViaPadDiameter: number + ENFORCE_VIA_SPACE?: boolean + minPortSpacing?: number DISTANCE_TO_COST: number RIP_THRESHOLD_START: number RIP_THRESHOLD_END: number @@ -358,6 +364,12 @@ export const applyTinyHyperGraphSolverOptions = ( return } + if (options.ENFORCE_VIA_SPACE !== undefined) { + solver.ENFORCE_VIA_SPACE = options.ENFORCE_VIA_SPACE + } + if (options.minPortSpacing !== undefined) { + solver.minPortSpacing = options.minPortSpacing + } if (options.minViaPadDiameter !== undefined) { solver.minViaPadDiameter = options.minViaPadDiameter } @@ -463,6 +475,8 @@ export const getTinyHyperGraphSolverOptions = ( solver: TinyHyperGraphSolverOptionTarget, ): TinyHyperGraphSolverOptions => ({ minViaPadDiameter: solver.minViaPadDiameter, + ENFORCE_VIA_SPACE: solver.ENFORCE_VIA_SPACE, + minPortSpacing: solver.minPortSpacing, DISTANCE_TO_COST: solver.DISTANCE_TO_COST, RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, @@ -555,6 +569,8 @@ export class TinyHyperGraphSolver extends BaseSolver { protected costAwareFinalReripBudget = 0 DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) + ENFORCE_VIA_SPACE = false + minPortSpacing = 0 minViaPadDiameter = DEFAULT_MIN_VIA_PAD_DIAMETER RIP_THRESHOLD_START = 0.05 @@ -885,12 +901,25 @@ export class TinyHyperGraphSolver extends BaseSolver { } for (const neighborPortId of neighbors) { + if ( + topology.portZ[currentCandidate.portId] !== + topology.portZ[neighborPortId] && + !this.canTransitionLayersInRegion(currentCandidate.nextRegionId) + ) { + continue + } if (neighborPortId === state.goalPortId) { const assignedNetId = state.portAssignment[neighborPortId] if (this.isPortReservedForDifferentNet(neighborPortId)) continue if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { continue } + if ( + this.ENFORCE_VIA_SPACE && + !Number.isFinite(this.computeG(currentCandidate, neighborPortId)) + ) { + continue + } this.onPathFound(currentCandidate) return } @@ -1038,12 +1067,25 @@ export class TinyHyperGraphSolver extends BaseSolver { const previousIndex = previousNeighborIndex previousNeighborIndex = currentNeighborIndex const neighborPortId = neighbors[currentNeighborIndex]! + if ( + topology.portZ[currentCandidate.portId] !== + topology.portZ[neighborPortId] && + !this.canTransitionLayersInRegion(currentCandidate.nextRegionId) + ) { + continue + } if (neighborPortId === state.goalPortId) { const assignedNetId = state.portAssignment[neighborPortId] if (this.isPortReservedForDifferentNet(neighborPortId)) continue if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { continue } + if ( + this.ENFORCE_VIA_SPACE && + !Number.isFinite(this.computeG(currentCandidate, neighborPortId)) + ) { + continue + } this.onPathFound(currentCandidate) return } @@ -1254,10 +1296,42 @@ export class TinyHyperGraphSolver extends BaseSolver { isPortReservedForDifferentNet(portId: PortId): boolean { const reservedNetId = this.problemSetup.portEndpointReservationNetId[portId] ?? -1 - return ( + if ( reservedNetId === -2 || (reservedNetId !== -1 && reservedNetId !== this.state.currentRouteNetId) - ) + ) { + return true + } + if (this.minPortSpacing <= 0) return false + const { topology, state, problemSetup, problem } = this + const netId = state.currentRouteNetId + const minDistanceSquared = this.minPortSpacing ** 2 - 1e-9 + for (const regionId of topology.incidentPortRegion[portId]) { + if (problem.regionNetId[regionId] !== -1) continue + for (const otherPortId of topology.regionIncidentPorts[regionId]) { + if ( + otherPortId === portId || + topology.portZ[portId] !== topology.portZ[otherPortId] + ) { + continue + } + const assignedNetId = state.portAssignment[otherPortId] + const otherReservedNetId = + problemSetup.portEndpointReservationNetId[otherPortId] + // CAD terminal positions are fixed rather than selectable crossings. + if (reservedNetId >= 0 && otherReservedNetId >= 0) continue + if ( + (assignedNetId === -1 || assignedNetId === netId) && + (otherReservedNetId === -1 || otherReservedNetId === netId) + ) { + continue + } + const dx = topology.portX[otherPortId] - topology.portX[portId] + const dy = topology.portY[otherPortId] - topology.portY[portId] + if (dx * dx + dy * dy < minDistanceSquared) return true + } + } + return false } isRegionReservedForDifferentNet(regionId: RegionId): boolean { @@ -1267,6 +1341,19 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } + canTransitionLayersInRegion(regionId: RegionId): boolean { + return ( + !this.ENFORCE_VIA_SPACE || + (this.topology.regionAvailableZMask?.[regionId] ?? 0) === 0 || + this.problem.regionNetId[regionId] !== -1 || + Math.min( + this.topology.regionWidth[regionId], + this.topology.regionHeight[regionId], + ) >= + this.minViaPadDiameter - 1e-9 + ) + } + isKnownSingleLayerRegion(regionId: RegionId): boolean { const regionAvailableZMask = this.topology.regionAvailableZMask?.[regionId] ?? 0 @@ -1713,7 +1800,13 @@ export class TinyHyperGraphSolver extends BaseSolver { options: TinyHyperGraphSolverOptions, _attempt: number, ): TinyHyperGraphSolver { - return new GreedyFinalRouteSolver(this.topology, this.problem, options) + return new GreedyFinalRouteSolver(this.topology, this.problem, { + ...options, + MAX_ITERATIONS: Math.max( + options.MAX_ITERATIONS ?? GREEDY_FINAL_ROUTE_MAX_ITERATIONS, + this.topology.portCount + this.problem.routeCount, + ), + }) } protected tryGreedyFinalRouteAcceptance(): boolean { @@ -2012,7 +2105,8 @@ export class TinyHyperGraphSolver extends BaseSolver { if ( newSameLayerIntersections > 0 && - this.isKnownSingleLayerRegion(nextRegionId) + (this.isKnownSingleLayerRegion(nextRegionId) || + !this.canTransitionLayersInRegion(nextRegionId)) ) { return Number.POSITIVE_INFINITY } @@ -2113,8 +2207,14 @@ export class TinyHyperGraphSolver extends BaseSolver { class GreedyFinalRouteSolver extends TinyHyperGraphSolver { override computeG( currentCandidate: Candidate, - _neighborPortId: PortId, + neighborPortId: PortId, ): number { + if ( + this.ENFORCE_VIA_SPACE && + !Number.isFinite(super.computeG(currentCandidate, neighborPortId)) + ) { + return Number.POSITIVE_INFINITY + } return currentCandidate.g } } diff --git a/lib/prepared/DenseRoutingWorkspace.ts b/lib/prepared/DenseRoutingWorkspace.ts index b13339a..c7e3b73 100644 --- a/lib/prepared/DenseRoutingWorkspace.ts +++ b/lib/prepared/DenseRoutingWorkspace.ts @@ -127,7 +127,9 @@ export class DenseRoutingWorkspace { cache, via: solver.minViaPadDiameter, density: solver.TRACE_DENSITY_COST_FACTOR, - single: mask > 0 && (mask & (mask - 1)) === 0, + single: + (mask > 0 && (mask & (mask - 1)) === 0) || + !solver.canTransitionLayersInRegion(region), costs: new Float64Array(2 * cache.netIds.length + 2), } // Multi-layer cost depends on 2*same + crossing + entry/exit. Single-layer @@ -181,7 +183,25 @@ export class DenseRoutingWorkspace { const before = previous previous = at const port = neighbors[at]! + if ( + solver.minPortSpacing > 0 && + solver.isPortReservedForDifferentNet(port) + ) { + continue + } + if ( + t.portZ[c.portId] !== t.portZ[port] && + !solver.canTransitionLayersInRegion(region) + ) { + continue + } if (port === state.goalPortId) { + if ( + solver.ENFORCE_VIA_SPACE && + !Number.isFinite(solver.computeG(c, port)) + ) { + continue + } solver.onPathFound(c) return } diff --git a/tests/solver/destination-crossing-respects-via-space.test.ts b/tests/solver/destination-crossing-respects-via-space.test.ts new file mode 100644 index 0000000..deadb35 --- /dev/null +++ b/tests/solver/destination-crossing-respects-via-space.test.ts @@ -0,0 +1,81 @@ +import { expect, test } from "bun:test" +import { TinyHyperGraphSolver } from "lib/core" +import { PreparedPortPointRoutingSolver } from "lib/prepared/PreparedPortPointRoutingSolver" + +test("checks the destination crossing before accepting a route without via space", (): void => { + for (const Solver of [TinyHyperGraphSolver, PreparedPortPointRoutingSolver]) { + for (const frontier of [false, true]) { + for (const completion of [false, true]) { + for (const [enforceViaSpace, regionSize, expectedSolved] of [ + [true, 0.2, false], + [true, 1, true], + [false, 0.2, true], + ] as const) { + const solver = new Solver( + { + portCount: 6, + regionCount: 5, + regionIncidentPorts: [[0, 1, 2, 3], [4, 0], [1], [5, 2], [3]], + incidentPortRegion: [[1, 0], [0, 2], [3, 0], [0, 4], [1], [3]], + regionWidth: new Float64Array([regionSize, 1, 1, 1, 1]), + regionHeight: new Float64Array([regionSize, 1, 1, 1, 1]), + regionCenterX: new Float64Array([0, -0.6, 0.6, 0, 0]), + regionCenterY: new Float64Array([0, 0, 0, -0.6, 0.6]), + regionAvailableZMask: new Int32Array([3, 1, 1, 1, 1]), + portAngleForRegion1: new Int32Array([ + 0, 0, 9000, 9000, 18000, 27000, + ]), + portAngleForRegion2: new Int32Array([ + 18000, 18000, 27000, 27000, 0, 0, + ]), + portX: new Float64Array([-0.1, 0.1, 0, 0, -0.6, 0]), + portY: new Float64Array([0, 0, -0.1, 0.1, 0, -0.6]), + portZ: new Int32Array(6), + }, + { + routeCount: 2, + portSectionMask: new Int8Array(6).fill(1), + routeStartPort: new Int32Array([4, 5]), + routeEndPort: new Int32Array([1, 3]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array([-1, 0, 0, 1, 1]), + }, + { + minViaPadDiameter: 0.3, + ENFORCE_VIA_SPACE: enforceViaSpace, + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: true, + GREEDY_FINAL_ROUTE_ITERS: 1, + MAX_ITERATIONS: 100, + STATIC_REACHABILITY_PRECHECK: false, + OWNED_REGIONAL_HOP_FRONTIER: frontier, + USE_SPARSE_CANDIDATE_STORAGE: false, + }, + ) + if (completion) solver.tryFinalAcceptance() + else solver.solve() + expect({ + solver: Solver.name, + frontier, + completion, + enforceViaSpace, + regionSize, + solved: solver.solved, + }).toEqual({ + solver: Solver.name, + frontier, + completion, + enforceViaSpace, + regionSize, + solved: expectedSolved, + }) + if (expectedSolved) { + expect( + new Set(solver.state.regionSegments[0].map(([route]) => route)) + .size, + ).toBe(2) + } + } + } + } + } +}) diff --git a/tests/solver/duplicate-congested-port-physical-capacity.test.ts b/tests/solver/duplicate-congested-port-physical-capacity.test.ts new file mode 100644 index 0000000..1c6db29 --- /dev/null +++ b/tests/solver/duplicate-congested-port-physical-capacity.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { DuplicateCongestedPortSolver } from "lib/DuplicateCongestedPortSolver" + +type SerializedPortId = SerializedHyperGraph["ports"][number]["portId"] + +class PhysicalCapacityTestSolver extends DuplicateCongestedPortSolver { + duplicatePorts( + portUseCounts: Map, + ): SerializedHyperGraph { + return this.duplicateCongestedPorts(portUseCounts) + } +} + +test("limits duplicated crossings to the physical capacity on each layer", () => { + for (const height of [0.4, 0.9]) { + const ports = [0, 1].map((z) => ({ + portId: `source-${z}`, + region1Id: "left", + region2Id: "right", + d: { x: 0, y: 0, z, cramped: true }, + })) + const graph: SerializedHyperGraph = { + regions: [-0.5, 0.5].map((x, index) => ({ + regionId: index === 0 ? "left" : "right", + pointIds: ports.map((port) => port.portId), + d: { center: { x, y: 0 }, width: 1, height }, + })), + ports, + connections: [], + } + const solver = new PhysicalCapacityTestSolver(graph, { + minPortSpacing: 0.3, + }) + const output = solver.duplicatePorts( + new Map(ports.map((port) => [port.portId, 4])), + ) + for (const z of [0, 1]) { + const layerPorts = output.ports.filter((port) => port.d?.z === z) + expect(layerPorts).toHaveLength(height === 0.4 ? 1 : 3) + expect(height / layerPorts.length).toBeGreaterThanOrEqual(0.3) + } + } +}) diff --git a/tests/solver/duplicate-congested-port-unordered-boundary.test.ts b/tests/solver/duplicate-congested-port-unordered-boundary.test.ts new file mode 100644 index 0000000..1046df1 --- /dev/null +++ b/tests/solver/duplicate-congested-port-unordered-boundary.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { DuplicateCongestedPortSolver } from "lib/DuplicateCongestedPortSolver" + +type SerializedPortId = SerializedHyperGraph["ports"][number]["portId"] + +class UnorderedBoundaryTestSolver extends DuplicateCongestedPortSolver { + duplicatePorts( + portUseCounts: Map, + ): SerializedHyperGraph { + return this.duplicateCongestedPorts(portUseCounts) + } +} + +test("reversing the boundary owners keeps duplicates on the same side of fixed copper", () => { + const positions: unknown[] = [] + for (const [region1Id, region2Id] of [ + ["left", "right"], + ["right", "left"], + ]) { + const solver = new UnorderedBoundaryTestSolver({ + regions: [-1, 1].map((x, index) => ({ + regionId: index === 0 ? "left" : "right", + pointIds: ["fixed-crossing"], + d: { center: { x, y: 0 }, width: 2, height: 2 }, + })), + ports: [ + { + portId: "fixed-crossing", + region1Id, + region2Id, + d: { x: 0, y: 0, z: 0, _preloadedFixedNetIds: ["fixed-net"] }, + }, + ], + connections: [], + }) + const duplicate = solver.duplicatePorts(new Map([["fixed-crossing", 2]])) + .ports[1] + positions.push({ x: duplicate.d?.x, y: duplicate.d?.y }) + } + expect(positions[0]).toEqual(positions[1]) +}) diff --git a/tests/solver/greedy-completion-avoids-crossings-without-via-space.test.ts b/tests/solver/greedy-completion-avoids-crossings-without-via-space.test.ts new file mode 100644 index 0000000..34dfec1 --- /dev/null +++ b/tests/solver/greedy-completion-avoids-crossings-without-via-space.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from "bun:test" +import { TinyHyperGraphSolver } from "lib/core" + +test("completion routes around a crossing when the region cannot fit a via", () => { + for (const narrowRegionLayers of [1, 3]) { + const solver = new TinyHyperGraphSolver( + { + portCount: 7, + regionCount: 6, + regionIncidentPorts: [ + [0, 1, 2, 3, 4, 5], + [4, 5], + [0], + [1], + [2], + [3, 6], + ], + incidentPortRegion: [ + [2, 0], + [0, 3], + [4, 0], + [0, 5], + [0, 1], + [0, 1], + [5], + ], + regionWidth: new Float64Array([0.2, 1, 1, 1, 1, 1]), + regionHeight: new Float64Array([0.2, 1, 1, 1, 1, 1]), + regionCenterX: new Float64Array([0, 0.6, -1, 1, 0, 0]), + regionCenterY: new Float64Array([0, 0, 0, 0, -1, 1]), + regionAvailableZMask: new Int32Array([ + narrowRegionLayers, + 3, + 1, + 1, + 1, + 1, + ]), + portAngleForRegion1: new Int32Array([0, 0, 0, 9000, 33343, 2656, 0]), + portAngleForRegion2: new Int32Array([ + 18000, 0, 27000, 0, 18571, 17429, 0, + ]), + portX: new Float64Array([-0.1, 0.1, 0, 0, 0.1, 0.1, 0]), + portY: new Float64Array([0, 0, -0.1, 0.1, -0.05, 0.05, 0.5]), + portZ: new Int32Array(7), + }, + { + routeCount: 2, + portSectionMask: new Int8Array(7).fill(1), + routeStartPort: new Int32Array([0, 2]), + routeEndPort: new Int32Array([1, 6]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array([-1, -1, 0, 0, 1, 1]), + }, + { + minViaPadDiameter: 0.3, + ENFORCE_VIA_SPACE: true, + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: true, + GREEDY_FINAL_ROUTE_ITERS: 1, + STATIC_REACHABILITY_PRECHECK: false, + }, + ) + + solver.tryFinalAcceptance() + + expect(solver.solved).toBe(true) + expect(solver.state.regionSegments[1].some(([route]) => route === 1)).toBe( + true, + ) + expect( + solver.state.regionSegments[0].some( + ([route, start, end]) => route === 1 && start === 2 && end === 3, + ), + ).toBe(false) + } +}) diff --git a/tests/solver/greedy-completion-respects-via-space.test.ts b/tests/solver/greedy-completion-respects-via-space.test.ts new file mode 100644 index 0000000..05a8d97 --- /dev/null +++ b/tests/solver/greedy-completion-respects-via-space.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { TinyHyperGraphSolver } from "lib/core" + +test("completion changes layers in a region with room for the via", () => { + for (const ownsNarrowRegion of [false, true]) { + const solver = new TinyHyperGraphSolver( + { + portCount: 4, + regionCount: 4, + regionIncidentPorts: [[0, 1, 2, 3], [2, 3], [0], [1]], + incidentPortRegion: [ + [0, 2], + [0, 3], + [0, 1], + [0, 1], + ], + regionWidth: new Float64Array([0.1, 2, 0.1, 0.1]), + regionHeight: new Float64Array(4).fill(2), + regionCenterX: new Float64Array([0, 1.05, -0.1, -0.1]), + regionCenterY: new Float64Array(4), + regionAvailableZMask: new Int32Array([3, 3, 1, 2]), + portAngleForRegion1: new Int32Array(4), + portAngleForRegion2: new Int32Array(4), + portX: new Float64Array([-0.05, -0.05, 0.05, 0.05]), + portY: new Float64Array(4), + portZ: new Int32Array([0, 1, 0, 1]), + }, + { + routeCount: 1, + portSectionMask: new Int8Array(4).fill(1), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array([ownsNarrowRegion ? 0 : -1, -1, 0, 0]), + }, + { + minViaPadDiameter: 0.3, + ENFORCE_VIA_SPACE: true, + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: true, + GREEDY_FINAL_ROUTE_ITERS: 1, + STATIC_REACHABILITY_PRECHECK: false, + }, + ) + + solver.tryFinalAcceptance() + + expect(solver.solved).toBe(true) + const viaRegion = ownsNarrowRegion ? 0 : 1 + expect( + solver.state.regionSegments[viaRegion].some( + ([, start, end]) => + solver.topology.portZ[start] !== solver.topology.portZ[end], + ), + ).toBe(true) + if (!ownsNarrowRegion) { + expect( + solver.state.regionSegments[0].every( + ([, start, end]) => + solver.topology.portZ[start] === solver.topology.portZ[end], + ), + ).toBe(true) + } + } +}) diff --git a/tests/solver/greedy-final-route-large-graph.test.ts b/tests/solver/greedy-final-route-large-graph.test.ts new file mode 100644 index 0000000..6f15575 --- /dev/null +++ b/tests/solver/greedy-final-route-large-graph.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test" +import { TinyHyperGraphSolver } from "lib/core" + +test("completion can traverse a graph larger than the fixed 50000-step limit", () => { + const portCount = 50_002 + const regionCount = portCount - 1 + const solver = new TinyHyperGraphSolver( + { + portCount, + regionCount, + regionIncidentPorts: Array.from({ length: regionCount }, (_, i) => [ + i, + i + 1, + ]), + incidentPortRegion: Array.from({ length: portCount }, (_, i) => + [i - 1, i].filter((region) => region >= 0 && region < regionCount), + ), + regionWidth: new Float64Array(regionCount).fill(1), + regionHeight: new Float64Array(regionCount).fill(1), + regionCenterX: Float64Array.from( + { length: regionCount }, + (_, i) => i + 0.5, + ), + regionCenterY: new Float64Array(regionCount), + regionAvailableZMask: new Int32Array(regionCount).fill(1), + portAngleForRegion1: new Int32Array(portCount), + portAngleForRegion2: new Int32Array(portCount).fill(18000), + portX: Float64Array.from({ length: portCount }, (_, i) => i), + portY: new Float64Array(portCount), + portZ: new Int32Array(portCount), + }, + { + routeCount: 1, + portSectionMask: new Int8Array(portCount).fill(1), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([portCount - 1]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array(regionCount).fill(-1), + }, + { + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: true, + GREEDY_FINAL_ROUTE_ITERS: 1, + STATIC_REACHABILITY_PRECHECK: false, + }, + ) + + solver.tryFinalAcceptance() + + expect(solver.solved).toBe(true) + expect(solver.state.unroutedRoutes).toHaveLength(0) + expect( + solver.state.regionSegments.every((segments) => segments.length === 1), + ).toBe(true) +}) diff --git a/tests/solver/port-spacing-avoids-nearby-foreign-net.test.ts b/tests/solver/port-spacing-avoids-nearby-foreign-net.test.ts new file mode 100644 index 0000000..091e350 --- /dev/null +++ b/tests/solver/port-spacing-avoids-nearby-foreign-net.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test" +import { TinyHyperGraphSolver } from "lib/core" +import { PreparedPortPointRoutingSolver } from "lib/prepared/PreparedPortPointRoutingSolver" + +test("routes around a nearby occupied port on the same layer", () => { + for (const Solver of [TinyHyperGraphSolver, PreparedPortPointRoutingSolver]) { + for (const completion of [false, true]) { + const solver = new Solver( + { + portCount: 6, + regionCount: 6, + regionIncidentPorts: [[0, 2, 3, 4, 5], [1, 2, 3], [0], [1], [4], [5]], + incidentPortRegion: [ + [0, 2], + [1, 3], + [0, 1], + [0, 1], + [0, 4], + [0, 5], + ], + regionWidth: new Float64Array(6).fill(2), + regionHeight: new Float64Array(6).fill(2), + regionCenterX: new Float64Array([-1, 1, -2, 2, -1, -1]), + regionCenterY: new Float64Array(6), + portAngleForRegion1: new Int32Array([0, 0, 0, 9000, 0, 9000]), + portAngleForRegion2: new Int32Array([0, 0, 18000, 18000, 0, 0]), + portX: new Float64Array([-1, 1, 0, 0, 0, -1]), + portY: new Float64Array([0, 0, 0, 0.5, 0.05, 0.5]), + portZ: new Int32Array(6), + }, + { + routeCount: 2, + portSectionMask: new Int8Array(6).fill(1), + routeStartPort: new Int32Array([4, 0]), + routeEndPort: new Int32Array([5, 1]), + routeNet: new Int32Array([1, 0]), + regionNetId: new Int32Array([-1, -1, 0, 0, 1, 1]), + }, + { + minPortSpacing: 0.3, + STATIC_REACHABILITY_PRECHECK: false, + ACCEPT_BEST_SOLUTION_ON_TIMEOUT: true, + GREEDY_FINAL_ROUTE_ITERS: 1, + }, + ) + if (completion) solver.tryFinalAcceptance() + else solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.state.portAssignment[2]).toBe(-1) + expect(solver.state.portAssignment[3]).toBe(0) + } + } +})