Skip to content

Commit ba19483

Browse files
authored
Merge pull request #22 from evcc-io/perf/simplex-alloc-pooling
CBC/Clp engine features: reliability branching, DSE dual, warm cut re-solves
2 parents db52073 + 87fe897 commit ba19483

12 files changed

Lines changed: 310 additions & 165 deletions

File tree

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,17 @@ It fails on any failure not listed in `testdata/pulp_known_failures.txt`.
9494
re-run on every improving incumbent, SOS1/SOS2 via Beale–Tomlin
9595
splitting; a failed pass restarts once on the same model, inheriting
9696
cuts, fixings and probe facts.
97-
- **Branching**: strong branching at shallow depths (to depth 6 on large
98-
instances, 16 otherwise) seeding pseudocosts, with CBC-style
99-
strong-branch fixing — a probe side that is infeasible or cannot beat
100-
the incumbent fixes the variable at the node without spending a
101-
branch; pseudocost selection deeper (reliability-branching shape).
97+
- **Branching**: CBC reliability branching — a column whose pseudocost
98+
isn't yet trusted (fewer than `numberBeforeTrust`=10 observed gains
99+
either way) is strong-branched with cheap capped dual probes to seed
100+
it; trusted columns are scored straight from their pseudocost. The
101+
branch variable is the best CBC score (max-weighted blend before an
102+
incumbent, product rule after). Untrusted columns float to the top so
103+
the per-node strong-branch budget (`maxStrong`=5) is spent where the
104+
pseudocosts are least reliable — so it runs at every depth yet
105+
self-limits once columns are seeded. CBC-style strong-branch fixing: a
106+
probe side that is infeasible or cannot beat the incumbent fixes the
107+
variable at the node without spending a branch.
102108
- **Heuristics**: caller-provided MIP start (`mip.Model.MIPStart`,
103109
completed via a warm child solve before the cut loop so reduced-cost
104110
fixing bites; the trivial start is deliberately not polished — measured

mip/cuts.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const (
1717
cutDropTol = 1e-11
1818
maxCutsPer = 32 // cuts per round
1919
maxCutRound = 30 // hard cap; rounds stop early once the bound stalls
20+
21+
// cutMarginFrac stops cut rounds once a round's bound gain drops below this
22+
// fraction of the cumulative gain (warm-invariant diminishing-returns test)
23+
cutMarginFrac = 0.02
2024
)
2125

2226
// probingCuts probes fractional binaries at x and adds violated implication
@@ -294,9 +298,11 @@ func (m *Model) gomoryCuts(st *simplex.State) int {
294298
// fractional tableau rows; the aggregate LHS stays integer, so the
295299
// same MIR derivation applies. Big instances only (cf. probing cuts).
296300
if m.LP.NumRows() > 1500 {
301+
// D3: aggregate many tableau pairs toward CBC's TwoMir cut count.
302+
topCap, pairCap := 24, 64
297303
topN := len(cands)
298-
if topN > 8 {
299-
topN = 8
304+
if topN > topCap {
305+
topN = topCap
300306
}
301307
tabs := make([][]float64, topN)
302308
bval := make([]float64, topN)
@@ -307,8 +313,8 @@ func (m *Model) gomoryCuts(st *simplex.State) int {
307313
}
308314
agg := make([]float64, nt)
309315
pairAdded := 0
310-
for a := 0; a < topN && pairAdded < 8; a++ {
311-
for b := a + 1; b < topN && pairAdded < 8; b++ {
316+
for a := 0; a < topN && pairAdded < pairCap; a++ {
317+
for b := a + 1; b < topN && pairAdded < pairCap; b++ {
312318
for _, sgn := range [2]float64{1, -1} {
313319
for j := range nt {
314320
agg[j] = tabs[a][j] + sgn*tabs[b][j]

mip/eliminate.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,9 @@ func eliminateSingletons(p *problem.Problem) (*problem.Problem, *reduction) {
167167
return q, &reduction{orig: p, records: records, colMap: colMap}
168168
}
169169

170-
// dropRedundantContinuousRows removes rows that (a) can never bind under the
171-
// current column bounds (min activity >= lb and max activity <= ub) and (b)
172-
// contain no integer column. Condition (a) makes removal exact for the LP;
173-
// condition (b) keeps it clear of the GMI/MIR cut suite, which only derives
174-
// from integer structure — so the relaxation, cuts and branch tree are all
175-
// unchanged, only inert continuous rows leave (CBC forcing-row removal).
176-
// Returns (nil,nil) when nothing qualifies; keep[origRow]=false when dropped.
177-
func dropRedundantContinuousRows(p *problem.Problem) (*problem.Problem, []bool) {
170+
// dropRedundantRows removes never-binding rows (exact; postsolve: a·x, 0 dual).
171+
// includeInt also drops integer rows — CBC CglPreProcess forcing removal (D1).
172+
func dropRedundantRows(p *problem.Problem, includeInt bool) (*problem.Problem, []bool) {
178173
inf := problem.Inf
179174
keep := make([]bool, len(p.Rows))
180175
nkeep := 0
@@ -208,7 +203,7 @@ func dropRedundantContinuousRows(p *problem.Problem) (*problem.Problem, []bool)
208203
mx += a * lb
209204
}
210205
}
211-
redundant := !hasInt && mni == 0 && mxi == 0 && mn >= rlb-1e-7 && mx <= rub+1e-7
206+
redundant := (includeInt || !hasInt) && mni == 0 && mxi == 0 && mn >= rlb-1e-7 && mx <= rub+1e-7
212207
keep[ri] = !redundant
213208
if keep[ri] {
214209
nkeep++
@@ -231,7 +226,7 @@ func dropRedundantContinuousRows(p *problem.Problem) (*problem.Problem, []bool)
231226
nri := q.AddRow(r.Name, r.Idx, r.Coef, r.Sense, r.RHS)
232227
q.Rows[nri].HasRange, q.Rows[nri].Range = r.HasRange, r.Range
233228
}
234-
debugf("presolve: dropped %d/%d redundant continuous rows", len(p.Rows)-nkeep, len(p.Rows))
229+
debugf("presolve: dropped %d/%d redundant rows (includeInt=%v)", len(p.Rows)-nkeep, len(p.Rows), includeInt)
235230
return q, keep
236231
}
237232

0 commit comments

Comments
 (0)