Skip to content

Commit acbd018

Browse files
andigclaude
andauthored
mip: cheap dual-only strong-branch probes (CBC-style limited iteration) (#19)
Strong branching re-solved each probe child to full optimality (warm primal, 80ms deadline), which grinds on the degenerate LPs these scheduling models produce. CBC instead runs a few dual-simplex iterations per probe and reads off the bound: the dual stays dual-feasible throughout, so its objective is a valid lower bound (a conservative branch-gain) at any iteration. This adds an LP probe mode: SetProbe(cap) makes a warm re-solve stop after the dual repair with an IterCap on the dual pivots, returning the dual bound and skipping the primal phase. strongBranch enables it around its candidate probes. The bound is a lower estimate, so branch scores, pseudocosts and cutoff-fathoming all stay sound (conservative). Golden suite (../optimizer): case 018 0.57s -> 0.34s (-40%), case 020 5.51s -> 4.99s (-9%); suite -10%. Case 021 (cut-loop bound, not branching bound) is unchanged within noise. All cases proven optimal. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c00a50f commit acbd018

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

mip/mip.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,10 @@ func (m *Model) nodeBound(nd *node, j int) (float64, float64) {
12161216
return lb, ub
12171217
}
12181218

1219+
// strongProbeCap bounds dual pivots per strong-branch probe: enough for a
1220+
// useful bound, cheap enough that probing all shallow nodes stays affordable.
1221+
const strongProbeCap = 100
1222+
12191223
// strongBranch (CBC-style) probes both children of the top candidates and
12201224
// returns the column whose worse child moves the bound most; -1 if none do.
12211225
// strongBranch probes both sides of the most fractional candidates. A side
@@ -1248,6 +1252,11 @@ func (m *Model) strongBranch(nd *node, x []float64, endState *simplex.State, obj
12481252
// short per-probe deadline: strong branching must never eat the budget
12491253
saved := m.LP.Deadline
12501254
defer func() { m.LP.Deadline = saved }()
1255+
// cheap strong-branch probes: a capped dual-only re-solve returns a valid
1256+
// lower bound (CBC limited-iteration strong branching) instead of a full
1257+
// node solve, at a fraction of the pivots
1258+
m.LP.SetProbe(strongProbeCap)
1259+
defer m.LP.ClearProbe()
12511260

12521261
bestCol, bestScore := -1, math.Inf(-1)
12531262
var fixed []boundOverride

simplex/simplex.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ type LP struct {
7979
// (checked periodically inside the pivot loop).
8080
Deadline time.Time
8181

82+
// IterCap >0 bounds total pivots per solve; probeMode returns the
83+
// dual-feasible bound after dual repair without a primal phase. Together
84+
// they make strong-branch probes cheap: a capped dual-only re-solve
85+
// yields a valid lower bound (CBC-style limited-iteration strong branching).
86+
IterCap int
87+
probeMode bool
88+
8289
// pricingWindow >0 makes chooseEntering scan a rotating column window
8390
// (partial pricing) instead of all n+m every pivot; 0 means full scan.
8491
pricingWindow int
@@ -308,9 +315,19 @@ func (lp *LP) warmSolve(st *State, touched []int, preserve bool) (Status, *State
308315
} else if !noDualRepair {
309316
lp.dualRun(st)
310317
}
318+
if lp.probeMode {
319+
// dual repair leaves a dual-feasible basis: its objective is a valid
320+
// lower bound (a conservative strong-branch gain) without a primal solve
321+
return Optimal, st, lp.objective(st)
322+
}
311323
return lp.solveFrom(st)
312324
}
313325

326+
// SetProbe enables cheap dual-only strong-branch probing (a valid bound
327+
// after at most `cap` dual pivots); ClearProbe restores normal solving.
328+
func (lp *LP) SetProbe(cap int) { lp.probeMode, lp.IterCap = true, cap }
329+
func (lp *LP) ClearProbe() { lp.probeMode, lp.IterCap = false, 0 }
330+
314331
// dualPivotCap bounds dual pivots per re-solve; a degenerate dual bails to
315332
// the primal run instead of grinding forever.
316333
const dualPivotCap = 1024
@@ -358,6 +375,10 @@ func (lp *LP) dualRun(st *State) {
358375
if !lp.Deadline.IsZero() && time.Now().After(lp.Deadline) {
359376
return
360377
}
378+
if lp.IterCap > 0 && iter >= lp.IterCap {
379+
lp.Stats.DualCap++
380+
return
381+
}
361382
if iter == dualPivotCap {
362383
lp.Stats.DualCap++
363384
return

0 commit comments

Comments
 (0)