diff --git a/README.md b/README.md index b9e423e..45e9298 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ from both retained ends, with a hard geometric travel limit on each frontier; if the frontiers cannot meet within that limit, the span safely falls back to the regular one-ended search. +Whole routes also search from both endpoints, without the local span's distance +limit. If either unrestricted frontier exhausts its candidates, reripping can +begin immediately instead of exploring the remaining graph from the other end. + The behavior can be tuned through `TinyHyperGraphSolverOptions`: ```ts diff --git a/lib/DuplicateCongestedPortSolver.ts b/lib/DuplicateCongestedPortSolver.ts index 2de9653..84eedd6 100644 --- a/lib/DuplicateCongestedPortSolver.ts +++ b/lib/DuplicateCongestedPortSolver.ts @@ -7,10 +7,11 @@ import { type TinyHyperGraphSolverOptions, type TinyHyperGraphTopology, } from "./core" -import type { PortId, RouteId } from "./types" +import type { NetId, PortId, RouteId } from "./types" type SerializedPort = SerializedHyperGraph["ports"][number] type SerializedRegion = SerializedHyperGraph["regions"][number] +type SerializedPortId = SerializedPort["portId"] export const DUPLICATE_PORT_PROXIMITY = 0.05 @@ -179,6 +180,23 @@ const getFallbackBoundaryDirection = ( ): Point => { const region1Center = getRegionCenter(regionById.get(sourcePort.region1Id)) const region2Center = getRegionCenter(regionById.get(sourcePort.region2Id)) + const region1Bounds = getRegionBounds(regionById.get(sourcePort.region1Id)) + const region2Bounds = getRegionBounds(regionById.get(sourcePort.region2Id)) + const sharedWidth = + Math.min(region1Bounds.maxX, region2Bounds.maxX) - + Math.max(region1Bounds.minX, region2Bounds.minX) + const sharedHeight = + Math.min(region1Bounds.maxY, region2Bounds.maxY) - + Math.max(region1Bounds.minY, region2Bounds.minY) + + // Neighboring rectangles can have offset centers. Their shared boundary + // is axis-aligned even when the line between their centers is diagonal. + if (Math.abs(sharedWidth) <= EPSILON && sharedHeight > EPSILON) { + return { x: 0, y: region2Center.x >= region1Center.x ? 1 : -1 } + } + if (Math.abs(sharedHeight) <= EPSILON && sharedWidth > EPSILON) { + return { x: region2Center.y >= region1Center.y ? -1 : 1, y: 0 } + } const perpendicular = normalize({ x: -(region2Center.y - region1Center.y), y: region2Center.x - region1Center.x, @@ -324,14 +342,14 @@ export class DuplicateCongestedPortSolver extends BaseSolver { } } - private getPortUseCounts(): Map { + private getPortUseCounts(): Map { const { topology, problem } = loadSerializedHyperGraph( this.serializedHyperGraph, ) if (this.options.useSerializedPortPenalties === false) { problem.portPenalty = undefined } - const portUseCounts = new Map() + const netsByPortId = new Map>() for (let routeId = 0; routeId < problem.routeCount; routeId++) { const routeProblem = createSingleRouteProblem(problem, routeId) @@ -352,14 +370,16 @@ export class DuplicateCongestedPortSolver extends BaseSolver { for (const portId of getUsedPortIdsForSolvedRoute(routeSolver)) { const serializedPortId = getSerializedPortId(topology, portId) - portUseCounts.set( - serializedPortId, - (portUseCounts.get(serializedPortId) ?? 0) + 1, - ) + const nets = netsByPortId.get(serializedPortId) ?? new Set() + nets.add(problem.routeNet[routeId]!) + netsByPortId.set(serializedPortId, nets) } } - return portUseCounts + // The router owns ports by net, so branches of one net share capacity. + return new Map( + [...netsByPortId].map(([portId, nets]) => [portId, nets.size]), + ) } private duplicateCongestedPorts( diff --git a/lib/core.ts b/lib/core.ts index 4c6cc93..a447c64 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -764,6 +764,9 @@ export class TinyHyperGraphSolver extends BaseSolver { if (assignedNetId !== -1 && assignedNetId !== state.currentRouteNetId) { continue } + if (!Number.isFinite(this.computeG(currentCandidate, neighborPortId))) { + continue + } this.onPathFound(currentCandidate) return } @@ -1780,8 +1783,13 @@ export class TinyHyperGraphSolver extends BaseSolver { class GreedyFinalRouteSolver extends TinyHyperGraphSolver { override computeG( currentCandidate: Candidate, - _neighborPortId: PortId, + neighborPortId: PortId, ): number { + // Greedy routing may ignore congestion costs, but a forbidden crossing + // still cannot be routed by the downstream single-layer solver. + if (!Number.isFinite(super.computeG(currentCandidate, neighborPortId))) { + return Number.POSITIVE_INFINITY + } return currentCandidate.g } } diff --git a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts index be89492..f1d6f22 100644 --- a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts +++ b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts @@ -73,9 +73,10 @@ const NO_PREFERRED_PRESERVED_ROUTE_IDS = new Set() * as a normal route with temporary endpoints, so all existing cost and hard * constraint checks continue to apply. * - * Outside-in frontier search is implemented by this class separately from the - * partial-rip state transition. Initial whole routes retain the established - * one-ended search; the bounded two-ended search applies to reopened spans. + * Outside-in frontier search applies to whole routes and reopened spans. Only + * reopened spans use the local distance bound; whole routes are bounded by the + * solver's iteration budget. Either frontier can detect a blocked endpoint + * without flooding the graph from the other end. */ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHyperGraphSolver { protected partialRipRoutePlans = new Map() @@ -729,10 +730,12 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy connectorDx * connectorDx + connectorDy * connectorDy, ) if ( + this.state.currentRouteId !== undefined && + this.partialRipRoutePlans.has(this.state.currentRouteId) && forwardCandidate.travelDistance + reverseCandidate.travelDistance + connectorDistance > - this.OUTSIDE_IN_MAX_DISTANCE * 2 + this.OUTSIDE_IN_MAX_DISTANCE * 2 ) { return undefined } @@ -884,7 +887,10 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy segmentDx * segmentDx + segmentDy * segmentDy, ) const travelDistance = candidate.travelDistance + segmentDistance - if (travelDistance > this.OUTSIDE_IN_MAX_DISTANCE) { + if ( + this.partialRipRoutePlans.has(search.routeId) && + travelDistance > this.OUTSIDE_IN_MAX_DISTANCE + ) { search.distanceLimitHit = true this.outsideInDistancePruneCount += 1 continue @@ -959,16 +965,6 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy return } - const routeIdToAdvance = - this.state.currentRouteId ?? this.state.unroutedRoutes[0] - if ( - routeIdToAdvance !== undefined && - !this.partialRipRoutePlans.has(routeIdToAdvance) - ) { - super._step() - return - } - if (this.oneSidedFallbackRouteId !== undefined) { super._step() return @@ -1011,10 +1007,11 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy } } + const forwardExhausted = search.forward.queue.length === 0 + const reverseExhausted = search.reverse.queue.length === 0 if ( - !expanded && - search.forward.queue.length === 0 && - search.reverse.queue.length === 0 + (!search.distanceLimitHit && (forwardExhausted || reverseExhausted)) || + (!expanded && forwardExhausted && reverseExhausted) ) { if (this.commitBestOutsideInJoin()) return this.outsideInRouteSearch = undefined diff --git a/lib/selective-rerip-tiny-hyper-graph-solver.ts b/lib/selective-rerip-tiny-hyper-graph-solver.ts index bb93ff6..4d80940 100644 --- a/lib/selective-rerip-tiny-hyper-graph-solver.ts +++ b/lib/selective-rerip-tiny-hyper-graph-solver.ts @@ -222,11 +222,13 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH const count = this.incrementFailedOwnerPair(failedRouteId, ownerRouteId) if (count >= 2) repeatedOwnerRouteIds.push(ownerRouteId) } - if ( - directOwnerRouteIds.some((ownerRouteId) => - this.hasFailedOwnerPath(ownerRouteId, failedRouteId), - ) - ) { + const cyclicOwnerRouteIds = directOwnerRouteIds.filter((ownerRouteId) => + this.hasFailedOwnerPath(ownerRouteId, failedRouteId), + ) + // Before the first complete solution, keep completed routes while + // looking for another blocker. Quality retries can use the established + // global rerip once a complete solution is available to restore. + if (cyclicOwnerRouteIds.length > 0 && this.bestSolvedStateSnapshot) { this.selectiveReripStats.globalReripCount += 1 this.selectiveReripStats.globalReripReason = "failed_owner_cycle" this.selectiveReripStats.lastFailedRouteId = failedRouteId @@ -242,6 +244,27 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH this.publishSelectiveReripStats() return } + const ownersToAvoid = new Set([ + ...repeatedOwnerRouteIds, + ...cyclicOwnerRouteIds, + ]) + // Alternate blockers also participate in cycles. Avoid every owner that + // would repeat a failed displacement, not only the direct path's owners. + for ( + let ownerRouteId = 0; + ownerRouteId < this.problem.routeCount; + ownerRouteId++ + ) { + if (ownerRouteId === failedRouteId) continue + const failureCount = + this.failedOwnerPairCounts.get(failedRouteId)?.get(ownerRouteId) ?? 0 + if ( + failureCount >= 2 || + this.hasFailedOwnerPath(ownerRouteId, failedRouteId) + ) { + ownersToAvoid.add(ownerRouteId) + } + } let alternatePath: | DistinctOwnerBlockerSearchResult< @@ -250,14 +273,16 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH RelaxedSearchHopData > | undefined - if (repeatedOwnerRouteIds.length > 0) { + if (ownersToAvoid.size > 0) { this.selectiveReripStats.alternateBlockerSearchCount += 1 - alternatePath = this.findRelaxedBlockerPathPreferringPreservedRoutes( - new Set(repeatedOwnerRouteIds), - ) + alternatePath = + this.findRelaxedBlockerPathPreferringPreservedRoutes(ownersToAvoid) if (!alternatePath.found) { this.selectiveReripStats.globalReripCount += 1 - this.selectiveReripStats.globalReripReason = alternatePath.reason + this.selectiveReripStats.globalReripReason = + cyclicOwnerRouteIds.length > 0 + ? "failed_owner_cycle" + : alternatePath.reason this.selectiveReripStats.lastFailedRouteId = failedRouteId this.selectiveReripStats.lastDirectOwnerRouteIds = directOwnerRouteIds this.selectiveReripStats.lastRepeatedOwnerRouteIds = @@ -268,6 +293,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH directPath.expandedLabelCount this.selectiveReripStats.lastAlternateSearchExpandedLabelCount = alternatePath.expandedLabelCount + if (cyclicOwnerRouteIds.length > 0) this.failedOwnerPairCounts.clear() super.onOutOfCandidates() this.publishSelectiveReripStats() return @@ -286,6 +312,9 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH const alternateOnlyOwnerRouteIds = (alternateOwnerRouteIds ?? []).filter( (ownerRouteId) => !directPath.owners.has(ownerRouteId), ) + for (const ownerRouteId of alternateOnlyOwnerRouteIds) { + this.incrementFailedOwnerPair(failedRouteId, ownerRouteId) + } if ( this.selectiveReripCongestionUpdateCount < MAX_SELECTIVE_RERIP_CONGESTION_UPDATES diff --git a/tests/outside-in-blocked-endpoint.test.ts b/tests/outside-in-blocked-endpoint.test.ts new file mode 100644 index 0000000..9cb80a8 --- /dev/null +++ b/tests/outside-in-blocked-endpoint.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test" +import { + OutsideInPartialRipTinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphTopology, +} from "lib/index" + +test("an exhausted endpoint starts reripping without flooding the opposite frontier", () => { + const chainPortCount = 200 + const portCount = chainPortCount + 1 + const regionCount = chainPortCount + 3 + const regionIncidentPorts: number[][] = Array.from( + { length: regionCount }, + () => [], + ) + const incidentPortRegion: number[][] = [] + for (let portId = 0; portId < portCount; portId++) { + const firstRegionId = portId === chainPortCount ? portId + 1 : portId + const secondRegionId = firstRegionId + 1 + incidentPortRegion.push([firstRegionId, secondRegionId]) + regionIncidentPorts[firstRegionId]!.push(portId) + regionIncidentPorts[secondRegionId]!.push(portId) + } + incidentPortRegion[0]!.reverse() + const topology: TinyHyperGraphTopology = { + portCount, + regionCount, + regionIncidentPorts, + incidentPortRegion, + regionWidth: new Float64Array(regionCount).fill(1), + regionHeight: new Float64Array(regionCount).fill(10), + regionCenterX: Float64Array.from( + { length: regionCount }, + (_, index) => index, + ), + regionCenterY: new Float64Array(regionCount), + portAngleForRegion1: new Int32Array(portCount), + portAngleForRegion2: new Int32Array(portCount).fill(18000), + portX: Float64Array.from({ length: portCount }, (_, index) => index + 0.5), + portY: new Float64Array(portCount), + portZ: new Int32Array(portCount), + } + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array(portCount).fill(1), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([chainPortCount]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array(regionCount).fill(-1), + } + const solver = new OutsideInPartialRipTinyHyperGraphSolver( + topology, + problem, + { + STATIC_REACHABILITY_PRECHECK: false, + }, + ) + while (solver.state.ripCount === 0 && !solver.failed) solver.step() + + expect(solver.solved).toBe(false) + expect(solver.state.ripCount).toBe(1) + expect(solver.iterations).toBe(2) + expect(solver.stats.outsideInForwardExpansionCount).toBe(1) + expect(solver.stats.outsideInReverseExpansionCount).toBe(1) + expect(solver.stats.outsideInFallbackRouteCount).toBe(0) +}) diff --git a/tests/outside-in-whole-route-distance.test.ts b/tests/outside-in-whole-route-distance.test.ts new file mode 100644 index 0000000..b9e1dee --- /dev/null +++ b/tests/outside-in-whole-route-distance.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "bun:test" +import { + OutsideInPartialRipTinyHyperGraphSolver, + type TinyHyperGraphProblem, + type TinyHyperGraphTopology, +} from "lib/index" + +test("whole routes use both frontiers beyond the partial-span distance limit", () => { + const topology: TinyHyperGraphTopology = { + portCount: 5, + regionCount: 6, + regionIncidentPorts: [[0], [0, 1], [1, 2], [2, 3], [3, 4], [4]], + incidentPortRegion: [ + [1, 0], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + ], + regionWidth: new Float64Array(6).fill(25), + regionHeight: new Float64Array(6).fill(10), + regionCenterX: new Float64Array([-12.5, 12.5, 37.5, 62.5, 87.5, 112.5]), + regionCenterY: new Float64Array(6), + portAngleForRegion1: new Int32Array([18000, 0, 0, 0, 0]), + portAngleForRegion2: new Int32Array([0, 18000, 18000, 18000, 18000]), + portX: new Float64Array([0, 25, 50, 75, 100]), + portY: new Float64Array(5), + portZ: new Int32Array(5), + } + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array(5).fill(1), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([4]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array(6).fill(-1), + } + const solver = new OutsideInPartialRipTinyHyperGraphSolver( + topology, + problem, + { + OUTSIDE_IN_MAX_DISTANCE: 24, + }, + ) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.stats.outsideInCompletedRouteCount).toBe(1) + expect(solver.stats.outsideInForwardExpansionCount).toBeGreaterThan(0) + expect(solver.stats.outsideInReverseExpansionCount).toBeGreaterThan(0) + expect(solver.stats.outsideInDistancePruneCount).toBe(0) + expect(solver.stats.outsideInFallbackRouteCount).toBe(0) + expect(solver.getOutput().solvedRoutes?.[0]?.path).toHaveLength(5) +}) diff --git a/tests/selective-rerip-cycle-alternate-path.test.ts b/tests/selective-rerip-cycle-alternate-path.test.ts new file mode 100644 index 0000000..c534682 --- /dev/null +++ b/tests/selective-rerip-cycle-alternate-path.test.ts @@ -0,0 +1,131 @@ +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { loadSerializedHyperGraph } from "lib/compat/loadSerializedHyperGraph" +import { SelectiveReripTinyHyperGraphSolver } from "lib/index" + +test("a rerip cycle uses another occupied channel without restarting completed routes", () => { + const regions: SerializedHyperGraph["regions"] = [] + const ports: SerializedHyperGraph["ports"] = [] + const routeDefinitions = [ + { name: "flexible", net: 0, y: 0, channels: ["a", "b"] }, + { name: "movable", net: 1, y: 1, channels: ["b", "c"] }, + { name: "constrained", net: 2, y: -1, channels: ["a"] }, + ] + for (const [channelIndex, channel] of ["a", "b", "c"].entries()) { + for (const side of ["left", "right"]) { + regions.push({ + regionId: `${channel}-${side}`, + pointIds: [], + d: { + center: { x: side === "left" ? -1 : 1, y: channelIndex }, + width: 2, + height: 1, + }, + }) + } + ports.push({ + portId: channel, + region1Id: `${channel}-left`, + region2Id: `${channel}-right`, + d: { + x: 0, + y: channelIndex, + z: 0, + tinyHypergraphPortPenalty: channelIndex * 2, + }, + }) + } + for (const route of routeDefinitions) { + for (const side of ["left", "right"]) { + const xDirection = side === "left" ? -1 : 1 + for (const [kind, x] of [ + ["terminal", 6], + ["branch", 4], + ] as const) { + regions.push({ + regionId: `${route.name}-${side}-${kind}`, + pointIds: [], + d: { + center: { x: x * xDirection, y: route.y }, + width: 2, + height: 1, + netId: route.net, + }, + }) + } + ports.push({ + portId: `${route.name}-${side}`, + region1Id: `${route.name}-${side}-branch`, + region2Id: `${route.name}-${side}-terminal`, + d: { x: 5 * xDirection, y: route.y, z: 0 }, + }) + for (const channel of route.channels) { + ports.push({ + portId: `${route.name}-${side}-${channel}`, + region1Id: `${route.name}-${side}-branch`, + region2Id: `${channel}-${side}`, + d: { x: 2 * xDirection, y: route.y, z: 0 }, + }) + } + } + } + for (const region of regions) { + region.pointIds = ports + .filter( + (port) => + port.region1Id === region.regionId || + port.region2Id === region.regionId, + ) + .map((port) => port.portId) + } + const { topology, problem } = loadSerializedHyperGraph({ + regions, + ports, + connections: routeDefinitions.map((route) => ({ + connectionId: route.name, + mutuallyConnectedNetworkId: route.name, + startRegionId: `${route.name}-left-terminal`, + endRegionId: `${route.name}-right-terminal`, + })), + }) + const solver = new SelectiveReripTinyHyperGraphSolver(topology, problem, { + RIP_THRESHOLD_START: 100, + RIP_THRESHOLD_END: 100, + MAX_ITERATIONS: 20_000, + GREEDY_FINAL_ROUTE_ITERS: 0, + }) + let checkedAlternateRipCount = 0 + let lastSelectiveRipCount = 0 + while (!solver.solved && !solver.failed) { + solver.step() + const stats = solver.getSelectiveReripStats() + if (stats.selectiveRipCount === lastSelectiveRipCount) continue + lastSelectiveRipCount = stats.selectiveRipCount + for (const ownerRouteId of stats.lastAlternateOwnerRouteIds) { + checkedAlternateRipCount += 1 + expect( + stats.failedOwnerPairs.some( + (pair) => + pair.failedRouteId === stats.lastFailedRouteId && + pair.ownerRouteId === ownerRouteId, + ), + ).toBe(true) + } + } + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.getSelectiveReripStats().globalReripCount).toBe(0) + expect(checkedAlternateRipCount).toBeGreaterThan(0) + expect( + solver.getSelectiveReripStats().alternateBlockerSearchCount, + ).toBeGreaterThan(0) + const netByChannel = Object.fromEntries( + ["a", "b", "c"].map((channel) => { + const portId = topology.portMetadata!.findIndex( + (port) => port.serializedPortId === channel, + ) + return [channel, solver.state.portAssignment[portId]] + }), + ) + expect(netByChannel).toEqual({ a: 2, b: 0, c: 1 }) +}) diff --git a/tests/solver/duplicate-congested-port-offset-regions.test.ts b/tests/solver/duplicate-congested-port-offset-regions.test.ts new file mode 100644 index 0000000..c54de33 --- /dev/null +++ b/tests/solver/duplicate-congested-port-offset-regions.test.ts @@ -0,0 +1,96 @@ +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { DuplicateCongestedPortSolver } from "lib/index" + +test("duplicated crossings stay on the shared edge of offset neighboring regions", () => { + const graph: SerializedHyperGraph = { + regions: [ + { + regionId: "start-a", + pointIds: ["sa"], + d: { center: { x: -3, y: 0.5 }, width: 2, height: 1 }, + }, + { + regionId: "start-b", + pointIds: ["sb"], + d: { center: { x: -3, y: 1.5 }, width: 2, height: 1 }, + }, + { + regionId: "left", + pointIds: ["sa", "sb", "shared"], + d: { center: { x: -1, y: 0 }, width: 2, height: 4 }, + }, + { + regionId: "right", + pointIds: ["shared", "ea", "eb"], + d: { center: { x: 1, y: 1 }, width: 2, height: 2 }, + }, + { + regionId: "end-a", + pointIds: ["ea"], + d: { center: { x: 3, y: 0.5 }, width: 2, height: 1 }, + }, + { + regionId: "end-b", + pointIds: ["eb"], + d: { center: { x: 3, y: 1.5 }, width: 2, height: 1 }, + }, + ], + ports: [ + { + portId: "sa", + region1Id: "start-a", + region2Id: "left", + d: { x: -2, y: 0.5, z: 0 }, + }, + { + portId: "sb", + region1Id: "start-b", + region2Id: "left", + d: { x: -2, y: 1.5, z: 0 }, + }, + { + portId: "shared", + region1Id: "left", + region2Id: "right", + d: { x: 0, y: 0.5, z: 0 }, + }, + { + portId: "ea", + region1Id: "right", + region2Id: "end-a", + d: { x: 2, y: 0.5, z: 0 }, + }, + { + portId: "eb", + region1Id: "right", + region2Id: "end-b", + d: { x: 2, y: 1.5, z: 0 }, + }, + ], + connections: [ + { + connectionId: "a", + startRegionId: "start-a", + endRegionId: "end-a", + mutuallyConnectedNetworkId: "a", + }, + { + connectionId: "b", + startRegionId: "start-b", + endRegionId: "end-b", + mutuallyConnectedNetworkId: "b", + }, + ], + } + const solver = new DuplicateCongestedPortSolver(graph) + solver.solve() + const duplicate = solver + .getOutput() + .ports.find((port) => port.portId === "shared::dup1") + expect(duplicate).toBeDefined() + expect(duplicate!.d!.x).toBe(0) + expect(duplicate!.d!.y).toBeGreaterThan(0.5) + expect(duplicate!.d!.y).toBeLessThanOrEqual(0.55) + expect(graph.ports).toHaveLength(5) +}) diff --git a/tests/solver/duplicate-congested-port-same-net.test.ts b/tests/solver/duplicate-congested-port-same-net.test.ts new file mode 100644 index 0000000..0d9a6c8 --- /dev/null +++ b/tests/solver/duplicate-congested-port-same-net.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { DuplicateCongestedPortSolver } from "lib/index" + +test("same-net routes share a crossing without allocating duplicate ports", () => { + const graph: SerializedHyperGraph = { + regions: [ + { + regionId: "start", + pointIds: ["left"], + d: { center: { x: -2, y: 0 }, width: 2, height: 2 }, + }, + { + regionId: "middle", + pointIds: ["left", "right"], + d: { center: { x: 0, y: 0 }, width: 2, height: 2 }, + }, + { + regionId: "end", + pointIds: ["right"], + d: { center: { x: 2, y: 0 }, width: 2, height: 2 }, + }, + ], + ports: [ + { + portId: "left", + region1Id: "start", + region2Id: "middle", + d: { x: -1, y: 0, z: 0 }, + }, + { + portId: "right", + region1Id: "middle", + region2Id: "end", + d: { x: 1, y: 0, z: 0 }, + }, + ], + connections: [ + { + connectionId: "ground-a", + startRegionId: "start", + endRegionId: "end", + mutuallyConnectedNetworkId: "ground", + }, + { + connectionId: "ground-b", + startRegionId: "start", + endRegionId: "end", + mutuallyConnectedNetworkId: "ground", + }, + ], + } + const solver = new DuplicateCongestedPortSolver(graph) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.report.portUseCounts).toEqual({ left: 1, right: 1 }) + expect(solver.report.duplicatedPorts).toHaveLength(0) + expect(solver.getOutput().ports).toEqual(graph.ports) +}) diff --git a/tests/solver/greedy-final-single-layer-crossings.test.ts b/tests/solver/greedy-final-single-layer-crossings.test.ts new file mode 100644 index 0000000..0c9d938 --- /dev/null +++ b/tests/solver/greedy-final-single-layer-crossings.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from "bun:test" +import { + type TinyHyperGraphProblem, + TinyHyperGraphSolver, + type TinyHyperGraphTopology, +} from "lib/index" + +test("greedy final routing preserves single-layer crossing constraints", () => { + for (const z of [0, 1, 2, 3]) { + const topology: TinyHyperGraphTopology = { + portCount: 4, + regionCount: 5, + regionIncidentPorts: [[0, 1, 2, 3], [0], [1], [2], [3]], + incidentPortRegion: [ + [0, 1], + [0, 2], + [0, 3], + [0, 4], + ], + regionWidth: new Float64Array(5).fill(3), + regionHeight: new Float64Array(5).fill(3), + regionCenterX: new Float64Array(5), + regionCenterY: new Float64Array(5), + regionAvailableZMask: new Int32Array(5).fill(1 << z), + portAngleForRegion1: new Int32Array([0, 9000, 18000, 27000]), + portAngleForRegion2: new Int32Array(4), + portX: new Float64Array([1, 0, -1, 0]), + portY: new Float64Array([0, 1, 0, -1]), + portZ: new Int32Array(4).fill(z), + } + const problem: TinyHyperGraphProblem = { + routeCount: 2, + portSectionMask: new Int8Array(4).fill(1), + routeStartPort: new Int32Array([0, 1]), + routeEndPort: new Int32Array([2, 3]), + routeNet: new Int32Array([0, 1]), + regionNetId: new Int32Array(5).fill(-1), + initialAssignments: [ + { routeId: 0, regionId: 0, fromPortId: 0, toPortId: 2 }, + ], + } + const solver = new TinyHyperGraphSolver(topology, problem) + solver.tryFinalAcceptance() + + expect(solver.solved).toBe(false) + expect(solver.stats.acceptedGreedyFinalRouteOnTimeout).toBeUndefined() + expect(solver.state.regionSegments[0]).toEqual([[0, 0, 2]]) + expect( + solver.state.regionIntersectionCaches[0].existingSameLayerIntersections, + ).toBe(0) + + topology.regionAvailableZMask!.fill((1 << z) | (1 << ((z + 1) % 4))) + const multilayerSolver = new TinyHyperGraphSolver(topology, problem) + multilayerSolver.tryFinalAcceptance() + expect(multilayerSolver.solved).toBe(true) + expect(multilayerSolver.state.regionSegments[0]).toHaveLength(2) + } +})