diff --git a/lib/compat/convertToSerializedHyperGraph.ts b/lib/compat/convertToSerializedHyperGraph.ts index 274cdb7..345f310 100644 --- a/lib/compat/convertToSerializedHyperGraph.ts +++ b/lib/compat/convertToSerializedHyperGraph.ts @@ -129,6 +129,16 @@ const getSerializedPortData = ( data.z = solver.topology.portZ[portId] } + if (typeof data.angleForRegion1 !== "number") { + data.angleForRegion1 = solver.topology.portAngleForRegion1[portId] + } + + if (typeof data.angleForRegion2 !== "number") { + data.angleForRegion2 = + solver.topology.portAngleForRegion2?.[portId] ?? + solver.topology.portAngleForRegion1[portId] + } + return data } diff --git a/lib/compat/loadSerializedHyperGraph.ts b/lib/compat/loadSerializedHyperGraph.ts index 7126ab0..d0401b4 100644 --- a/lib/compat/loadSerializedHyperGraph.ts +++ b/lib/compat/loadSerializedHyperGraph.ts @@ -351,14 +351,14 @@ export const loadSerializedHyperGraph = ( portX[portIndex] = Number(port.d?.x ?? 0) portY[portIndex] = Number(port.d?.y ?? 0) portZ[portIndex] = Number(port.d?.z ?? 0) - portAngleForRegion1[portIndex] = computePortAngle( - port, - filteredHyperGraph.regions[region1Index], - ) - portAngleForRegion2[portIndex] = computePortAngle( - port, - filteredHyperGraph.regions[region2Index], - ) + portAngleForRegion1[portIndex] = + typeof port.d?.angleForRegion1 === "number" + ? Number(port.d.angleForRegion1) + : computePortAngle(port, filteredHyperGraph.regions[region1Index]) + portAngleForRegion2[portIndex] = + typeof port.d?.angleForRegion2 === "number" + ? Number(port.d.angleForRegion2) + : computePortAngle(port, filteredHyperGraph.regions[region2Index]) }) const connections = filteredHyperGraph.connections ?? [] diff --git a/lib/core.ts b/lib/core.ts index 650cae5..b32e982 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -2,16 +2,25 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { convertToSerializedHyperGraph } from "./compat/convertToSerializedHyperGraph" import { computeRegionCost, isKnownSingleLayerMask } from "./computeRegionCost" -import { countNewIntersectionsWithValues } from "./countNewIntersections" +import { + countNewIntersectionsWithValues, + doAngleIntervalsCross, +} from "./countNewIntersections" +import { + computePenaltyPointContribution, + getLineSegmentIntersectionPoint, +} from "./intersectionPenalty" import { MinHeap } from "./MinHeap" import { shuffle } from "./shuffle" import type { DynamicAnglePair, HopId, + IntersectionPenaltyPoint, NetId, PortId, RegionId, RegionIntersectionCache, + RipCongestionMode, RouteId, } from "./types" import { range } from "./utils" @@ -23,6 +32,8 @@ export const createEmptyRegionIntersectionCache = lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + fromPortIds: new Int32Array(0), + toPortIds: new Int32Array(0), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -147,6 +158,12 @@ export interface TinyHyperGraphWorkingState { /** regionCongestionCost[regionId] = congestion cost */ regionCongestionCost: Float64Array + /** portCongestionCost[portId] = congestion cost contributed by penalty points */ + portCongestionCost: Float64Array + /** Persistent intersection penalty points accumulated across rerips. */ + intersectionPenaltyPoints: IntersectionPenaltyPoint[] + /** Penalty points discovered in the current routed attempt. */ + pendingIntersectionPenaltyPoints: IntersectionPenaltyPoint[] } export interface TinyHyperGraphSolverOptions { @@ -155,6 +172,10 @@ export interface TinyHyperGraphSolverOptions { RIP_THRESHOLD_END?: number RIP_THRESHOLD_RAMP_ATTEMPTS?: number RIP_CONGESTION_REGION_COST_FACTOR?: number + RIP_CONGESTION_MODE?: RipCongestionMode + INTERSECTION_PENALTY_POINT_RADIUS?: number + INTERSECTION_PENALTY_POINT_FALLOFF?: number + INTERSECTION_PENALTY_POINT_MAGNITUDE?: number MAX_ITERATIONS?: number } @@ -164,6 +185,10 @@ export interface TinyHyperGraphSolverOptionTarget { RIP_THRESHOLD_END: number RIP_THRESHOLD_RAMP_ATTEMPTS: number RIP_CONGESTION_REGION_COST_FACTOR: number + RIP_CONGESTION_MODE: RipCongestionMode + INTERSECTION_PENALTY_POINT_RADIUS: number + INTERSECTION_PENALTY_POINT_FALLOFF: number + INTERSECTION_PENALTY_POINT_MAGNITUDE: number MAX_ITERATIONS: number } @@ -191,6 +216,21 @@ export const applyTinyHyperGraphSolverOptions = ( solver.RIP_CONGESTION_REGION_COST_FACTOR = options.RIP_CONGESTION_REGION_COST_FACTOR } + if (options.RIP_CONGESTION_MODE !== undefined) { + solver.RIP_CONGESTION_MODE = options.RIP_CONGESTION_MODE + } + if (options.INTERSECTION_PENALTY_POINT_RADIUS !== undefined) { + solver.INTERSECTION_PENALTY_POINT_RADIUS = + options.INTERSECTION_PENALTY_POINT_RADIUS + } + if (options.INTERSECTION_PENALTY_POINT_FALLOFF !== undefined) { + solver.INTERSECTION_PENALTY_POINT_FALLOFF = + options.INTERSECTION_PENALTY_POINT_FALLOFF + } + if (options.INTERSECTION_PENALTY_POINT_MAGNITUDE !== undefined) { + solver.INTERSECTION_PENALTY_POINT_MAGNITUDE = + options.INTERSECTION_PENALTY_POINT_MAGNITUDE + } if (options.MAX_ITERATIONS !== undefined) { solver.MAX_ITERATIONS = options.MAX_ITERATIONS } @@ -204,6 +244,11 @@ export const getTinyHyperGraphSolverOptions = ( RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, RIP_THRESHOLD_RAMP_ATTEMPTS: solver.RIP_THRESHOLD_RAMP_ATTEMPTS, RIP_CONGESTION_REGION_COST_FACTOR: solver.RIP_CONGESTION_REGION_COST_FACTOR, + RIP_CONGESTION_MODE: solver.RIP_CONGESTION_MODE, + INTERSECTION_PENALTY_POINT_RADIUS: solver.INTERSECTION_PENALTY_POINT_RADIUS, + INTERSECTION_PENALTY_POINT_FALLOFF: solver.INTERSECTION_PENALTY_POINT_FALLOFF, + INTERSECTION_PENALTY_POINT_MAGNITUDE: + solver.INTERSECTION_PENALTY_POINT_MAGNITUDE, MAX_ITERATIONS: solver.MAX_ITERATIONS, }) @@ -234,9 +279,15 @@ export class TinyHyperGraphSolver extends BaseSolver { RIP_THRESHOLD_RAMP_ATTEMPTS = 50 RIP_CONGESTION_REGION_COST_FACTOR = 0.1 + RIP_CONGESTION_MODE: RipCongestionMode = "region" + INTERSECTION_PENALTY_POINT_RADIUS = 0.8 + INTERSECTION_PENALTY_POINT_FALLOFF = 1.5 + INTERSECTION_PENALTY_POINT_MAGNITUDE = 0.2 override MAX_ITERATIONS = 1e6 + recordIntersectionPenaltyPoints = true + constructor( public topology: TinyHyperGraphTopology, public problem: TinyHyperGraphProblem, @@ -265,6 +316,9 @@ export class TinyHyperGraphSolver extends BaseSolver { goalPortId: -1, ripCount: 0, regionCongestionCost: new Float64Array(topology.regionCount).fill(0), + portCongestionCost: new Float64Array(topology.portCount).fill(0), + intersectionPenaltyPoints: [], + pendingIntersectionPenaltyPoints: [], } } @@ -394,6 +448,9 @@ export class TinyHyperGraphSolver extends BaseSolver { continue } if (neighborPortId === currentCandidate.portId) continue + if (this.candidatePathContainsPort(currentCandidate, neighborPortId)) { + continue + } if (problem.portSectionMask[neighborPortId] === 0) continue const g = this.computeG(currentCandidate, neighborPortId) @@ -488,6 +545,23 @@ export class TinyHyperGraphSolver extends BaseSolver { ) } + candidatePathContainsPort( + candidate: Candidate | undefined, + portId: PortId, + ): boolean { + let cursor = candidate + + while (cursor) { + if (cursor.portId === portId) { + return true + } + + cursor = cursor.prevCandidate + } + + return false + } + isPortReservedForDifferentNet(portId: PortId): boolean { const reservedNetIds = this.problemSetup.portEndpointNetIds[portId] if (!reservedNetIds) { @@ -511,7 +585,8 @@ export class TinyHyperGraphSolver extends BaseSolver { } isKnownSingleLayerRegion(regionId: RegionId): boolean { - const regionAvailableZMask = this.topology.regionAvailableZMask?.[regionId] ?? 0 + const regionAvailableZMask = + this.topology.regionAvailableZMask?.[regionId] ?? 0 return isKnownSingleLayerMask(regionAvailableZMask) } @@ -525,15 +600,17 @@ export class TinyHyperGraphSolver extends BaseSolver { const port1IncidentRegions = topology.incidentPortRegion[port1Id] const port2IncidentRegions = topology.incidentPortRegion[port2Id] const angle1 = - port1IncidentRegions[0] === regionId || port1IncidentRegions[1] !== regionId + port1IncidentRegions[0] === regionId || + port1IncidentRegions[1] !== regionId ? topology.portAngleForRegion1[port1Id] - : topology.portAngleForRegion2?.[port1Id] ?? - topology.portAngleForRegion1[port1Id] + : (topology.portAngleForRegion2?.[port1Id] ?? + topology.portAngleForRegion1[port1Id]) const angle2 = - port2IncidentRegions[0] === regionId || port2IncidentRegions[1] !== regionId + port2IncidentRegions[0] === regionId || + port2IncidentRegions[1] !== regionId ? topology.portAngleForRegion1[port2Id] - : topology.portAngleForRegion2?.[port2Id] ?? - topology.portAngleForRegion1[port2Id] + : (topology.portAngleForRegion2?.[port2Id] ?? + topology.portAngleForRegion1[port2Id]) const z1 = topology.portZ[port1Id] const z2 = topology.portZ[port2Id] scratch.lesserAngle = angle1 < angle2 ? angle1 : angle2 @@ -544,6 +621,153 @@ export class TinyHyperGraphSolver extends BaseSolver { return scratch } + createIntersectionPenaltyPoint( + regionId: RegionId, + port1Id: PortId, + port2Id: PortId, + otherPort1Id: PortId, + otherPort2Id: PortId, + sameLayerIntersection: boolean, + ): IntersectionPenaltyPoint | null { + const radius = this.INTERSECTION_PENALTY_POINT_RADIUS + const baseMagnitude = this.INTERSECTION_PENALTY_POINT_MAGNITUDE + + if (radius <= 0 || baseMagnitude <= 0) { + return null + } + + const { topology } = this + const intersectionPoint = getLineSegmentIntersectionPoint( + topology.portX[port1Id], + topology.portY[port1Id], + topology.portX[port2Id], + topology.portY[port2Id], + topology.portX[otherPort1Id], + topology.portY[otherPort1Id], + topology.portX[otherPort2Id], + topology.portY[otherPort2Id], + ) ?? { + x: topology.regionCenterX[regionId], + y: topology.regionCenterY[regionId], + } + + return { + x: intersectionPoint.x, + y: intersectionPoint.y, + magnitude: sameLayerIntersection ? baseMagnitude : baseMagnitude * 0.5, + radius, + falloff: this.INTERSECTION_PENALTY_POINT_FALLOFF, + } + } + + recordPendingIntersectionPenaltyPoints( + regionId: RegionId, + port1Id: PortId, + port2Id: PortId, + lesserAngle: number, + greaterAngle: number, + layerMask: number, + regionCache: RegionIntersectionCache, + ) { + if ( + !this.recordIntersectionPenaltyPoints || + this.RIP_CONGESTION_MODE !== "penalty-points" + ) { + return + } + + const currentRouteNetId = this.state.currentRouteNetId + + if (currentRouteNetId === undefined) { + return + } + + for ( + let segmentIndex = 0; + segmentIndex < regionCache.netIds.length; + segmentIndex++ + ) { + if (regionCache.netIds[segmentIndex] === currentRouteNetId) { + continue + } + + if ( + !doAngleIntervalsCross( + lesserAngle, + greaterAngle, + regionCache.lesserAngles[segmentIndex]!, + regionCache.greaterAngles[segmentIndex]!, + ) + ) { + continue + } + + const penaltyPoint = this.createIntersectionPenaltyPoint( + regionId, + port1Id, + port2Id, + regionCache.fromPortIds[segmentIndex]!, + regionCache.toPortIds[segmentIndex]!, + (layerMask & regionCache.layerMasks[segmentIndex]!) !== 0, + ) + + if (penaltyPoint) { + this.state.pendingIntersectionPenaltyPoints.push(penaltyPoint) + } + } + } + + addPenaltyPointToPortCongestion(point: IntersectionPenaltyPoint) { + const { topology, state } = this + const radiusSquared = point.radius * point.radius + + for (let portId = 0; portId < topology.portCount; portId++) { + const dx = topology.portX[portId] - point.x + if (Math.abs(dx) >= point.radius) { + continue + } + + const dy = topology.portY[portId] - point.y + if (Math.abs(dy) >= point.radius) { + continue + } + + const distanceSquared = dx * dx + dy * dy + if (distanceSquared >= radiusSquared) { + continue + } + + state.portCongestionCost[portId] += computePenaltyPointContribution( + Math.sqrt(distanceSquared), + point.radius, + point.magnitude, + point.falloff, + ) + } + } + + applyPendingIntersectionPenaltyPoints() { + const { state } = this + + if (this.RIP_CONGESTION_MODE !== "penalty-points") { + state.pendingIntersectionPenaltyPoints = [] + return + } + + for (const penaltyPoint of state.pendingIntersectionPenaltyPoints) { + state.intersectionPenaltyPoints.push(penaltyPoint) + this.addPenaltyPointToPortCongestion(penaltyPoint) + } + + state.pendingIntersectionPenaltyPoints = [] + } + + getRipCongestionCost(nextRegionId: RegionId, neighborPortId: PortId) { + return this.RIP_CONGESTION_MODE === "penalty-points" + ? this.state.portCongestionCost[neighborPortId]! + : this.state.regionCongestionCost[nextRegionId]! + } + appendSegmentToRegionCache( regionId: RegionId, port1Id: PortId, @@ -568,6 +792,15 @@ export class TinyHyperGraphSolver extends BaseSolver { segmentGeometry.layerMask, segmentGeometry.entryExitLayerChanges, ) + this.recordPendingIntersectionPenaltyPoints( + regionId, + port1Id, + port2Id, + segmentGeometry.lesserAngle, + segmentGeometry.greaterAngle, + segmentGeometry.layerMask, + regionCache, + ) const nextLength = regionCache.netIds.length + 1 const netIds = new Int32Array(nextLength) @@ -586,6 +819,14 @@ export class TinyHyperGraphSolver extends BaseSolver { layerMasks.set(regionCache.layerMasks) layerMasks[nextLength - 1] = segmentGeometry.layerMask + const fromPortIds = new Int32Array(nextLength) + fromPortIds.set(regionCache.fromPortIds) + fromPortIds[nextLength - 1] = port1Id + + const toPortIds = new Int32Array(nextLength) + toPortIds.set(regionCache.toPortIds) + toPortIds[nextLength - 1] = port2Id + const existingSameLayerIntersections = regionCache.existingSameLayerIntersections + newSameLayerIntersections const existingCrossingLayerIntersections = @@ -600,6 +841,8 @@ export class TinyHyperGraphSolver extends BaseSolver { lesserAngles, greaterAngles, layerMasks, + fromPortIds, + toPortIds, existingSameLayerIntersections, existingCrossingLayerIntersections, existingEntryExitLayerChanges, @@ -674,6 +917,7 @@ export class TinyHyperGraphSolver extends BaseSolver { state.candidateQueue.clear() this.resetCandidateBestCosts() state.goalPortId = -1 + state.pendingIntersectionPenaltyPoints = [] } onAllRoutesRouted() { @@ -717,9 +961,13 @@ export class TinyHyperGraphSolver extends BaseSolver { return } - for (let regionId = 0; regionId < topology.regionCount; regionId++) { - state.regionCongestionCost[regionId] += - regionCosts[regionId] * this.RIP_CONGESTION_REGION_COST_FACTOR + if (this.RIP_CONGESTION_MODE === "penalty-points") { + this.applyPendingIntersectionPenaltyPoints() + } else { + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + state.regionCongestionCost[regionId] += + regionCosts[regionId] * this.RIP_CONGESTION_REGION_COST_FACTOR + } } state.ripCount += 1 @@ -734,11 +982,15 @@ export class TinyHyperGraphSolver extends BaseSolver { onOutOfCandidates() { const { topology, state } = this - for (let regionId = 0; regionId < topology.regionCount; regionId++) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 - state.regionCongestionCost[regionId] += - regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR + if (this.RIP_CONGESTION_MODE === "penalty-points") { + this.applyPendingIntersectionPenaltyPoints() + } else { + for (let regionId = 0; regionId < topology.regionCount; regionId++) { + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + state.regionCongestionCost[regionId] += + regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR + } } state.ripCount += 1 @@ -821,7 +1073,7 @@ export class TinyHyperGraphSolver extends BaseSolver { return ( currentCandidate.g + newRegionCost + - state.regionCongestionCost[nextRegionId] + this.getRipCongestionCost(nextRegionId, neighborPortId) ) } diff --git a/lib/countNewIntersections.ts b/lib/countNewIntersections.ts index 355f8e2..2c5b7c8 100644 --- a/lib/countNewIntersections.ts +++ b/lib/countNewIntersections.ts @@ -1,5 +1,28 @@ import type { DynamicAnglePair, DynamicAnglePairArrays } from "./types" +export const doAngleIntervalsCross = ( + leftLesserAngle: number, + leftGreaterAngle: number, + rightLesserAngle: number, + rightGreaterAngle: number, +) => { + const rightLesserAngleIsInsideLeftInterval = + leftLesserAngle < rightLesserAngle && rightLesserAngle < leftGreaterAngle + const rightGreaterAngleIsInsideLeftInterval = + leftLesserAngle < rightGreaterAngle && rightGreaterAngle < leftGreaterAngle + const leftLesserAngleIsInsideRightInterval = + rightLesserAngle < leftLesserAngle && leftLesserAngle < rightGreaterAngle + const leftGreaterAngleIsInsideRightInterval = + rightLesserAngle < leftGreaterAngle && leftGreaterAngle < rightGreaterAngle + + return ( + rightLesserAngleIsInsideLeftInterval !== + rightGreaterAngleIsInsideLeftInterval && + leftLesserAngleIsInsideRightInterval !== + leftGreaterAngleIsInsideRightInterval + ) +} + export const createDynamicAnglePairArrays = ( anglePairs: Array, ): DynamicAnglePairArrays => { @@ -40,12 +63,16 @@ export const countNewIntersectionsWithValues = ( for (let i = 0; i < netIds.length; i++) { if (newNet === netIds[i]) continue - const lesserAngleIsInsideInterval = - newLesserAngle < lesserAngles[i] && lesserAngles[i] < newGreaterAngle - const greaterAngleIsInsideInterval = - newLesserAngle < greaterAngles[i] && greaterAngles[i] < newGreaterAngle - - if (lesserAngleIsInsideInterval === greaterAngleIsInsideInterval) continue + if ( + !doAngleIntervalsCross( + newLesserAngle, + newGreaterAngle, + lesserAngles[i]!, + greaterAngles[i]!, + ) + ) { + continue + } if ((newLayerMask & layerMasks[i]) !== 0) { sameLayerIntersectionCount++ diff --git a/lib/intersectionPenalty.ts b/lib/intersectionPenalty.ts new file mode 100644 index 0000000..a1c90c9 --- /dev/null +++ b/lib/intersectionPenalty.ts @@ -0,0 +1,52 @@ +const EPSILON = 1e-9 + +export const computePenaltyPointContribution = ( + distance: number, + radius: number, + magnitude: number, + falloff: number, +) => { + if (radius <= 0 || magnitude === 0 || distance >= radius) { + return 0 + } + + const clampedFalloff = Math.max(falloff, EPSILON) + const normalizedDistance = Math.min(Math.max(distance / radius, 0), 1) + + return magnitude * Math.pow(1 - normalizedDistance, clampedFalloff) +} + +export const getLineSegmentIntersectionPoint = ( + ax1: number, + ay1: number, + ax2: number, + ay2: number, + bx1: number, + by1: number, + bx2: number, + by2: number, +): { x: number; y: number } | null => { + const aDx = ax2 - ax1 + const aDy = ay2 - ay1 + const bDx = bx2 - bx1 + const bDy = by2 - by1 + const denominator = aDx * bDy - aDy * bDx + + if (Math.abs(denominator) <= EPSILON) { + return null + } + + const deltaX = bx1 - ax1 + const deltaY = by1 - ay1 + const t = (deltaX * bDy - deltaY * bDx) / denominator + const u = (deltaX * aDy - deltaY * aDx) / denominator + + if (t <= EPSILON || t >= 1 - EPSILON || u <= EPSILON || u >= 1 - EPSILON) { + return null + } + + return { + x: ax1 + aDx * t, + y: ay1 + aDy * t, + } +} diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index fdc3ce5..34dfdb1 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -66,6 +66,10 @@ const DEFAULT_SECTION_SOLVER_OPTIONS: TinyHyperGraphSectionSolverOptions = { DISTANCE_TO_COST: 0.05, RIP_THRESHOLD_RAMP_ATTEMPTS: 16, RIP_CONGESTION_REGION_COST_FACTOR: 0.1, + RIP_CONGESTION_MODE: "penalty-points", + INTERSECTION_PENALTY_POINT_RADIUS: 1.4, + INTERSECTION_PENALTY_POINT_FALLOFF: 2, + INTERSECTION_PENALTY_POINT_MAGNITUDE: 0.4, MAX_ITERATIONS: 1e6, MAX_RIPS_WITHOUT_MAX_REGION_COST_IMPROVEMENT: 6, EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST: Number.POSITIVE_INFINITY, diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index e92c053..6704501 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -16,6 +16,7 @@ import type { PortId, RegionId, RegionIntersectionCache, + RipCongestionMode, RouteId, } from "../types" import { visualizeTinyGraph } from "../visualizeTinyGraph" @@ -52,9 +53,7 @@ export interface TinyHyperGraphSectionSolverOptions } const applyTinyHyperGraphSectionSolverOptions = ( - solver: - | TinyHyperGraphSectionSearchSolver - | TinyHyperGraphSectionSolver, + solver: TinyHyperGraphSectionSearchSolver | TinyHyperGraphSectionSolver, options?: TinyHyperGraphSectionSolverOptions, ) => { applyTinyHyperGraphSolverOptions(solver, options) @@ -104,6 +103,8 @@ const cloneRegionIntersectionCache = ( lesserAngles: new Int32Array(regionIntersectionCache.lesserAngles), greaterAngles: new Int32Array(regionIntersectionCache.greaterAngles), layerMasks: new Int32Array(regionIntersectionCache.layerMasks), + fromPortIds: new Int32Array(regionIntersectionCache.fromPortIds), + toPortIds: new Int32Array(regionIntersectionCache.toPortIds), existingCrossingLayerIntersections: regionIntersectionCache.existingCrossingLayerIntersections, existingSameLayerIntersections: @@ -131,7 +132,8 @@ const restoreSolvedStateSnapshot = ( const clonedSnapshot = cloneSolvedStateSnapshot(snapshot) solver.state.portAssignment = clonedSnapshot.portAssignment solver.state.regionSegments = clonedSnapshot.regionSegments - solver.state.regionIntersectionCaches = clonedSnapshot.regionIntersectionCaches + solver.state.regionIntersectionCaches = + clonedSnapshot.regionIntersectionCaches } const summarizeRegionIntersectionCaches = ( @@ -245,7 +247,8 @@ const getOrderedRoutePath = ( orderedRegionIds: RegionId[] } => { const routeSegments = solution.solvedRoutePathSegments[routeId] ?? [] - const routeSegmentRegionIds = solution.solvedRoutePathRegionIds?.[routeId] ?? [] + const routeSegmentRegionIds = + solution.solvedRoutePathRegionIds?.[routeId] ?? [] const startPortId = problem.routeStartPort[routeId] const endPortId = problem.routeEndPort[routeId] @@ -363,6 +366,9 @@ const applyRouteSegmentsToSolver = ( solver.state.goalPortId = -1 solver.state.ripCount = 0 solver.state.regionCongestionCost.fill(0) + solver.state.portCongestionCost.fill(0) + solver.state.intersectionPenaltyPoints = [] + solver.state.pendingIntersectionPenaltyPoints = [] for (let regionId = 0; regionId < routeSegmentsByRegion.length; regionId++) { for (const [routeId, fromPortId, toPortId] of routeSegmentsByRegion[ @@ -382,6 +388,7 @@ const applyRouteSegmentsToSolver = ( solver.state.currentRouteId = undefined solver.state.currentRouteNetId = undefined + solver.state.pendingIntersectionPenaltyPoints = [] solver.solved = true solver.failed = false solver.error = null @@ -615,8 +622,15 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { } applyFixedSegments() { + const previousTrackingState = this.recordIntersectionPenaltyPoints + this.recordIntersectionPenaltyPoints = false + for (const routePlan of this.routePlans) { - for (const { regionId, fromPortId, toPortId } of routePlan.fixedSegments) { + for (const { + regionId, + fromPortId, + toPortId, + } of routePlan.fixedSegments) { this.state.currentRouteNetId = this.problem.routeNet[routePlan.routeId] this.state.regionSegments[regionId]!.push([ routePlan.routeId, @@ -629,8 +643,10 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { } } + this.recordIntersectionPenaltyPoints = previousTrackingState this.state.currentRouteId = undefined this.state.currentRouteNetId = undefined + this.state.pendingIntersectionPenaltyPoints = [] } captureBestState(summary: RegionCostSummary) { @@ -696,6 +712,7 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { this.state.candidateQueue.clear() this.resetCandidateBestCosts() this.state.goalPortId = -1 + this.state.pendingIntersectionPenaltyPoints = [] } override onAllRoutesRouted() { @@ -789,15 +806,19 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { return } - for ( - let mutableRegionIndex = 0; - mutableRegionIndex < this.mutableRegionIds.length; - mutableRegionIndex++ - ) { - const regionId = this.mutableRegionIds[mutableRegionIndex]! - state.regionCongestionCost[regionId] += - mutableRegionCosts[mutableRegionIndex]! * - this.RIP_CONGESTION_REGION_COST_FACTOR + if (this.RIP_CONGESTION_MODE === "penalty-points") { + this.applyPendingIntersectionPenaltyPoints() + } else { + for ( + let mutableRegionIndex = 0; + mutableRegionIndex < this.mutableRegionIds.length; + mutableRegionIndex++ + ) { + const regionId = this.mutableRegionIds[mutableRegionIndex]! + state.regionCongestionCost[regionId] += + mutableRegionCosts[mutableRegionIndex]! * + this.RIP_CONGESTION_REGION_COST_FACTOR + } } state.ripCount += 1 @@ -812,11 +833,15 @@ class TinyHyperGraphSectionSearchSolver extends TinyHyperGraphSolver { override onOutOfCandidates() { const { state } = this - for (const regionId of this.mutableRegionIds) { - const regionCost = - state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 - state.regionCongestionCost[regionId] += - regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR + if (this.RIP_CONGESTION_MODE === "penalty-points") { + this.applyPendingIntersectionPenaltyPoints() + } else { + for (const regionId of this.mutableRegionIds) { + const regionCost = + state.regionIntersectionCaches[regionId]?.existingRegionCost ?? 0 + state.regionCongestionCost[regionId] += + regionCost * this.RIP_CONGESTION_REGION_COST_FACTOR + } } state.ripCount += 1 @@ -866,6 +891,10 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { EXTRA_RIPS_AFTER_BEATING_BASELINE_MAX_REGION_COST = 10 RIP_CONGESTION_REGION_COST_FACTOR = 0.1 + RIP_CONGESTION_MODE: RipCongestionMode = "penalty-points" + INTERSECTION_PENALTY_POINT_RADIUS = 1.4 + INTERSECTION_PENALTY_POINT_FALLOFF = 2 + INTERSECTION_PENALTY_POINT_MAGNITUDE = 0.4 override MAX_ITERATIONS = 1e6 @@ -943,7 +972,8 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { this.stats = { ...this.stats, sectionBaselineMaxRegionCost: this.sectionBaselineSummary.maxRegionCost, - sectionBaselineTotalRegionCost: this.sectionBaselineSummary.totalRegionCost, + sectionBaselineTotalRegionCost: + this.sectionBaselineSummary.totalRegionCost, effectiveRipThresholdStart: this.RIP_THRESHOLD_START, effectiveRipThresholdEnd: this.RIP_THRESHOLD_END, effectiveMaxRips: this.MAX_RIPS, diff --git a/lib/types.ts b/lib/types.ts index b109000..6f3da52 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -15,6 +15,7 @@ export type LesserAngle = number export type Z1 = number export type GreaterAngle = number export type Z2 = number +export type RipCongestionMode = "region" | "penalty-points" export type DynamicAnglePair = [NetId, LesserAngle, Z1, GreaterAngle, Z2] export interface DynamicAnglePairArrays { @@ -25,6 +26,8 @@ export interface DynamicAnglePairArrays { } export interface RegionIntersectionCache extends DynamicAnglePairArrays { + fromPortIds: Int32Array + toPortIds: Int32Array existingSameLayerIntersections: Integer existingCrossingLayerIntersections: Integer existingEntryExitLayerChanges: Integer @@ -32,6 +35,14 @@ export interface RegionIntersectionCache extends DynamicAnglePairArrays { existingSegmentCount: number } +export interface IntersectionPenaltyPoint { + x: number + y: number + magnitude: number + radius: number + falloff: number +} + export type SameLayerIntersectionCount = number export type CrossingLayerIntersectionCount = number export type EntryExitLayerChanges = number diff --git a/package.json b/package.json index d74a545..21dd209 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "format:check": "biome format .", "typecheck": "tsc -p tsconfig.json --pretty false", "benchmark1": "bun run --cpu-prof-md scripts/profiling/hg07-first10.ts", + "benchmark:sweep:intersection-penalties": "bun run scripts/benchmarking/sweep-intersection-penalties.ts", "benchmark:section": "bun run scripts/benchmarking/hg07-section-pipeline.ts", "benchmark:section:profile": "bun run scripts/benchmarking/hg07-first40-section-profile.ts", "benchmark:port-point-pathing": "bun run scripts/benchmarking/port-point-pathing-section-pipeline.ts" diff --git a/scripts/benchmarking/benchmark.ts b/scripts/benchmarking/benchmark.ts index 623c1de..617e553 100644 --- a/scripts/benchmarking/benchmark.ts +++ b/scripts/benchmarking/benchmark.ts @@ -11,6 +11,7 @@ import { TinyHyperGraphSectionPipelineSolver, TinyHyperGraphSectionSolver, type TinyHyperGraphSolver, + type TinyHyperGraphSolverOptions, } from "../../lib/index" type DatasetModule = Record & { @@ -56,6 +57,16 @@ Run the hg07 section-pipeline benchmark and write per-sample artifacts under ./r Options: --limit N Run the first N samples from the dataset. --sample NUM Run a specific sample by number or name (e.g. 2, 002, sample002). + --rip-congestion-mode MODE + Use \`region\` or \`penalty-points\`. + --region-cost-factor N + Region-mode rerip congestion factor. + --penalty-radius N + Penalty-point radius in board units. + --penalty-falloff N + Penalty-point falloff exponent. + --penalty-magnitude N + Penalty-point magnitude before falloff. --help Show this help text. Examples: @@ -63,6 +74,8 @@ Examples: ./benchmark.sh --limit 20 ./benchmark.sh --sample 2 ./benchmark.sh --sample sample002 + ./benchmark.sh --rip-congestion-mode region + ./benchmark.sh --penalty-radius 0.8 --penalty-falloff 1.5 --penalty-magnitude 0.2 Outputs: - Incremental progress lines always include duration=... for hillclimbing workflows. @@ -100,6 +113,7 @@ const formatSampleName = (value: string): string => { const parseArgs = () => { let limit: number | null = null let sampleName: string | null = null + const solverOptions: TinyHyperGraphSolverOptions = {} for (let index = 0; index < process.argv.length; index += 1) { const arg = process.argv[index] @@ -133,6 +147,82 @@ const parseArgs = () => { continue } + if (arg === "--rip-congestion-mode") { + const rawValue = process.argv[index + 1] + if (rawValue !== "region" && rawValue !== "penalty-points") { + usageError( + `Invalid --rip-congestion-mode value: ${rawValue ?? ""}`, + ) + } + + const ripCongestionMode: NonNullable< + TinyHyperGraphSolverOptions["RIP_CONGESTION_MODE"] + > = rawValue as NonNullable< + TinyHyperGraphSolverOptions["RIP_CONGESTION_MODE"] + > + solverOptions.RIP_CONGESTION_MODE = ripCongestionMode + index += 1 + continue + } + + if (arg === "--region-cost-factor") { + const rawValue = process.argv[index + 1] + const parsedValue = Number(rawValue) + + if (!rawValue || !Number.isFinite(parsedValue)) { + usageError( + `Invalid --region-cost-factor value: ${rawValue ?? ""}`, + ) + } + + solverOptions.RIP_CONGESTION_REGION_COST_FACTOR = parsedValue + index += 1 + continue + } + + if (arg === "--penalty-radius") { + const rawValue = process.argv[index + 1] + const parsedValue = Number(rawValue) + + if (!rawValue || !Number.isFinite(parsedValue) || parsedValue < 0) { + usageError(`Invalid --penalty-radius value: ${rawValue ?? ""}`) + } + + solverOptions.INTERSECTION_PENALTY_POINT_RADIUS = parsedValue + index += 1 + continue + } + + if (arg === "--penalty-falloff") { + const rawValue = process.argv[index + 1] + const parsedValue = Number(rawValue) + + if (!rawValue || !Number.isFinite(parsedValue) || parsedValue < 0) { + usageError( + `Invalid --penalty-falloff value: ${rawValue ?? ""}`, + ) + } + + solverOptions.INTERSECTION_PENALTY_POINT_FALLOFF = parsedValue + index += 1 + continue + } + + if (arg === "--penalty-magnitude") { + const rawValue = process.argv[index + 1] + const parsedValue = Number(rawValue) + + if (!rawValue || !Number.isFinite(parsedValue) || parsedValue < 0) { + usageError( + `Invalid --penalty-magnitude value: ${rawValue ?? ""}`, + ) + } + + solverOptions.INTERSECTION_PENALTY_POINT_MAGNITUDE = parsedValue + index += 1 + continue + } + if (index >= 2 && arg.startsWith("-")) { usageError(`Unknown option: ${arg}`) } @@ -142,10 +232,16 @@ const parseArgs = () => { usageError("Use either --limit or --sample, not both") } - return { limit, sampleName } + return { + limit, + sampleName, + solverOptions: + Object.keys(solverOptions).length > 0 ? solverOptions : undefined, + } } -const formatSeconds = (durationMs: number) => `${(durationMs / 1000).toFixed(3)}s` +const formatSeconds = (durationMs: number) => + `${(durationMs / 1000).toFixed(3)}s` const formatMetric = (value: number | null, digits = 3) => value === null ? "n/a" : value.toFixed(digits) @@ -269,7 +365,7 @@ const stringifyLogValue = (value: unknown) => typeof value === "string" ? value : JSON.stringify(value, null, 2) const main = async () => { - const { limit, sampleName } = parseArgs() + const { limit, sampleName, solverOptions } = parseArgs() const datasetModule = await loadDatasetModule() const cwd = process.cwd() const resultsDir = path.join(cwd, "results") @@ -291,6 +387,7 @@ const main = async () => { try { const pipelineSolver = new TinyHyperGraphSectionPipelineSolver({ serializedHyperGraph, + sectionSolverOptions: solverOptions, }) pipelineSolver.solve() @@ -311,8 +408,9 @@ const main = async () => { const baselineMaxRegionCost = getSerializedOutputMaxRegionCost(solveGraphOutput) - const finalMaxRegionCost = - getSerializedOutputMaxRegionCost(optimizeSectionOutput) + const finalMaxRegionCost = getSerializedOutputMaxRegionCost( + optimizeSectionOutput, + ) const delta = baselineMaxRegionCost - finalMaxRegionCost const durationMs = performance.now() - sampleStart const optimized = delta > IMPROVEMENT_EPSILON @@ -339,7 +437,8 @@ const main = async () => { candidateCount, generatedCandidateCount, duplicateCandidateCount, - selectedCandidateLabel: pipelineSolver.selectedSectionCandidateLabel ?? null, + selectedCandidateLabel: + pipelineSolver.selectedSectionCandidateLabel ?? null, selectedCandidateFamily: pipelineSolver.selectedSectionCandidateFamily ?? null, error: null, @@ -359,12 +458,11 @@ const main = async () => { `duration=${formatSeconds(durationMs)}`, ].join(" "), ) - console.log( - `# no artifacts written` - ) + console.log(`# no artifacts written`) } catch (error) { const durationMs = performance.now() - sampleStart - const errorMessage = error instanceof Error ? error.stack ?? error.message : String(error) + const errorMessage = + error instanceof Error ? (error.stack ?? error.message) : String(error) const sampleDir = path.join(runDir, sampleMeta.sampleName) const logsPath = path.join(sampleDir, "logs.txt") const snapshotPath = path.join(sampleDir, "snapshot.png") @@ -375,6 +473,7 @@ const main = async () => { try { const pipelineSolver = new TinyHyperGraphSectionPipelineSolver({ serializedHyperGraph, + sectionSolverOptions: solverOptions, }) const png = await getSnapshotPng(pipelineSolver) await writeFile(snapshotPath, png) @@ -382,7 +481,7 @@ const main = async () => { } catch (snapshotError) { snapshotErrorMessage = snapshotError instanceof Error - ? snapshotError.stack ?? snapshotError.message + ? (snapshotError.stack ?? snapshotError.message) : String(snapshotError) } @@ -456,7 +555,9 @@ const main = async () => { const finalCosts = successfulResults .map((result) => result.finalMaxRegionCost) .filter((value): value is number => value !== null) - const candidateCounts = successfulResults.map((result) => result.candidateCount) + const candidateCounts = successfulResults.map( + (result) => result.candidateCount, + ) const successCount = successfulResults.length const improvedCount = successfulResults.filter( (result) => result.optimized, @@ -470,7 +571,9 @@ const main = async () => { console.log( `zero-final-max-region-cost rate: ${formatPercent(zeroFinalCostCount, successCount)}`, ) - console.log(`avg baseline max region cost: ${average(baselineCosts).toFixed(3)}`) + console.log( + `avg baseline max region cost: ${average(baselineCosts).toFixed(3)}`, + ) console.log(`avg final max region cost: ${average(finalCosts).toFixed(3)}`) console.log(`avg max region delta: ${average(deltas).toFixed(3)}`) console.log(`avg candidate count: ${average(candidateCounts).toFixed(3)}`) diff --git a/scripts/benchmarking/sweep-intersection-penalties.ts b/scripts/benchmarking/sweep-intersection-penalties.ts new file mode 100644 index 0000000..477aae7 --- /dev/null +++ b/scripts/benchmarking/sweep-intersection-penalties.ts @@ -0,0 +1,371 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import * as datasetHg07 from "dataset-hg07" +import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" +import { + TinyHyperGraphSectionPipelineSolver, + TinyHyperGraphSectionSolver, + type TinyHyperGraphSolver, + type TinyHyperGraphSolverOptions, +} from "../../lib/index" + +type DatasetSampleMeta = { + sampleName: string + circuitId: string +} + +type DatasetModule = Record & { + manifest: { + sampleCount: number + samples: DatasetSampleMeta[] + } +} + +type BenchmarkSummary = { + label: string + solverOptions: TinyHyperGraphSolverOptions + sampleCount: number + successCount: number + improvedCount: number + zeroFinalCostCount: number + failedCount: number + avgBaselineMaxRegionCost: number + avgFinalMaxRegionCost: number + avgDelta: number + avgDurationMs: number + elapsedMs: number +} + +const datasetModule = datasetHg07 as DatasetModule +const IMPROVEMENT_EPSILON = 1e-9 + +const HELP_TEXT = `Usage: bun run scripts/benchmarking/sweep-intersection-penalties.ts [options] + +Grid-search penalty-point parameters against the section pipeline benchmark. + +Options: + --limit N Run the first N samples from the dataset. + --sample NUM Run a specific sample by number or name. + --radii CSV Penalty radii to evaluate. Default: 0.5,0.8,1.1,1.4 + --falloffs CSV Penalty falloff exponents. Default: 0.75,1,1.5,2 + --magnitudes CSV Penalty magnitudes. Default: 0.05,0.1,0.15,0.2,0.3 + --top N Number of top configs to print. Default: 10 + --help Show this help text. +` + +const usageError = (message: string): never => { + console.error(message) + console.error("") + console.error(HELP_TEXT) + process.exit(1) +} + +const formatSampleName = (value: string): string => { + if (/^sample\d+$/i.test(value)) { + const digits = value.replace(/^sample/i, "") + return `sample${digits.padStart(3, "0")}` + } + + if (/^\d+$/.test(value)) { + return `sample${value.padStart(3, "0")}` + } + + return usageError(`Invalid --sample value: ${value}`) +} + +const parseCsvNumbers = (rawValue: string | undefined, flag: string) => { + const requiredValue = rawValue ?? usageError(`Missing value for ${flag}`) + + const values = requiredValue + .split(",") + .map((value) => Number(value.trim())) + .filter((value) => Number.isFinite(value)) + + if (values.length === 0) { + usageError(`Invalid ${flag} value: ${requiredValue}`) + } + + return values +} + +const parseArgs = () => { + let limit: number | null = null + let sampleName: string | null = null + let radii = [0.5, 0.8, 1.1, 1.4] + let falloffs = [0.75, 1, 1.5, 2] + let magnitudes = [0.05, 0.1, 0.15, 0.2, 0.3] + let top = 10 + + for (let index = 2; index < process.argv.length; index += 1) { + const arg = process.argv[index] + + if (arg === "--help" || arg === "-h") { + console.log(HELP_TEXT) + process.exit(0) + } + + if (arg === "--limit") { + const rawValue = process.argv[index + 1] + const parsedValue = Number(rawValue) + + if (!rawValue || !Number.isFinite(parsedValue) || parsedValue <= 0) { + usageError(`Invalid --limit value: ${rawValue ?? ""}`) + } + + limit = Math.floor(parsedValue) + index += 1 + continue + } + + if (arg === "--sample") { + sampleName = formatSampleName(process.argv[index + 1]) + index += 1 + continue + } + + if (arg === "--radii") { + radii = parseCsvNumbers(process.argv[index + 1], "--radii") + index += 1 + continue + } + + if (arg === "--falloffs") { + falloffs = parseCsvNumbers(process.argv[index + 1], "--falloffs") + index += 1 + continue + } + + if (arg === "--magnitudes") { + magnitudes = parseCsvNumbers(process.argv[index + 1], "--magnitudes") + index += 1 + continue + } + + if (arg === "--top") { + const rawValue = process.argv[index + 1] + const parsedValue = Number(rawValue) + + if (!rawValue || !Number.isFinite(parsedValue) || parsedValue <= 0) { + usageError(`Invalid --top value: ${rawValue ?? ""}`) + } + + top = Math.floor(parsedValue) + index += 1 + continue + } + + usageError(`Unknown option: ${arg}`) + } + + if (limit !== null && sampleName !== null) { + usageError("Use either --limit or --sample, not both") + } + + return { + limit, + sampleName, + radii, + falloffs, + magnitudes, + top, + } +} + +const average = (values: number[]) => + values.length === 0 + ? 0 + : values.reduce((sum, value) => sum + value, 0) / values.length + +const getMaxRegionCost = (solver: TinyHyperGraphSolver) => + solver.state.regionIntersectionCaches.reduce( + (maxRegionCost, regionIntersectionCache) => + Math.max(maxRegionCost, regionIntersectionCache.existingRegionCost), + 0, + ) + +const getSerializedOutputMaxRegionCost = ( + serializedHyperGraph: SerializedHyperGraph, +) => { + const { topology, problem, solution } = + loadSerializedHyperGraph(serializedHyperGraph) + const replaySolver = new TinyHyperGraphSectionSolver( + topology, + problem, + solution, + ) + + return getMaxRegionCost(replaySolver.baselineSolver) +} + +const getSelectedSamples = ( + limit: number | null, + sampleName: string | null, +): DatasetSampleMeta[] => { + if (sampleName) { + const sampleMeta = datasetModule.manifest.samples.find( + ({ sampleName: candidateName }) => candidateName === sampleName, + ) + + if (!sampleMeta) { + usageError(`Unknown sample: ${sampleName}`) + } + + const selectedSampleMeta: DatasetSampleMeta = sampleMeta! + return [selectedSampleMeta] + } + + const sampleCount = + limit === null + ? datasetModule.manifest.sampleCount + : Math.min(limit, datasetModule.manifest.sampleCount) + + return datasetModule.manifest.samples.slice(0, sampleCount) +} + +const runPipelineBenchmark = async ( + sampleMetas: DatasetSampleMeta[], + label: string, + solverOptions: TinyHyperGraphSolverOptions, +): Promise => { + const startTime = performance.now() + let successCount = 0 + let improvedCount = 0 + let zeroFinalCostCount = 0 + const baselineCosts: number[] = [] + const finalCosts: number[] = [] + const deltas: number[] = [] + const durations: number[] = [] + + for (const sampleMeta of sampleMetas) { + const sampleStartTime = performance.now() + const serializedHyperGraph = datasetModule[ + sampleMeta.sampleName + ] as SerializedHyperGraph + + try { + const pipelineSolver = new TinyHyperGraphSectionPipelineSolver({ + serializedHyperGraph, + sectionSolverOptions: solverOptions, + }) + pipelineSolver.solve() + + if (pipelineSolver.failed) { + throw new Error(pipelineSolver.error ?? "pipeline solver failed") + } + + const solveGraphOutput = + pipelineSolver.getStageOutput("solveGraph") + const optimizeSectionOutput = + pipelineSolver.getStageOutput("optimizeSection") + + if (!solveGraphOutput || !optimizeSectionOutput) { + throw new Error("pipeline did not produce both stage outputs") + } + + const baselineMaxRegionCost = + getSerializedOutputMaxRegionCost(solveGraphOutput) + const finalMaxRegionCost = getSerializedOutputMaxRegionCost( + optimizeSectionOutput, + ) + const delta = baselineMaxRegionCost - finalMaxRegionCost + + successCount += 1 + improvedCount += delta > IMPROVEMENT_EPSILON ? 1 : 0 + zeroFinalCostCount += finalMaxRegionCost <= IMPROVEMENT_EPSILON ? 1 : 0 + baselineCosts.push(baselineMaxRegionCost) + finalCosts.push(finalMaxRegionCost) + deltas.push(delta) + durations.push(performance.now() - sampleStartTime) + } catch { + durations.push(performance.now() - sampleStartTime) + } + } + + return { + label, + solverOptions, + sampleCount: sampleMetas.length, + successCount, + improvedCount, + zeroFinalCostCount, + failedCount: sampleMetas.length - successCount, + avgBaselineMaxRegionCost: average(baselineCosts), + avgFinalMaxRegionCost: average(finalCosts), + avgDelta: average(deltas), + avgDurationMs: average(durations), + elapsedMs: performance.now() - startTime, + } +} + +const compareSummaries = (left: BenchmarkSummary, right: BenchmarkSummary) => { + if (left.avgDelta !== right.avgDelta) { + return right.avgDelta - left.avgDelta + } + + if (left.improvedCount !== right.improvedCount) { + return right.improvedCount - left.improvedCount + } + + if (left.avgFinalMaxRegionCost !== right.avgFinalMaxRegionCost) { + return left.avgFinalMaxRegionCost - right.avgFinalMaxRegionCost + } + + return left.avgDurationMs - right.avgDurationMs +} + +const main = async () => { + const { limit, sampleName, radii, falloffs, magnitudes, top } = parseArgs() + const sampleMetas = getSelectedSamples(limit, sampleName) + + const baseline = await runPipelineBenchmark(sampleMetas, "baseline-region", { + RIP_CONGESTION_MODE: "region", + RIP_CONGESTION_REGION_COST_FACTOR: 0.1, + }) + + const candidateSummaries: BenchmarkSummary[] = [] + + for (const radius of radii) { + for (const falloff of falloffs) { + for (const magnitude of magnitudes) { + const label = `point-r${radius}-f${falloff}-m${magnitude}` + candidateSummaries.push( + await runPipelineBenchmark(sampleMetas, label, { + RIP_CONGESTION_MODE: "penalty-points", + INTERSECTION_PENALTY_POINT_RADIUS: radius, + INTERSECTION_PENALTY_POINT_FALLOFF: falloff, + INTERSECTION_PENALTY_POINT_MAGNITUDE: magnitude, + }), + ) + } + } + } + + const topCandidates = candidateSummaries.sort(compareSummaries).slice(0, top) + const bestCandidate = topCandidates[0] + + console.log( + JSON.stringify( + { + sampleCount: sampleMetas.length, + baseline, + bestCandidate, + improvementVsBaseline: bestCandidate + ? { + avgDelta: bestCandidate.avgDelta - baseline.avgDelta, + avgFinalMaxRegionCost: + baseline.avgFinalMaxRegionCost - + bestCandidate.avgFinalMaxRegionCost, + improvedSamples: + bestCandidate.improvedCount - baseline.improvedCount, + zeroFinalCostSamples: + bestCandidate.zeroFinalCostCount - baseline.zeroFinalCostCount, + } + : null, + topCandidates, + }, + null, + 2, + ), + ) +} + +await main() diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 53c6757..7fc3b82 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -15,6 +15,8 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + fromPortIds: new Int32Array(0), + toPortIds: new Int32Array(0), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -72,7 +74,9 @@ const createTestSolver = ( } test("completed routing rerips when a region exceeds the current threshold", () => { - const solver = createTestSolver() + const solver = createTestSolver({ + RIP_CONGESTION_MODE: "region", + }) solver.state.unroutedRoutes = [] solver.state.portAssignment.set([0, 0, 1, 1]) @@ -139,6 +143,40 @@ test("completed routing is accepted once all region costs are under the threshol expect(Array.from(solver.state.regionCongestionCost)).toEqual([0, 0]) }) +test("point-mode rerips add an accumulated port penalty around intersections", () => { + const solver = createTestSolver({ + RIP_CONGESTION_MODE: "penalty-points", + INTERSECTION_PENALTY_POINT_RADIUS: 2, + INTERSECTION_PENALTY_POINT_FALLOFF: 1, + INTERSECTION_PENALTY_POINT_MAGNITUDE: 1, + }) + + solver.topology.portX = new Float64Array([-1, 0, 1, 0]) + solver.topology.portY = new Float64Array([0, 1, 0, -1]) + solver.topology.portAngleForRegion1 = new Int32Array([18000, 9000, 0, 27000]) + solver.topology.portAngleForRegion2 = new Int32Array([18000, 9000, 0, 27000]) + solver.topology.regionIncidentPorts[0] = [0, 1, 2, 3] + solver.topology.regionIncidentPorts[1] = [] + solver.state.unroutedRoutes = [] + + solver.state.currentRouteNetId = 0 + solver.appendSegmentToRegionCache(0, 0, 2) + solver.state.regionSegments[0] = [[0, 0, 2]] + + solver.state.currentRouteNetId = 1 + solver.appendSegmentToRegionCache(0, 1, 3) + solver.state.regionSegments[0].push([1, 1, 3]) + + solver.step() + + expect(solver.solved).toBe(false) + expect(solver.state.ripCount).toBe(1) + expect(solver.state.intersectionPenaltyPoints).toHaveLength(1) + expect(solver.state.portCongestionCost[0]).toBeGreaterThan(0) + expect(solver.state.portCongestionCost[1]).toBeGreaterThan(0) + expect(solver.state.pendingIntersectionPenaltyPoints).toHaveLength(0) +}) + test("constructor options override snake-case hyperparameters before setup", () => { const solver = createTestSolver({ DISTANCE_TO_COST: 0.25, diff --git a/tests/solver/region-net-id.test.ts b/tests/solver/region-net-id.test.ts index 53a60c3..64575e0 100644 --- a/tests/solver/region-net-id.test.ts +++ b/tests/solver/region-net-id.test.ts @@ -13,6 +13,8 @@ const createRegionCache = ( lesserAngles: new Int32Array(0), greaterAngles: new Int32Array(0), layerMasks: new Int32Array(0), + fromPortIds: new Int32Array(0), + toPortIds: new Int32Array(0), existingCrossingLayerIntersections: 0, existingSameLayerIntersections: 0, existingEntryExitLayerChanges: 0, @@ -51,7 +53,9 @@ test("solver does not traverse regions reserved for a different net", () => { regionNetId: Int32Array.from([-1, 1, -1, -1, -1]), } - const solver = new TinyHyperGraphSolver(topology, problem) + const solver = new TinyHyperGraphSolver(topology, problem, { + RIP_CONGESTION_MODE: "region", + }) solver.state.regionIntersectionCaches[0] = createRegionCache(0.5) solver.step()