Skip to content

Commit a5784fc

Browse files
authored
Merge pull request #23 from evcc-io/rewrite/clp-factorization-core
Clp-core: true Forrest-Tomlin (sparse Hessenberg) + solution-writer & unbounded fixes
2 parents ba19483 + e323dae commit a5784fc

20 files changed

Lines changed: 3324 additions & 283 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ debug.mps
44
.pulpenv/
55
bin/
66
simplex.test
7+
/cbc
8+
/realcbc

README.md

Lines changed: 167 additions & 114 deletions
Large diffs are not rendered by default.

cmd/cbc/main.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
"runtime/pprof"
910
"strconv"
1011
"strings"
1112
"time"
@@ -26,10 +27,25 @@ var valueOnlyFlags = map[string]bool{
2627
}
2728

2829
func main() {
30+
// CBC_CPUPROFILE gates a dev-only CPU profile of the whole solve
31+
if pf := os.Getenv("CBC_CPUPROFILE"); pf != "" {
32+
if f, err := os.Create(pf); err == nil {
33+
pprof.StartCPUProfile(f)
34+
defer pprof.StopCPUProfile()
35+
}
36+
}
2937
if err := run(os.Args[1:]); err != nil {
38+
pprof.StopCPUProfile()
3039
fmt.Fprintln(os.Stderr, err)
3140
os.Exit(1)
3241
}
42+
// CBC_MEMPROFILE gates a dev-only allocation profile of the whole solve
43+
if pf := os.Getenv("CBC_MEMPROFILE"); pf != "" {
44+
if f, err := os.Create(pf); err == nil {
45+
pprof.WriteHeapProfile(f)
46+
f.Close()
47+
}
48+
}
3349
}
3450

3551
func run(args []string) error {
@@ -40,7 +56,9 @@ func run(args []string) error {
4056
var solutionFile, mipsFile string
4157
maximize := false
4258
lpOnly := false
43-
limits := mip.Limits{GapRel: 1e-9, GapAbs: 1e-9}
59+
// GapAbs 1e-5 = CBC's default cutoff increment (CbcCutoffIncrement):
60+
// nodes within 1e-5 of the incumbent are pruned, as real CBC does
61+
limits := mip.Limits{GapRel: 1e-9, GapAbs: 1e-5}
4462

4563
rest := args[1:]
4664
for i := 0; i < len(rest); i++ {

docs/rewrite-clp-core.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Rewrite: Clp-faithful engine core
2+
3+
Goal: reach CBC/Clp wall-clock and pivot-count parity. The existing feature
4+
ports (reliability branching, DSE `updateWeights`, warm cut re-solves, TwoMir)
5+
are matched to source and active, but KPI/time parity is blocked: CBC wins
6+
because its engine is **co-designed** on a reduced model where no vertex is a
7+
razor edge. Retrofitting single features onto cbcgo's full-size model perturbs
8+
the fragile 020 optimum (proven: DSE-everywhere, D1, Markowitz LU, warm cut
9+
re-solves each regress 020 in isolation).
10+
11+
So the parity path is a coordinated core rewrite, not more feature flags.
12+
13+
References (local): `../Clp` (ClpSimplexDual, ClpFactorization,
14+
ClpDualRowSteepest), `../CoinUtils` (CoinFactorization = Forrest-Tomlin LU),
15+
`../cbc` (CbcNode, CglPreProcess wiring).
16+
17+
## Components, in dependency order
18+
19+
1. **Forrest-Tomlin factorization** (`CoinFactorization`) — replace cbcgo's
20+
product-form eta updates with FT (`replaceColumn` / `updateColumnFT`).
21+
Cheaper, more stable updates; enables sparse two-column FTRAN so the DSE
22+
`tau` solve is nearly free. This is the per-pivot cost lever.
23+
2. **Full Markowitz + threshold pivoting** in the LU — sparser kernel, but only
24+
safe once (1) gives FT stability; on the current engine it perturbs 020.
25+
3. **`CglPreProcess` model reduction** — coefficient strengthening + forcing/
26+
redundant-row removal to the ~1375-row model CBC solves. Requires (4) so the
27+
reduced model keeps its cut strength.
28+
4. **Full `Cgl` suite** — Gomory, Probing, Knapsack, Clique, MixedIntegerRound2,
29+
FlowCover, TwoMir, ZeroHalf run every node (CBC frequency), so the reduced
30+
model's bound holds.
31+
5. **`ClpSimplexDual` dual loop** — incremental infeasibility list + partial
32+
pricing (`numberWanted`), Harris ratio, proper degeneracy/perturbation.
33+
34+
Parity is expected only once 1+3+4 land together (the co-design). Each lands
35+
behind a measurement gate and is benchmarked against `../optimizer` golden +
36+
real CBC (`cbc_run.py`) before activation.
37+
38+
## Status
39+
40+
- [x] 1. Forrest-Tomlin factorization — sparse L-col/U-row LU + O(nnz) solves,
41+
alloc-free pooled `replaceColumn`, WIRED behind `CBC_FT` (clone-safe).
42+
NUMERICALLY STABLE: trailing-block Bartels-Golub with PARTIAL PIVOTING,
43+
expressed as a general R-file (interleaved swap + elimination ops),
44+
property-tested; |mult|<=1. Correct end-to-end on the golden suite.
45+
TRUE FT: the trailing-block re-triangularization is now sparse Hessenberg
46+
elimination on the U rows (O(spike + fill), no dense block, no block cap),
47+
with an `ftFillCap` bail to refactorize on pathological fill. ~10x faster
48+
per update than the dense block; PuLP suite under `CBC_FT` matches the
49+
default engine and is no slower. Stays active on large trailing blocks.
50+
- [~] 2. Sparse factorize — shortest-row + largest-entry pivoting, sparse
51+
L-columns + U-rows, no dense work matrix, property-tested. Not yet fast:
52+
needs a singleton pre-pass and bucketed pivot search (currently O(m^2)
53+
scans), and full Markowitz; not yet faster than the tuned dense path.
54+
- [ ] 3. CglPreProcess (large subsystem — not started)
55+
- [x] 4. Cgl generators — 9 of 9: GMI, probing, single-row MIR, TwoMir (active);
56+
knapsack-cover, clique, zero-half, flow-cover (PVRW), lift-and-project
57+
(lifted cover cuts) behind CBC_CGL. All sound (soundness-tested +
58+
golden 13/13 objectives correct with CBC_CGL=1).
59+
- [~] 5. Dual loop — `dual2.go` DSE dual is built and active via the mixed
60+
engine (shipped); Clp partial-pricing / infeasibility-list not ported.

0 commit comments

Comments
 (0)