From e8df7c254413693d42aca503edd1f9ae482b185f Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Sun, 16 Aug 2026 17:19:39 +0200 Subject: [PATCH] Prefer preserving caller-owned routes during rerip --- ...e-in-partial-rip-tiny-hypergraph-solver.ts | 17 +++- ...selective-rerip-tiny-hyper-graph-solver.ts | 31 +++++++- ...partial-rip-tiny-hypergraph-solver.test.ts | 34 ++++++++ ...tive-rerip-tiny-hyper-graph-solver.test.ts | 78 +++++++++++++++++++ 4 files changed, 157 insertions(+), 3 deletions(-) diff --git a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts index da977e9..2f95812 100644 --- a/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts +++ b/lib/outside-in-partial-rip-tiny-hypergraph-solver.ts @@ -65,6 +65,8 @@ type CompletedRoundSummary = RegionCostSummary & { squaredRegionSegmentCount: number } +const NO_PREFERRED_PRESERVED_ROUTE_IDS = new Set() + /** * Retains the two outside portions of a completed route and only reroutes a * bounded window around a congested region. The active window is represented @@ -234,6 +236,14 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy } } + /** + * Quality rerips retain these routes when other routes cross the same hot + * regions. Completion rerips may still use them when they are unavoidable. + */ + protected getRouteIdsPreferredForPreservation(): ReadonlySet { + return NO_PREFERRED_PRESERVED_ROUTE_IDS + } + private getCommittedRouteSegments( routeId: RouteId, ): CommittedRouteSegment[] | undefined { @@ -423,6 +433,8 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy } } if (routeIdsTouchingHotRegions.size === 0) return false + const preferredPreservedRouteIds = + this.getRouteIdsPreferredForPreservation() const retainedSegmentsByRegion = Array.from( { length: this.topology.regionCount }, @@ -436,7 +448,10 @@ export class OutsideInPartialRipTinyHyperGraphSolver extends DistanceAwareTinyHy const orderedSegments = this.getCommittedRouteSegments(routeId) if (!orderedSegments) return false - if (!routeIdsTouchingHotRegions.has(routeId)) { + if ( + !routeIdsTouchingHotRegions.has(routeId) || + preferredPreservedRouteIds.has(routeId) + ) { for (const segment of orderedSegments) { this.appendRetainedSegment(retainedSegmentsByRegion, routeId, segment) retainedSegmentCount += 1 diff --git a/lib/selective-rerip-tiny-hyper-graph-solver.ts b/lib/selective-rerip-tiny-hyper-graph-solver.ts index a297c00..1e671ac 100644 --- a/lib/selective-rerip-tiny-hyper-graph-solver.ts +++ b/lib/selective-rerip-tiny-hyper-graph-solver.ts @@ -193,7 +193,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH ) } - const directPath = this.findRelaxedBlockerPath() + const directPath = this.findRelaxedBlockerPathPreferringPreservedRoutes() if (!directPath.found || directPath.owners.size === 0) { this.selectiveReripStats.globalReripCount += 1 this.selectiveReripStats.globalReripReason = !directPath.found @@ -228,7 +228,7 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH | undefined if (repeatedOwnerRouteIds.length > 0) { this.selectiveReripStats.alternateBlockerSearchCount += 1 - alternatePath = this.findRelaxedBlockerPath( + alternatePath = this.findRelaxedBlockerPathPreferringPreservedRoutes( new Set(repeatedOwnerRouteIds), ) if (!alternatePath.found) { @@ -350,6 +350,33 @@ export class SelectiveReripTinyHyperGraphSolver extends OutsideInPartialRipTinyH }) } + protected findRelaxedBlockerPathPreferringPreservedRoutes( + forbiddenOwnerRouteIds: ReadonlySet = new Set(), + ): DistinctOwnerBlockerSearchResult< + RelaxedSearchState, + RouteId, + RelaxedSearchHopData + > { + const preferredPreservedRouteIds = + this.getRouteIdsPreferredForPreservation() + if (preferredPreservedRouteIds.size === 0) { + return this.findRelaxedBlockerPath(forbiddenOwnerRouteIds) + } + + const preferredForbiddenOwnerRouteIds = new Set(forbiddenOwnerRouteIds) + for (const routeId of preferredPreservedRouteIds) { + preferredForbiddenOwnerRouteIds.add(routeId) + } + const preferredPath = this.findRelaxedBlockerPath( + preferredForbiddenOwnerRouteIds, + ) + if (preferredPath.found && preferredPath.owners.size > 0) { + return preferredPath + } + + return this.findRelaxedBlockerPath(forbiddenOwnerRouteIds) + } + protected getRelaxedSearchExpansionLimit(): number { let incidentHopCount = 0 for (const incidentRegions of this.topology.incidentPortRegion) { diff --git a/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts b/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts index 8d1b2b5..294ddb7 100644 --- a/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts +++ b/tests/outside-in-partial-rip-tiny-hypergraph-solver.test.ts @@ -8,6 +8,12 @@ import { import type { PortId, RegionId, RouteId } from "lib/types" class TestOutsideInPartialRipSolver extends OutsideInPartialRipTinyHyperGraphSolver { + preferredPreservedRouteIds = new Set() + + protected override getRouteIdsPreferredForPreservation() { + return this.preferredPreservedRouteIds + } + prepare(hotRegionIds: RegionId[], regionCosts: Float64Array): boolean { return this.preparePartialRip(hotRegionIds, regionCosts) } @@ -86,6 +92,34 @@ test("partial rip preserves both outside route ends", () => { expect(solver.stats.retainedPartialRipSegmentCount).toBe(3) }) +test("partial rip leaves preferred routes unchanged", () => { + const solver = createLinearSolver(24, {}, 2) + solver.preferredPreservedRouteIds.add(0) + for (let regionId = 1; regionId <= 4; regionId++) { + const [routeZeroSegment] = solver.state.regionSegments[regionId]! + solver.state.regionSegments[regionId]!.push([ + 1, + routeZeroSegment![1], + routeZeroSegment![2], + ]) + } + const regionCosts = new Float64Array(6) + regionCosts[3] = 1 + + expect(solver.prepare([3], regionCosts)).toBe(true) + expect(solver.state.unroutedRoutes).toEqual([1]) + expect( + solver.state.regionSegments + .flat() + .filter(([routeId]) => routeId === 0), + ).toEqual([ + [0, 0, 1], + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + ]) +}) + test("a near-target initial solution selects the larger quality window", () => { const nearTargetSolver = createLinearSolver() nearTargetSolver.PARTIAL_RIP_QUALITY_MAX_DISTANCE = 8 diff --git a/tests/selective-rerip-tiny-hyper-graph-solver.test.ts b/tests/selective-rerip-tiny-hyper-graph-solver.test.ts index 86cf46d..d9b6062 100644 --- a/tests/selective-rerip-tiny-hyper-graph-solver.test.ts +++ b/tests/selective-rerip-tiny-hyper-graph-solver.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import { orderRoutesAfterSelectiveRerip, + SelectiveReripTinyHyperGraphSolver, selectOwnerRouteIdsToRip, } from "lib/selective-rerip-tiny-hyper-graph-solver" @@ -32,3 +33,80 @@ test("keeps pending routes ahead of newly ripped routes", () => { }), ).toEqual([7, 3, 8, 9, 4, 2]) }) + +test("prefers another blocker owner over a route marked for preservation", () => { + const searches: number[][] = [] + class SolverWithPreferredPreservedRoute extends SelectiveReripTinyHyperGraphSolver { + protected override getRouteIdsPreferredForPreservation() { + return new Set([1]) + } + + protected override findRelaxedBlockerPath( + forbiddenOwnerRouteIds: ReadonlySet = new Set(), + ) { + searches.push([...forbiddenOwnerRouteIds]) + return { + found: true as const, + states: [], + hops: [], + owners: new Set(forbiddenOwnerRouteIds.has(1) ? [2] : [1]), + distance: 1, + expandedLabelCount: 1, + } + } + + findPreferredBlockerPath() { + return this.findRelaxedBlockerPathPreferringPreservedRoutes() + } + } + const solver = Object.create( + SolverWithPreferredPreservedRoute.prototype, + ) as SolverWithPreferredPreservedRoute + + const result = solver.findPreferredBlockerPath() + + expect(searches).toEqual([[1]]) + expect(result.found && [...result.owners]).toEqual([2]) +}) + +test("rerips a preserved route when no other blocker path exists", () => { + const searches: number[][] = [] + class SolverWithUnavoidablePreservedRoute extends SelectiveReripTinyHyperGraphSolver { + protected override getRouteIdsPreferredForPreservation() { + return new Set([1]) + } + + protected override findRelaxedBlockerPath( + forbiddenOwnerRouteIds: ReadonlySet = new Set(), + ) { + searches.push([...forbiddenOwnerRouteIds]) + if (forbiddenOwnerRouteIds.has(1)) { + return { + found: false as const, + reason: "no_path" as const, + expandedLabelCount: 1, + } + } + return { + found: true as const, + states: [], + hops: [], + owners: new Set([1]), + distance: 1, + expandedLabelCount: 1, + } + } + + findPreferredBlockerPath() { + return this.findRelaxedBlockerPathPreferringPreservedRoutes() + } + } + const solver = Object.create( + SolverWithUnavoidablePreservedRoute.prototype, + ) as SolverWithUnavoidablePreservedRoute + + const result = solver.findPreferredBlockerPath() + + expect(searches).toEqual([[1], []]) + expect(result.found && [...result.owners]).toEqual([1]) +})