Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 28 additions & 8 deletions lib/DuplicateCongestedPortSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -324,14 +342,14 @@ export class DuplicateCongestedPortSolver extends BaseSolver {
}
}

private getPortUseCounts(): Map<string, number> {
private getPortUseCounts(): Map<SerializedPortId, number> {
const { topology, problem } = loadSerializedHyperGraph(
this.serializedHyperGraph,
)
if (this.options.useSerializedPortPenalties === false) {
problem.portPenalty = undefined
}
const portUseCounts = new Map<string, number>()
const netsByPortId = new Map<SerializedPortId, Set<NetId>>()

for (let routeId = 0; routeId < problem.routeCount; routeId++) {
const routeProblem = createSingleRouteProblem(problem, routeId)
Expand All @@ -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<NetId>()
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(
Expand Down
10 changes: 9 additions & 1 deletion lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
}
33 changes: 15 additions & 18 deletions lib/outside-in-partial-rip-tiny-hypergraph-solver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ const NO_PREFERRED_PRESERVED_ROUTE_IDS = new Set<RouteId>()
* 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<RouteId, PartialRipRoutePlan>()
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
49 changes: 39 additions & 10 deletions lib/selective-rerip-tiny-hyper-graph-solver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<
Expand All @@ -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 =
Expand All @@ -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
Expand All @@ -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
Expand Down
66 changes: 66 additions & 0 deletions tests/outside-in-blocked-endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
55 changes: 55 additions & 0 deletions tests/outside-in-whole-route-distance.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
Loading
Loading