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
110 changes: 87 additions & 23 deletions lib/HighDensitySolverA08/A08_BreakoutSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ type ForceIterationSnapshot = {
snapshots: MidpointForceSnapshot[]
}

type ShrinkAmountBySide = Partial<Record<Side, number>>

type ShrinkRectResult =
| { ok: true; rect: RectBounds }
| { ok: true; rect: RectBounds; appliedShrinkBySide: ShrinkAmountBySide }
| { ok: false; reason: "unchanged" | "collapsed" }

const BREAKOUT_MIDPOINT_INDEX = 1
Expand All @@ -133,6 +135,7 @@ export interface A08BreakoutSolverProps {
initialRectMarginMm?: number
innerRectMarginMm?: number
rectShrinkStepMm?: number
maxShrinkMargin?: number
breakoutTraceMarginMm?: number
breakoutBoundaryMarginMm?: number
breakoutSegmentCount?: number
Expand Down Expand Up @@ -364,6 +367,7 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
effort: number
initialRectMarginMm: number
rectShrinkStepMm: number
maxShrinkMargin?: number
breakoutTraceMarginMm: number
breakoutBoundaryMarginMm: number
breakoutSegmentCount: number
Expand All @@ -388,12 +392,26 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
private anchorsBySide = new Map<Side, SpreadAnchor[]>()
private pendingShrinkSides: Side[] = []
private nextForceSideCursor = 0
private shrinkMarginBySide: Record<Side, number> = {
left: 0,
right: 0,
bottom: 0,
top: 0,
}

constructor(props: A08BreakoutSolverProps) {
super()
const breakoutTraceMarginMm =
props.breakoutTraceMarginMm ?? defaultA08Params.breakoutTraceMarginMm
this.constructorProps = props
const maxShrinkMargin =
props.maxShrinkMargin === undefined ||
!Number.isFinite(props.maxShrinkMargin)
? defaultA08Params.maxShrinkMargin
: Math.max(0, props.maxShrinkMargin)
this.constructorProps = {
...props,
maxShrinkMargin,
}
this.nodeWithPortPoints = props.nodeWithPortPoints
this.cellSizeMm = props.cellSizeMm ?? defaultA08Params.cellSizeMm
this.maxCellCount = props.maxCellCount
Expand All @@ -407,6 +425,7 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
defaultA08Params.initialRectMarginMm
this.rectShrinkStepMm =
props.rectShrinkStepMm ?? defaultA08Params.rectShrinkStepMm
this.maxShrinkMargin = maxShrinkMargin
this.breakoutTraceMarginMm = breakoutTraceMarginMm
this.breakoutBoundaryMarginMm = getDefaultA08BreakoutBoundaryMarginMm(props)
// The breakout path is implemented as a single midpoint, so this stays fixed.
Expand Down Expand Up @@ -452,6 +471,12 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
this.iterationsAtCurrentRect = 0
this.lastForceIteration = null
this.nextForceSideCursor = 0
this.shrinkMarginBySide = {
left: 0,
right: 0,
bottom: 0,
top: 0,
}
this.spreadAssignments = []
this.breakoutRoutes = []
this.innerNodeWithPortPoints = null
Expand Down Expand Up @@ -1395,9 +1420,29 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
)
}

private getShrinkAmountsBySide(sides: Side[]): ShrinkAmountBySide {
const shrinkBySide: ShrinkAmountBySide = {}

for (const side of sides) {
const remainingShrinkMargin =
this.maxShrinkMargin === undefined
? this.rectShrinkStepMm
: Math.max(0, this.maxShrinkMargin - this.shrinkMarginBySide[side])
const shrinkAmount = Math.min(
this.rectShrinkStepMm,
remainingShrinkMargin,
)
if (shrinkAmount > EPSILON) {
shrinkBySide[side] = shrinkAmount
}
}

return shrinkBySide
}

