From c21fc6657a6bc6df24529994b251cd5b8306ff97 Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 13 Apr 2026 17:48:44 -0700 Subject: [PATCH] non center cost implementation --- lib/core.ts | 89 ++++++++++++++++--- .../TinyHyperGraphSectionPipelineSolver.ts | 4 +- lib/section-solver/index.ts | 2 + .../benchmarking/hg07-section-benchmark.ts | 9 +- tests/solver/non-center-cost.test.ts | 76 ++++++++++++++++ tests/solver/on-all-routes-routed.test.ts | 2 + tests/solver/section-solver.test.ts | 3 + 7 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 tests/solver/non-center-cost.test.ts diff --git a/lib/core.ts b/lib/core.ts index 650cae5..16fcae0 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -151,6 +151,7 @@ export interface TinyHyperGraphWorkingState { export interface TinyHyperGraphSolverOptions { DISTANCE_TO_COST?: number + NON_CENTER_COST_PER_MM?: number RIP_THRESHOLD_START?: number RIP_THRESHOLD_END?: number RIP_THRESHOLD_RAMP_ATTEMPTS?: number @@ -160,6 +161,7 @@ export interface TinyHyperGraphSolverOptions { export interface TinyHyperGraphSolverOptionTarget { DISTANCE_TO_COST: number + NON_CENTER_COST_PER_MM: number RIP_THRESHOLD_START: number RIP_THRESHOLD_END: number RIP_THRESHOLD_RAMP_ATTEMPTS: number @@ -178,6 +180,9 @@ export const applyTinyHyperGraphSolverOptions = ( if (options.DISTANCE_TO_COST !== undefined) { solver.DISTANCE_TO_COST = options.DISTANCE_TO_COST } + if (options.NON_CENTER_COST_PER_MM !== undefined) { + solver.NON_CENTER_COST_PER_MM = options.NON_CENTER_COST_PER_MM + } if (options.RIP_THRESHOLD_START !== undefined) { solver.RIP_THRESHOLD_START = options.RIP_THRESHOLD_START } @@ -200,6 +205,7 @@ export const getTinyHyperGraphSolverOptions = ( solver: TinyHyperGraphSolverOptionTarget, ): TinyHyperGraphSolverOptions => ({ DISTANCE_TO_COST: solver.DISTANCE_TO_COST, + NON_CENTER_COST_PER_MM: solver.NON_CENTER_COST_PER_MM, RIP_THRESHOLD_START: solver.RIP_THRESHOLD_START, RIP_THRESHOLD_END: solver.RIP_THRESHOLD_END, RIP_THRESHOLD_RAMP_ATTEMPTS: solver.RIP_THRESHOLD_RAMP_ATTEMPTS, @@ -217,6 +223,8 @@ interface SegmentGeometryScratch { entryExitLayerChanges: number } +export const DEFAULT_NON_CENTER_COST_PER_MM = 0.31 + export class TinyHyperGraphSolver extends BaseSolver { state: TinyHyperGraphWorkingState private _problemSetup?: TinyHyperGraphProblemSetup @@ -228,6 +236,7 @@ export class TinyHyperGraphSolver extends BaseSolver { } DISTANCE_TO_COST = 0.05 // 50mm = 1 cost unit (1 cost unit ~ 100% chance of failure) + NON_CENTER_COST_PER_MM = DEFAULT_NON_CENTER_COST_PER_MM RIP_THRESHOLD_START = 0.05 RIP_THRESHOLD_END = 0.8 @@ -515,6 +524,70 @@ export class TinyHyperGraphSolver extends BaseSolver { return isKnownSingleLayerMask(regionAvailableZMask) } + getPortAngleForRegion(regionId: RegionId, portId: PortId): number { + const incidentRegions = this.topology.incidentPortRegion[portId] ?? [] + + if (incidentRegions[0] === regionId || incidentRegions[1] !== regionId) { + return this.topology.portAngleForRegion1[portId] + } + + return ( + this.topology.portAngleForRegion2?.[portId] ?? + this.topology.portAngleForRegion1[portId] + ) + } + + computePortNonCenterDistance(regionId: RegionId, portId: PortId): number { + const angle = this.getPortAngleForRegion(regionId, portId) + + if (angle <= 9000) { + return (Math.abs(angle - 4500) * this.topology.regionHeight[regionId]) / 9000 + } + + if (angle <= 18000) { + return (Math.abs(angle - 13500) * this.topology.regionWidth[regionId]) / 9000 + } + + if (angle <= 27000) { + return (Math.abs(angle - 22500) * this.topology.regionHeight[regionId]) / 9000 + } + + return (Math.abs(angle - 31500) * this.topology.regionWidth[regionId]) / 9000 + } + + computePortNonCenterPenalty(regionId: RegionId, portId: PortId): number { + if (this.NON_CENTER_COST_PER_MM <= 0) { + return 0 + } + + return ( + this.computePortNonCenterDistance(regionId, portId) * + this.NON_CENTER_COST_PER_MM + ) + } + + computeBestPortNonCenterPenalty(portId: PortId): number { + if (this.NON_CENTER_COST_PER_MM <= 0) { + return 0 + } + + const incidentRegions = this.topology.incidentPortRegion[portId] ?? [] + let bestPenalty = Number.POSITIVE_INFINITY + + for (const regionId of incidentRegions) { + if (regionId === undefined || regionId < 0) { + continue + } + + bestPenalty = Math.min( + bestPenalty, + this.computePortNonCenterPenalty(regionId, portId), + ) + } + + return Number.isFinite(bestPenalty) ? bestPenalty : 0 + } + populateSegmentGeometryScratch( regionId: RegionId, port1Id: PortId, @@ -522,18 +595,8 @@ export class TinyHyperGraphSolver extends BaseSolver { ): SegmentGeometryScratch { const { topology } = this const scratch = this.segmentGeometryScratch - const port1IncidentRegions = topology.incidentPortRegion[port1Id] - const port2IncidentRegions = topology.incidentPortRegion[port2Id] - const angle1 = - port1IncidentRegions[0] === regionId || port1IncidentRegions[1] !== regionId - ? topology.portAngleForRegion1[port1Id] - : topology.portAngleForRegion2?.[port1Id] ?? - topology.portAngleForRegion1[port1Id] - const angle2 = - port2IncidentRegions[0] === regionId || port2IncidentRegions[1] !== regionId - ? topology.portAngleForRegion1[port2Id] - : topology.portAngleForRegion2?.[port2Id] ?? - topology.portAngleForRegion1[port2Id] + const angle1 = this.getPortAngleForRegion(regionId, port1Id) + const angle2 = this.getPortAngleForRegion(regionId, port2Id) const z1 = topology.portZ[port1Id] const z2 = topology.portZ[port2Id] scratch.lesserAngle = angle1 < angle2 ? angle1 : angle2 @@ -817,10 +880,12 @@ export class TinyHyperGraphSolver extends BaseSolver { regionCache.existingSegmentCount + 1, topology.regionAvailableZMask?.[nextRegionId] ?? 0, ) - regionCache.existingRegionCost + const nonCenterPenalty = this.computeBestPortNonCenterPenalty(neighborPortId) return ( currentCandidate.g + newRegionCost + + nonCenterPenalty + state.regionCongestionCost[nextRegionId] ) } diff --git a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts index fdc3ce5..f74ca8e 100644 --- a/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts +++ b/lib/section-solver/TinyHyperGraphSectionPipelineSolver.ts @@ -8,7 +8,7 @@ import type { TinyHyperGraphSolverOptions, TinyHyperGraphTopology, } from "../core" -import { TinyHyperGraphSolver } from "../core" +import { DEFAULT_NON_CENTER_COST_PER_MM, TinyHyperGraphSolver } from "../core" import type { RegionId } from "../types" import type { TinyHyperGraphSectionSolverOptions } from "./index" import { getActiveSectionRouteIds, TinyHyperGraphSectionSolver } from "./index" @@ -59,11 +59,13 @@ type AutomaticSectionSearchResult = { } const DEFAULT_SOLVE_GRAPH_OPTIONS: TinyHyperGraphSolverOptions = { + NON_CENTER_COST_PER_MM: DEFAULT_NON_CENTER_COST_PER_MM, RIP_THRESHOLD_RAMP_ATTEMPTS: 5, } const DEFAULT_SECTION_SOLVER_OPTIONS: TinyHyperGraphSectionSolverOptions = { DISTANCE_TO_COST: 0.05, + NON_CENTER_COST_PER_MM: DEFAULT_NON_CENTER_COST_PER_MM, RIP_THRESHOLD_RAMP_ATTEMPTS: 16, RIP_CONGESTION_REGION_COST_FACTOR: 0.1, MAX_ITERATIONS: 1e6, diff --git a/lib/section-solver/index.ts b/lib/section-solver/index.ts index e92c053..70b9904 100644 --- a/lib/section-solver/index.ts +++ b/lib/section-solver/index.ts @@ -1,6 +1,7 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import { + DEFAULT_NON_CENTER_COST_PER_MM, applyTinyHyperGraphSolverOptions, createEmptyRegionIntersectionCache, getTinyHyperGraphSolverOptions, @@ -857,6 +858,7 @@ export class TinyHyperGraphSectionSolver extends BaseSolver { activeRouteIds: RouteId[] = [] DISTANCE_TO_COST = 0.05 + NON_CENTER_COST_PER_MM = DEFAULT_NON_CENTER_COST_PER_MM RIP_THRESHOLD_START = 0.05 RIP_THRESHOLD_END = 0.8 diff --git a/scripts/benchmarking/hg07-section-benchmark.ts b/scripts/benchmarking/hg07-section-benchmark.ts index 52de77a..952fff2 100644 --- a/scripts/benchmarking/hg07-section-benchmark.ts +++ b/scripts/benchmarking/hg07-section-benchmark.ts @@ -2,6 +2,7 @@ import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import * as datasetHg07 from "dataset-hg07" import { loadSerializedHyperGraph } from "../../lib/compat/loadSerializedHyperGraph" import { + DEFAULT_NON_CENTER_COST_PER_MM, TinyHyperGraphSectionSolver, TinyHyperGraphSolver, type TinyHyperGraphProblem, @@ -83,6 +84,7 @@ export type SectionSolverBenchmarkConfig = { improvementEpsilon: number sectionSolver: { distanceToCost: number + nonCenterCostPerMm: number ripThresholdStart: number ripThresholdEnd: number ripThresholdRampAttempts: number @@ -162,6 +164,7 @@ export const legacySectionSolverBenchmarkConfig: SectionSolverBenchmarkConfig = improvementEpsilon: 1e-9, sectionSolver: { distanceToCost: 0.05, + nonCenterCostPerMm: DEFAULT_NON_CENTER_COST_PER_MM, ripThresholdStart: 0.05, ripThresholdEnd: 0.8, ripThresholdRampAttempts: 50, @@ -406,6 +409,8 @@ const applySectionSolverConfig = ( config: SectionSolverBenchmarkConfig, ) => { sectionSolver.DISTANCE_TO_COST = config.sectionSolver.distanceToCost + sectionSolver.NON_CENTER_COST_PER_MM = + config.sectionSolver.nonCenterCostPerMm sectionSolver.RIP_THRESHOLD_START = config.sectionSolver.ripThresholdStart sectionSolver.RIP_THRESHOLD_END = config.sectionSolver.ripThresholdEnd sectionSolver.RIP_THRESHOLD_RAMP_ATTEMPTS = @@ -427,7 +432,9 @@ const runBestSectionOptimizationPass = ( ): SectionPassResult => { const solveGraphStartTime = performance.now() const { topology, problem } = loadSerializedHyperGraph(serializedHyperGraph) - const solveGraphSolver = new TinyHyperGraphSolver(topology, problem) + const solveGraphSolver = new TinyHyperGraphSolver(topology, problem, { + NON_CENTER_COST_PER_MM: config.sectionSolver.nonCenterCostPerMm, + }) solveGraphSolver.solve() profiling.totalSolveGraphMs += performance.now() - solveGraphStartTime diff --git a/tests/solver/non-center-cost.test.ts b/tests/solver/non-center-cost.test.ts new file mode 100644 index 0000000..b6b5143 --- /dev/null +++ b/tests/solver/non-center-cost.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from "bun:test" +import { TinyHyperGraphSolver, type TinyHyperGraphProblem, type TinyHyperGraphTopology } from "lib/index" + +const createNonCenterTestSolver = ( + options?: ConstructorParameters[2], +) => { + const topology: TinyHyperGraphTopology = { + portCount: 4, + regionCount: 2, + regionIncidentPorts: [ + [0, 1, 2, 3], + [0, 1, 2, 3], + ], + incidentPortRegion: [ + [0, 1], + [0, 1], + [0, 1], + [1, 0], + ], + regionWidth: new Float64Array([6, 6]), + regionHeight: new Float64Array([10, 10]), + regionCenterX: new Float64Array([0, 0]), + regionCenterY: new Float64Array([0, 0]), + portAngleForRegion1: new Int32Array([4500, 0, 13500, 0]), + portAngleForRegion2: new Int32Array([4500, 0, 13500, 18000]), + portX: new Float64Array(4), + portY: new Float64Array(4), + portZ: new Int32Array(4), + } + + topology.portAngleForRegion1[3] = 4500 + + const problem: TinyHyperGraphProblem = { + routeCount: 1, + portSectionMask: new Int8Array([1, 1, 1, 1]), + routeStartPort: new Int32Array([0]), + routeEndPort: new Int32Array([1]), + routeNet: new Int32Array([0]), + regionNetId: new Int32Array(2).fill(-1), + } + + return new TinyHyperGraphSolver(topology, problem, options) +} + +test("non-center penalty is based on distance from the middle of the touched side", () => { + const solver = createNonCenterTestSolver({ + NON_CENTER_COST_PER_MM: 0.2, + }) + + expect(solver.computePortNonCenterPenalty(0, 0)).toBeCloseTo(0, 6) + expect(solver.computePortNonCenterPenalty(0, 1)).toBeCloseTo(1, 6) + expect(solver.computePortNonCenterPenalty(0, 2)).toBeCloseTo(0, 6) + expect(solver.computePortNonCenterPenalty(0, 3)).toBeCloseTo(0.6, 6) + expect(solver.computeBestPortNonCenterPenalty(3)).toBeCloseTo(0, 6) +}) + +test("computeG uses the lower non-center penalty across both incident regions", () => { + const solver = createNonCenterTestSolver({ + NON_CENTER_COST_PER_MM: 0.2, + }) + + solver.state.currentRouteId = 0 + solver.state.currentRouteNetId = 0 + + const currentCandidate = { + portId: 0, + nextRegionId: 0, + g: 0, + h: 0, + f: 0, + } + + expect(solver.computeG(currentCandidate, 2)).toBeCloseTo(0, 6) + expect(solver.computeG(currentCandidate, 1)).toBeCloseTo(1, 6) + expect(solver.computeG(currentCandidate, 3)).toBeCloseTo(0, 6) +}) diff --git a/tests/solver/on-all-routes-routed.test.ts b/tests/solver/on-all-routes-routed.test.ts index 53c6757..dd4a68f 100644 --- a/tests/solver/on-all-routes-routed.test.ts +++ b/tests/solver/on-all-routes-routed.test.ts @@ -142,6 +142,7 @@ test("completed routing is accepted once all region costs are under the threshol test("constructor options override snake-case hyperparameters before setup", () => { const solver = createTestSolver({ DISTANCE_TO_COST: 0.25, + NON_CENTER_COST_PER_MM: 0.125, RIP_THRESHOLD_START: 0.12, RIP_THRESHOLD_END: 0.34, RIP_THRESHOLD_RAMP_ATTEMPTS: 7, @@ -150,6 +151,7 @@ test("constructor options override snake-case hyperparameters before setup", () }) expect(solver.DISTANCE_TO_COST).toBe(0.25) + expect(solver.NON_CENTER_COST_PER_MM).toBe(0.125) expect(solver.RIP_THRESHOLD_START).toBe(0.12) expect(solver.RIP_THRESHOLD_END).toBe(0.34) expect(solver.RIP_THRESHOLD_RAMP_ATTEMPTS).toBe(7) diff --git a/tests/solver/section-solver.test.ts b/tests/solver/section-solver.test.ts index e70a0fb..821042a 100644 --- a/tests/solver/section-solver.test.ts +++ b/tests/solver/section-solver.test.ts @@ -134,6 +134,7 @@ test("section solver enforces section-specific rip thresholds and max rip cap", solution, { DISTANCE_TO_COST: 0.2, + NON_CENTER_COST_PER_MM: 0.07, RIP_THRESHOLD_START: 0.11, RIP_THRESHOLD_END: 0.22, RIP_THRESHOLD_RAMP_ATTEMPTS: 9, @@ -148,7 +149,9 @@ test("section solver enforces section-specific rip thresholds and max rip cap", sectionSolver.setup() expect(sectionSolver.DISTANCE_TO_COST).toBe(0.2) + expect(sectionSolver.NON_CENTER_COST_PER_MM).toBe(0.07) expect(sectionSolver.sectionSolver?.DISTANCE_TO_COST).toBe(0.2) + expect(sectionSolver.sectionSolver?.NON_CENTER_COST_PER_MM).toBe(0.07) expect(sectionSolver.RIP_THRESHOLD_START).toBe(0.05) expect(sectionSolver.sectionSolver?.RIP_THRESHOLD_START).toBe(0.05) expect(sectionSolver.RIP_THRESHOLD_END).toBe(