Skip to content

Commit c00a50f

Browse files
andigclaude
andauthored
mip: presolve forcing-row removal for redundant continuous rows (#18)
CBC's CglPreProcess drops rows that can never bind (min activity >= lb, max activity <= ub) after bound tightening. cbcgo's presolve tightened bounds and coefficients but kept every row. This adds dropRedundantContinuousRows: after presolve+probe, remove rows that are both (a) inert under the current column bounds and (b) free of any integer column. Condition (a) makes removal exact for the LP; condition (b) keeps it clear of the GMI/MIR cut suite, which derives only from integer structure -- so the relaxation, cuts and branch tree are unchanged, only inert continuous rows leave. Dropping integer-bearing rows was tried and regressed (it re-rolls the cut/vertex lottery on the demand-rate cases); restricting to continuous rows avoids that. Exact postsolve (expandRows) rebuilds the dropped rows' activity (a.x) and dual (0) so row-indexed outputs stay in the original row space. Golden suite (../optimizer): case 018 1.07s -> 0.55s (-48%, 103 -> 29 nodes); 020/021 unchanged (their redundant rows are integer big-M); suite -5%, all cases proven optimal, no regressions. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2af03e5 commit c00a50f

2 files changed

Lines changed: 112 additions & 0 deletions

File tree

mip/eliminate.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,105 @@ 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) {
178+
inf := problem.Inf
179+
keep := make([]bool, len(p.Rows))
180+
nkeep := 0
181+
for ri := range p.Rows {
182+
r := &p.Rows[ri]
183+
rlb, rub := r.Bounds()
184+
var mn, mx float64
185+
var mni, mxi int
186+
hasInt := false
187+
for k, j := range r.Idx {
188+
if p.Cols[j].Integer {
189+
hasInt = true
190+
}
191+
a := r.Coef[k]
192+
c := &p.Cols[j]
193+
lb, ub := c.LB, c.UB
194+
loInf := (a > 0 && lb <= -inf) || (a < 0 && ub >= inf)
195+
hiInf := (a > 0 && ub >= inf) || (a < 0 && lb <= -inf)
196+
if loInf {
197+
mni++
198+
} else if a >= 0 {
199+
mn += a * lb
200+
} else {
201+
mn += a * ub
202+
}
203+
if hiInf {
204+
mxi++
205+
} else if a >= 0 {
206+
mx += a * ub
207+
} else {
208+
mx += a * lb
209+
}
210+
}
211+
redundant := !hasInt && mni == 0 && mxi == 0 && mn >= rlb-1e-7 && mx <= rub+1e-7
212+
keep[ri] = !redundant
213+
if keep[ri] {
214+
nkeep++
215+
}
216+
}
217+
if nkeep == len(p.Rows) {
218+
return nil, nil
219+
}
220+
q := problem.New()
221+
q.Name, q.ObjSense, q.SOSs = p.Name, p.ObjSense, p.SOSs
222+
for j := range p.Cols {
223+
c := &p.Cols[j]
224+
q.AddCol(c.Name, c.LB, c.UB, c.Obj, c.Integer, nil, nil)
225+
}
226+
for ri := range p.Rows {
227+
if !keep[ri] {
228+
continue
229+
}
230+
r := &p.Rows[ri]
231+
nri := q.AddRow(r.Name, r.Idx, r.Coef, r.Sense, r.RHS)
232+
q.Rows[nri].HasRange, q.Rows[nri].Range = r.HasRange, r.Range
233+
}
234+
debugf("presolve: dropped %d/%d redundant continuous rows", len(p.Rows)-nkeep, len(p.Rows))
235+
return q, keep
236+
}
237+
238+
// expandRows restores full-length row outputs after a continuous-row drop:
239+
// kept rows copy through in order, dropped inert rows get activity a·x and a
240+
// zero dual (they never bind). orig is the pre-drop row set.
241+
func expandRows(res *Result, keep []bool, orig []problem.Row) {
242+
if res.X == nil {
243+
return
244+
}
245+
act := make([]float64, len(keep))
246+
price := make([]float64, len(keep))
247+
ri := 0
248+
for i, k := range keep {
249+
if k {
250+
if ri < len(res.RowActivity) {
251+
act[i] = res.RowActivity[ri]
252+
}
253+
if ri < len(res.RowPrice) {
254+
price[i] = res.RowPrice[ri]
255+
}
256+
ri++
257+
continue
258+
}
259+
r := &orig[i]
260+
a := 0.0
261+
for kk, j := range r.Idx {
262+
a += r.Coef[kk] * res.X[j]
263+
}
264+
act[i] = a // price stays 0: an inert row never binds
265+
}
266+
res.RowActivity, res.RowPrice = act, price
267+
}
268+
170269
// setRowBounds rewrites a row's sense/rhs/range to represent [lb, ub].
171270
func setRowBounds(r *problem.Row, lb, ub float64) {
172271
inf := problem.Inf

mip/mip.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ type Model struct {
102102
// per-column pseudocosts: observed bound gain per unit fraction
103103
psUp, psDn []float64
104104
psUpN, psDnN []int
105+
106+
// continuous-row drop postsolve: keep mask + pre-drop rows for a·x
107+
rrKeep []bool
108+
rrRows []problem.Row
105109
}
106110

107111
// psRecord accumulates one observed branching gain for column j.
@@ -205,6 +209,12 @@ func (m *Model) Solve() Result {
205209
}
206210
probe(m.P, probeDeadline)
207211
presolve(m.P)
212+
// forcing-row removal: drop redundant continuous rows (inert AND
213+
// clear of the integer cut suite). Postsolve rebuilds their outputs.
214+
if q, keep := dropRedundantContinuousRows(m.P); q != nil {
215+
m.rrKeep, m.rrRows = keep, m.P.Rows
216+
m.P = q
217+
}
208218
if q, red := eliminateSingletons(m.P); red != nil {
209219
m.P, m.red = q, red
210220
if len(m.MIPStart) == len(red.orig.Cols) {
@@ -677,6 +687,9 @@ func (m *Model) Solve() Result {
677687
if m.red != nil {
678688
m.red.expand(&res)
679689
}
690+
if m.rrKeep != nil {
691+
expandRows(&res, m.rrKeep, m.rrRows)
692+
}
680693
return res
681694
}
682695

0 commit comments

Comments
 (0)