private shrinkRectBySides(
innerRect: RectBounds,
sides: Side[],
shrinkBySide: ShrinkAmountBySide,
): ShrinkRectResult {
const minDimension = Math.max(
this.cellSizeMm,
Expand All @@ -1409,43 +1454,45 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
minY: innerRect.minY,
maxY: innerRect.maxY,
}
const appliedShrinkBySide: ShrinkAmountBySide = {}
let changed = false

for (const side of sides) {
for (const side of SIDE_ORDER) {
const shrinkAmount = shrinkBySide[side] ?? 0
if (shrinkAmount <= EPSILON) continue

switch (side) {
case "left":
if (
nextBounds.maxX - (nextBounds.minX + this.rectShrinkStepMm) >
nextBounds.maxX - (nextBounds.minX + shrinkAmount) >
minDimension
) {
nextBounds.minX += this.rectShrinkStepMm
nextBounds.minX += shrinkAmount
appliedShrinkBySide.left = shrinkAmount
changed = true
}
break
case "right":
if (
nextBounds.maxX - this.rectShrinkStepMm - nextBounds.minX >
minDimension
) {
nextBounds.maxX -= this.rectShrinkStepMm
if (nextBounds.maxX - shrinkAmount - nextBounds.minX > minDimension) {
nextBounds.maxX -= shrinkAmount
appliedShrinkBySide.right = shrinkAmount
changed = true
}
break
case "bottom":
if (
nextBounds.maxY - (nextBounds.minY + this.rectShrinkStepMm) >
nextBounds.maxY - (nextBounds.minY + shrinkAmount) >
minDimension
) {
nextBounds.minY += this.rectShrinkStepMm
nextBounds.minY += shrinkAmount
appliedShrinkBySide.bottom = shrinkAmount
changed = true
}
break
case "top":
if (
nextBounds.maxY - this.rectShrinkStepMm - nextBounds.minY >
minDimension
) {
nextBounds.maxY -= this.rectShrinkStepMm
if (nextBounds.maxY - shrinkAmount - nextBounds.minY > minDimension) {
nextBounds.maxY -= shrinkAmount
appliedShrinkBySide.top = shrinkAmount
changed = true
}
break
Expand All @@ -1464,7 +1511,7 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
return { ok: false, reason: "collapsed" }
}

return { ok: true, rect: nextRect }
return { ok: true, rect: nextRect, appliedShrinkBySide }
}

private applyPendingShrink() {
Expand All @@ -1474,13 +1521,19 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
return false
}

const shrinkResult = this.shrinkRectBySides(
this.innerRect,
this.pendingShrinkSides,
)
const shrinkBySide = this.getShrinkAmountsBySide(this.pendingShrinkSides)
this.pendingShrinkSides = []

if (Object.keys(shrinkBySide).length === 0) {
return this.acceptCurrentInnerRectForA01()
}

const shrinkResult = this.shrinkRectBySides(this.innerRect, shrinkBySide)

if (!shrinkResult.ok) {
if (this.maxShrinkMargin !== undefined) {
return this.acceptCurrentInnerRectForA01()
}
this.error =
shrinkResult.reason === "collapsed"
? "A08_BreakoutSolver inner rect collapsed during shrink"
Expand All @@ -1489,11 +1542,22 @@ export class HighDensitySolverA08BreakoutSolver extends BaseSolver {
return false
}

for (const side of SIDE_ORDER) {
this.shrinkMarginBySide[side] +=
shrinkResult.appliedShrinkBySide[side] ?? 0
}
this.shrinkCount += 1
this.reinitializeForInnerRect(shrinkResult.rect)
return true
}

private acceptCurrentInnerRectForA01() {
this.lastForceIteration = null
this.solved = true
this.progress = 1
return true
}

override visualize(): GraphicsObject {
const rects = [
{
Expand Down
8 changes: 8 additions & 0 deletions lib/HighDensitySolverA08/HighDensitySolverA08.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface HighDensitySolverA08Props extends HighDensitySolverA01Props {
initialRectMarginMm?: number
innerRectMarginMm?: number
rectShrinkStepMm?: number
maxShrinkMargin?: number
breakoutTraceMarginMm?: number
breakoutBoundaryMarginMm?: number
breakoutSegmentCount?: number
Expand All @@ -48,6 +49,11 @@ function normalizeA08Props(
): HighDensitySolverA08Props {
const breakoutTraceMarginMm =
props.breakoutTraceMarginMm ?? defaultA08Params.breakoutTraceMarginMm
const maxShrinkMargin =
props.maxShrinkMargin === undefined ||
!Number.isFinite(props.maxShrinkMargin)
? defaultA08Params.maxShrinkMargin
: Math.max(0, props.maxShrinkMargin)

return {
...props,
Expand All @@ -67,6 +73,7 @@ function normalizeA08Props(
defaultA08Params.initialRectMarginMm,
rectShrinkStepMm:
props.rectShrinkStepMm ?? defaultA08Params.rectShrinkStepMm,
maxShrinkMargin,
breakoutTraceMarginMm,
breakoutBoundaryMarginMm: getDefaultA08BreakoutBoundaryMarginMm(props),
breakoutSegmentCount:
Expand Down Expand Up @@ -103,6 +110,7 @@ function toBreakoutSolverProps(
initialRectMarginMm: props.initialRectMarginMm,
innerRectMarginMm: props.innerRectMarginMm,
rectShrinkStepMm: props.rectShrinkStepMm,
maxShrinkMargin: props.maxShrinkMargin,
breakoutTraceMarginMm: props.breakoutTraceMarginMm,
breakoutBoundaryMarginMm: props.breakoutBoundaryMarginMm,
breakoutSegmentCount: props.breakoutSegmentCount,
Expand Down
2 changes: 2 additions & 0 deletions lib/default-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const defaultA08Params: Required<
| "effort"
| "initialRectMarginMm"
| "rectShrinkStepMm"
| "maxShrinkMargin"
| "breakoutTraceMarginMm"
| "breakoutSegmentCount"
| "breakoutMaxIterationsPerRect"
Expand All @@ -120,6 +121,7 @@ export const defaultA08Params: Required<
effort: 1,
initialRectMarginMm: 0.2,
rectShrinkStepMm: 0.1,
maxShrinkMargin: 2.4,
breakoutTraceMarginMm: 0.1,
breakoutSegmentCount: 2,
breakoutMaxIterationsPerRect: 60,
Expand Down
1 change: 1 addition & 0 deletions tests/a08/default-params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test("A08 breakout solver defaults stay aligned with defaultA08Params", () => {
expect(solver.effort).toBe(defaultA08Params.effort)
expect(solver.initialRectMarginMm).toBe(defaultA08Params.initialRectMarginMm)
expect(solver.rectShrinkStepMm).toBe(defaultA08Params.rectShrinkStepMm)
expect(solver.maxShrinkMargin).toBe(defaultA08Params.maxShrinkMargin)
expect(solver.breakoutTraceMarginMm).toBe(
defaultA08Params.breakoutTraceMarginMm,
)
Expand Down
83 changes: 83 additions & 0 deletions tests/a08/max-shrink-margin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect, test } from "bun:test"
import dataset02Json from "@tscircuit/hypergraph/datasets/jumper-graph-solver/dataset02.json"
import {
convertDataset02SampleToNodeWithPortPoints,
type Dataset02Sample,
} from "../../lib/dataset02/convertDataset02SampleToNodeWithPortPoints"
import { defaultA08Params } from "../../lib/default-params"
import {
HighDensitySolverA08,
HighDensitySolverA08BreakoutSolver,
} from "../../lib/HighDensitySolverA08/HighDensitySolverA08"
import { getNodeBounds } from "../../lib/HighDensitySolverA08/shared"
import type { NodeWithPortPoints } from "../../lib/types"

const dataset02 = dataset02Json as Dataset02Sample[]

test("A08 breakout shrink honors a partial maxShrinkMargin per side", () => {
const nodeWithPortPoints: NodeWithPortPoints = {
capacityMeshNodeId: "a08-max-shrink-margin",
center: { x: 0, y: 0 },
width: 10,
height: 10,
availableZ: [0, 1],
portPoints: [],
}

const solver = new HighDensitySolverA08BreakoutSolver({
...defaultA08Params,
nodeWithPortPoints,
cellSizeMm: 1,
rectShrinkStepMm: 0.1,
maxShrinkMargin: 0.05,
maxCellCount: 100,
})

solver.solve()

expect(solver.failed).toBeFalse()
expect(solver.solved).toBeTrue()
expect(solver.innerRect).not.toBeNull()
expect(solver.shrinkCount).toBe(1)
expect(solver.innerRect!.width).toBeCloseTo(9.9, 6)
expect(solver.innerRect!.height).toBeCloseTo(9.9, 6)
})

test("A08 advances to A01 after reaching maxShrinkMargin even if breakout checks stay unsolved", () => {
const sample = dataset02[9]
if (!sample) {
throw new Error("dataset02 sample010 is missing")
}

const nodeWithPortPoints = convertDataset02SampleToNodeWithPortPoints(
sample,
{
capacityMeshNodeId: "dataset02-10-max-shrink-margin",
availableZ: [0, 1],
},
)

const solver = new HighDensitySolverA08({
...defaultA08Params,
nodeWithPortPoints,
effort: 10,
initialRectMarginMm: 1,
breakoutMaxIterationsPerRect: 1,
maxShrinkMargin: 0,
})
solver.MAX_ITERATIONS = 100_000_000
solver.solveUntilStage("A01")

const outerBounds = getNodeBounds(nodeWithPortPoints)

expect(solver.stage).toBe("A01")
expect(solver.failed).toBeFalse()
expect(solver.breakoutSolver).not.toBeNull()
expect(solver.breakoutSolver!.stats?.shrinkCount).toBe(0)
expect(solver.breakoutSolver!.stats?.unsolvedSides).toContain("bottom")
expect(solver.innerRect).not.toBeNull()
expect(solver.innerRect!.minX).toBeCloseTo(outerBounds.minX + 1, 6)
expect(solver.innerRect!.maxX).toBeCloseTo(outerBounds.maxX - 1, 6)
expect(solver.innerRect!.minY).toBeCloseTo(outerBounds.minY + 1, 6)
expect(solver.innerRect!.maxY).toBeCloseTo(outerBounds.maxY - 1, 6)
})
Loading