Skip to content
Closed
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
52 changes: 49 additions & 3 deletions lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { countNewIntersectionsWithValues } from "./countNewIntersections"
import { MinHeap } from "./MinHeap"
import { shuffle } from "./shuffle"
import { SparseCandidateBestCostTable } from "./sparse-candidate-best-cost-table"
import type { StaticallyUnroutableRouteSummary } from "./static-reachability"
import {
createStaticallyUnroutableRouteSummary,
Expand Down Expand Up @@ -345,6 +346,7 @@ interface SegmentGeometryScratch {
export class TinyHyperGraphSolver extends BaseSolver {
state: TinyHyperGraphWorkingState
private _problemSetup?: TinyHyperGraphProblemSetup
private readonly sparseCandidateBestCostTable?: SparseCandidateBestCostTable
protected routeAttemptCountByRouteId: Uint32Array
protected routeSuccessCountByRouteId: Uint32Array
protected bestSolvedStateSnapshot?: SolvedStateSnapshot
Expand Down Expand Up @@ -383,6 +385,18 @@ export class TinyHyperGraphSolver extends BaseSolver {
) {
super()
applyTinyHyperGraphSolverOptions(this, options)
if (this.USE_SPARSE_CANDIDATE_STORAGE) {
let incidentHopCount = 0
for (const incidentRegions of topology.incidentPortRegion) {
incidentHopCount += incidentRegions.length
}
this.sparseCandidateBestCostTable = new SparseCandidateBestCostTable(
incidentHopCount,
(hopId) => {
this.assertValidIncidentHopId(hopId)
},
)
}
this.state = {
portAssignment: new Int32Array(topology.portCount).fill(-1),
regionSegments: Array.from({ length: topology.regionCount }, () => []),
Expand All @@ -395,10 +409,10 @@ export class TinyHyperGraphSolver extends BaseSolver {
unroutedRoutes: range(problem.routeCount),
candidateQueue: new MinHeap([], compareCandidatesByF),
candidateBestCostByHopId: this.USE_SPARSE_CANDIDATE_STORAGE
? new Map()
? new Float64Array(0)
: new Float64Array(topology.portCount * topology.regionCount),
candidateBestCostGenerationByHopId: this.USE_SPARSE_CANDIDATE_STORAGE
? new Map()
? new Uint32Array(0)
: new Uint32Array(topology.portCount * topology.regionCount),
candidateBestCostGeneration: 1,
goalPortId: -1,
Expand Down Expand Up @@ -608,6 +622,10 @@ export class TinyHyperGraphSolver extends BaseSolver {

resetCandidateBestCosts() {
const { state } = this
if (this.sparseCandidateBestCostTable) {
this.sparseCandidateBestCostTable.reset()
return
}

if (state.candidateBestCostGeneration === 0xffffffff) {
if (state.candidateBestCostByHopId instanceof Map) {
Expand All @@ -626,6 +644,10 @@ export class TinyHyperGraphSolver extends BaseSolver {
}

getCandidateBestCost(hopId: HopId) {
if (this.sparseCandidateBestCostTable) {
return this.sparseCandidateBestCostTable.get(hopId)
}

const { state } = this
const bestCostGeneration = state.candidateBestCostGenerationByHopId

Expand All @@ -639,8 +661,12 @@ export class TinyHyperGraphSolver extends BaseSolver {
}

setCandidateBestCost(hopId: HopId, bestCost: number) {
const { state } = this
if (this.sparseCandidateBestCostTable) {
this.sparseCandidateBestCostTable.set(hopId, bestCost)
return
}

const { state } = this
if (state.candidateBestCostGenerationByHopId instanceof Map) {
state.candidateBestCostGenerationByHopId.set(
hopId,
Expand All @@ -662,6 +688,26 @@ export class TinyHyperGraphSolver extends BaseSolver {
return portId * this.topology.regionCount + nextRegionId
}

private assertValidIncidentHopId(hopId: HopId): void {
const portId = Math.floor(hopId / this.topology.regionCount)
const regionId = hopId - portId * this.topology.regionCount
if (
portId >= this.topology.portCount ||
regionId >= this.topology.regionCount ||
!this.topology.incidentPortRegion[portId]?.includes(regionId)
) {
throw new Error(
"Sparse candidate best-cost table received non-incident hop " +
hopId +
" (port " +
portId +
", region " +
regionId +
")",
)
}
}

getStartingNextRegionId(
routeId: RouteId,
startingPortId: PortId,
Expand Down
111 changes: 111 additions & 0 deletions lib/sparse-candidate-best-cost-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const MAX_GENERATION = 0xffffffff
const MIN_CAPACITY = 2

type ValidateHopId = (hopId: number) => void

export class SparseCandidateBestCostTable {
private readonly keys: Float64Array
private readonly occupied: Uint8Array
private readonly generations: Uint32Array
private readonly costs: Float64Array
private readonly mask: number
private generation = 1
private entryCount = 0

constructor(
private readonly maxEntryCount: number,
private readonly validateHopId: ValidateHopId,
) {
if (!Number.isSafeInteger(maxEntryCount) || maxEntryCount < 0) {
throw new Error(
"Sparse candidate best-cost table requires a non-negative safe entry count, received " +
maxEntryCount,
)
}

let capacity = MIN_CAPACITY
const minimumCapacity = Math.max(MIN_CAPACITY, maxEntryCount * 2)
while (capacity < minimumCapacity) capacity *= 2

this.keys = new Float64Array(capacity)
this.occupied = new Uint8Array(capacity)
this.generations = new Uint32Array(capacity)
this.costs = new Float64Array(capacity)
this.mask = capacity - 1
}

get size(): number {
return this.entryCount
}

reset(): void {
if (this.generation === MAX_GENERATION) {
this.generations.fill(0)
this.generation = 1
return
}

this.generation += 1
}

get(hopId: number): number {
const slot = this.findSlot(hopId)
if (slot === undefined || this.generations[slot] !== this.generation) {
return Number.POSITIVE_INFINITY
}

return this.costs[slot]!
}

set(hopId: number, cost: number): void {
if (!Number.isSafeInteger(hopId) || hopId < 0) {
throw new Error(
"Sparse candidate best-cost table received invalid hop id " + hopId,
)
}

const existingSlot = this.findSlot(hopId)
if (existingSlot !== undefined) {
this.generations[existingSlot] = this.generation
this.costs[existingSlot] = cost
return
}

if (this.entryCount >= this.maxEntryCount) {
throw new Error(
"Sparse candidate best-cost table exhausted its " +
this.maxEntryCount +
" legal hop entries while inserting hop " +
hopId,
)
}

this.validateHopId(hopId)
let slot = this.hash(hopId)
while (this.occupied[slot] !== 0) {
slot = (slot + 1) & this.mask
}

this.keys[slot] = hopId
this.occupied[slot] = 1
this.generations[slot] = this.generation
this.costs[slot] = cost
this.entryCount += 1
}

private findSlot(hopId: number): number | undefined {
let slot = this.hash(hopId)
while (this.occupied[slot] !== 0) {
if (this.keys[slot] === hopId) return slot
slot = (slot + 1) & this.mask
}

return undefined
}

private hash(hopId: number): number {
const lowBits = hopId >>> 0
const highBits = Math.floor(hopId / 0x100000000) >>> 0
return Math.imul(lowBits ^ highBits, 0x9e3779b1) & this.mask
}
}
70 changes: 70 additions & 0 deletions tests/solver/sparse-candidate-storage-equivalence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, test } from "bun:test"
import {
createEmptyRegionIntersectionCache,
type TinyHyperGraphProblem,
TinyHyperGraphSolver,
type TinyHyperGraphTopology,
} from "lib/index"

const createSolver = (useSparseCandidateStorage: boolean) => {
const topology: TinyHyperGraphTopology = {
portCount: 2,
regionCount: 3,
regionIncidentPorts: [[0], [0, 1], [1]],
incidentPortRegion: [
[1, 0],
[1, 2],
],
regionWidth: new Float64Array(3).fill(10),
regionHeight: new Float64Array(3).fill(10),
regionCenterX: new Float64Array([0, 1, 2]),
regionCenterY: new Float64Array(3),
portAngleForRegion1: new Int32Array(2),
portAngleForRegion2: new Int32Array(2),
portX: new Float64Array([0, 1]),
portY: new Float64Array(2),
portZ: new Int32Array(2),
}
const problem: TinyHyperGraphProblem = {
routeCount: 1,
portSectionMask: new Int8Array(2).fill(1),
routeStartPort: Int32Array.from([0]),
routeEndPort: Int32Array.from([1]),
routeNet: Int32Array.from([0]),
regionNetId: new Int32Array(3).fill(-1),
}
const solver = new TinyHyperGraphSolver(topology, problem, {
USE_SPARSE_CANDIDATE_STORAGE: useSparseCandidateStorage,
})

solver.state.unroutedRoutes = []
solver.state.portAssignment.set([0, 0])
solver.state.regionSegments[1] = [[0, 0, 1]]
const congestedCache = createEmptyRegionIntersectionCache()
congestedCache.existingRegionCost = 0.5
solver.state.regionIntersectionCaches[1] = congestedCache
solver.solve()

return {
solved: solver.solved,
failed: solver.failed,
error: solver.error,
iterations: solver.iterations,
ripCount: solver.state.ripCount,
portAssignment: Array.from(solver.state.portAssignment),
regionSegments: solver.state.regionSegments,
unroutedRoutes: solver.state.unroutedRoutes,
stats: solver.stats,
}
}

test("sparse and dense candidate storage stay equivalent across a rerip", () => {
const denseResult = createSolver(false)
const sparseResult = createSolver(true)

expect(sparseResult).toEqual(denseResult)
expect(sparseResult.solved).toBe(true)
expect(sparseResult.failed).toBe(false)
expect(sparseResult.ripCount).toBe(1)
expect(sparseResult.regionSegments[1]).toEqual([[0, 0, 1]])
})
43 changes: 43 additions & 0 deletions tests/sparse-candidate-best-cost-table.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, test } from "bun:test"
import { SparseCandidateBestCostTable } from "lib/sparse-candidate-best-cost-table"

test("stores bounded generation-scoped costs across colliding hop ids", () => {
const validatedHopIds: number[] = []
const table = new SparseCandidateBestCostTable(4, (hopId) => {
validatedHopIds.push(hopId)
})

table.set(0, 8)
table.set(8, 4)
table.set(16, 2)

expect(table.size).toBe(3)
expect(table.get(0)).toBe(8)
expect(table.get(8)).toBe(4)
expect(table.get(16)).toBe(2)

table.set(8, 3)
expect(table.get(8)).toBe(3)
expect(validatedHopIds).toEqual([0, 8, 16])

table.reset()
expect(table.get(0)).toBe(Number.POSITIVE_INFINITY)
expect(table.get(8)).toBe(Number.POSITIVE_INFINITY)

table.set(8, 1)
table.set(24, 6)
expect(table.size).toBe(4)
expect(table.get(8)).toBe(1)
expect(table.get(24)).toBe(6)
expect(validatedHopIds).toEqual([0, 8, 16, 24])

expect(() => table.set(32, 5)).toThrow(
"Sparse candidate best-cost table exhausted its 4 legal hop entries",
)

const invalidTable = new SparseCandidateBestCostTable(1, (hopId) => {
throw new Error("invalid incident hop " + hopId)
})
expect(() => invalidTable.set(1, 1)).toThrow("invalid incident hop 1")
expect(() => invalidTable.set(-1, 1)).toThrow("invalid hop id -1")
})
Loading