🔬 mipster_diag

Diagnose wrong MIP results — wrong-optimal and wrong-infeasible — in minutes.

mipster_diag is a standalone diagnostic binary shipped alongside mipster. When the solver produces a result that contradicts a certified objective value — claiming optimal at the wrong value, or declaring infeasibility on a feasible problem — mipster_diag automates the debugging process and pinpoints the root cause with clean, actionable output.

First tool to reach for when you suspect a soundness bug. The default flow completes in 1–3 minutes for most instances.

How it works

1
Baseline run
Silently re-solves the instance with default settings to confirm the bug reproduces within the time limit. If the correct result is found, it exits immediately — no bug to debug.
2
Auto-obtain reference solution
If no -sol file is provided, tries several configurations (preprocessing off, etc.) to find a valid incumbent matching the certified objective — needed for the debugCuts pass.
3
debugCuts pass
Re-solves with OsiRowCutDebugger active against the reference solution. Any cut or bound-propagation step that would exclude the known-optimal solution is printed immediately:

bad row N lb <= act <= ub — an invalid row cut
nodeBoundProp BAD FIXING … — a wrong bound-propagation fixing
4
Feature scan (optional, -scan)
Re-solves 13 times, each disabling one cut family or feature (cgraph, nodeBoundProp, gomory, flow, MIR, probing, two-MIR, zero-half, …). Configs that reach the certified objective are marked LEAD — strong suspects. Slow (up to 13 × time-limit), but exhaustive.

Usage

mipster_diag <problem.{mps,lp}[.gz]> -opt <value> [OPTIONS]
OptionDescription
-opt NRequired. Certified optimal objective value.
-sol FILEKnown-good .sol file for the debugCuts pass. If omitted the tool attempts to auto-obtain one.
-time NPer-run wall-clock time limit in seconds (default: 60).
-maxMaximisation problem (default: minimisation).
-p PAR VALExtra solver parameter applied to every run. Repeatable. Use to reproduce bugs triggered by specific settings.
-scanAlso run the 13-config feature-disabling sweep after the debugCuts pass.

Worked example — enlight_hard

Scenario: A bug in node-level bound propagation caused MIPster to declare enlight_hard infeasible, even though the certified optimal is 37. The bug was triggered by the parameters nodeBoundProp on, nodeBoundPropMinDepth 2, nodeBoundPropDepthInterval 3.

Step 1 — run mipster_diag

mipster_diag enlight_hard.mps.gz -opt 37 \
    -p nodeBoundProp on \
    -p nodeBoundPropMinDepth 2 \
    -p nodeBoundPropDepthInterval 3

Step 2 — read the output

mipster_diag Problem: enlight_hard.mps.gz Certified opt: 37 (min) Time limit: 60s per run Extra param: nodeBoundProp on Extra param: nodeBoundPropMinDepth 2 Extra param: nodeBoundPropDepthInterval 3 Baseline run (default settings, 60s)... Baseline: NOSOL bound=27.3333 — proceeding. debugCuts pass — reference: /tmp/enlight_hard_ref.sol Watch for: 'bad row N lb <= act <= ub' invalid row cut 'nodeBoundProp BAD FIXING (phase,rnd)' wrong bound fixing debugCuts: loaded 200 of 200 values from /tmp/enlight_hard_ref.sol nodeBoundProp BAD FIXING (propagation, round 0): col 156 (y#9#2) type=binary old=[1,3] new=[1,1] but optimal has 2 * 156 2 BAD 156 1 <= 2 <= 1 nodeBoundProp BAD FIXING (propagation, round 0): col 175 (y#10#5) type=binary old=[1,2] new=[1,1] but optimal has 2 nodeBoundProp BAD FIXING (propagation, round 0): col 177 (y#10#7) type=binary old=[1,2] new=[1,1] but optimal has 2 * 175 2 * 177 2 BAD 175 1 <= 2 <= 1 BAD 177 1 <= 2 <= 1 debugCuts result: NOSOL obj=1.79769e+308 (certified=37) Tip: if debugCuts output above is not conclusive, rerun with -scan to run 13 feature-disabling configurations.

Step 3 — interpret

The three nodeBoundProp BAD FIXING lines are the smoking gun:

Root cause identified: OsiClpSolverInterface::getColType() was returning a cached column-type array not invalidated by branching bounds. Variables that started as general integers were misclassified as binary and wrongly fixed to [0,1] or [1,1] at B&B nodes. Fixed by calling getColType(true) to force recomputation.

When the debugCuts pass is not conclusive

If no bad row or BAD FIXING lines appear, the bug may be in a feature that the debugger doesn't trace directly (e.g. branching, heuristics, preprocessing). Add -scan to run the 13-config sweep:

mipster_diag problem.mps -opt <N> -sol /tmp/ref.sol -scan

Each config result is printed on one line:

[DIAG 1/13] no-cgraph → not proven obj=8235 bound=7620 gap=7.5% (time limit) [DIAG 8/13] no-flow → not proven obj=8181 bound=8050 gap=1.6% (time limit) *** LEAD *** [DIAG 12/13] no-twomir → WRONG obj=8204 (same wrong result) [DIAG 13/13] no-all-cuts → OK obj=8181 CORRECT (certified=8181)

Tips

Bug reproduces only with specific settings? Pass them via -p:

mipster_diag problem.mps -opt 37 \
    -p nodeBoundProp on \
    -p nodeBoundPropMinDepth 2 \
    -p nodeBoundPropDepthInterval 3

Hard instance that needs more time? Increase -time:

mipster_diag problem.mps -opt <N> -time 300

Already have a reference solution? Skip auto-obtain with -sol to save time. Obtain one with preprocessing disabled:

mipster problem.mps -preprocess off -solve -solu /tmp/ref.sol
mipster_diag problem.mps -opt <N> -sol /tmp/ref.sol

🔍 mipster_validate_sol

Validate MIP solution feasibility — bounds, integrality, constraints, and objective.

mipster_validate_sol checks whether a solution file (.sol) is truly feasible for a given MIP problem. It is useful for:

Usage

mipster_validate_sol <problem.{mps,lp}[.gz]> <solution.sol>
Exit code Meaning
0Feasible — all checks pass within tolerance
1Infeasible — one or more violations found
2File/usage error

What is checked

The tool loads the problem model (without solving) and computes:

  1. Variable boundslb ≤ x[i] ≤ ub for every variable
  2. Integrality|x[i] − round(x[i])| ≤ int_tol for integer/binary variables
  3. Row activitiesrowLB ≤ Ax ≤ rowUB for every constraint
  4. Objective value — computed c·x vs. the value claimed in the .sol header

Default tolerances:

primal (bounds / constraints) = 1e-5 (scaled)
integrality                   = 1e-5
objective (relative)          = 1e-6

Worked example — air05

Solve and save a solution, then validate it:

mipster air05.mps.gz -preprocess off -solve -solu /tmp/air05.sol
mipster_validate_sol air05.mps.gz /tmp/air05.sol

Output for a valid solution:

mipster_validate_sol
  Problem:  air05.mps.gz
  Solution: /tmp/air05.sol

  Tolerances: primal=1e-05  integer=1e-05  objective=1e-06

  Model: 7195 variables (7195 integer),  426 constraints
  Solution file: 66 non-zero variables listed

  Claimed status: Optimal   claimed obj = 26374

RESULT: FEASIBLE ✓

  Bounds:       7195 variables — no violations
  Integrality:  7195 integer variables — no violations
  Constraints:  426 rows — no violations
  Objective:    computed=26374  claimed=26374  rel_delta=0.000e+00

─── CLOSEST TO TOLERANCE ────────────────────────────────────────
  Objective discrepancy:      rel_delta=0.000e+00

Output for an infeasible solution (example — one variable modified to 0.3):

RESULT: INFEASIBLE ✗

─── VIOLATIONS FOUND (6) ─────────────────────────────────────
  INTEG: col 2 (x#1#3)  val=0.3  nearest_int=0  error=3.000e-01
  ROW:   row 1 (inner_area_2)  activity=-0.7  bounds=[0, 0]  viol=7.000e-01 [BELOW LB]
  ROW:   row 64 (upper_border_1)  activity=-0.7  bounds=[0, 0]  viol=7.000e-01 [BELOW LB]
  ROW:   row 65 (upper_border_2)  activity=-0.7  bounds=[0, 0]  viol=7.000e-01 [BELOW LB]
  ROW:   row 66 (upper_border_3)  activity=-0.7  bounds=[0, 0]  viol=7.000e-01 [BELOW LB]
  OBJ:   computed=36.3  claimed=37  rel_delta=1.892e-02

Summary:
  Integrality violations:      1
  Constraint violations:       4
  Objective mismatch:          computed=36.3  claimed=37

Scanning experiment results

To validate all solutions saved by an experiment run (when solutions are saved with -solu), use a shell loop:

for sol in /path/to/experiment/*.sol; do
  inst=$(basename "$sol" .sol)
  result=$(mipster_validate_sol "$MIPSTER_INSTANCES/miplib/2017+spp/${inst}.mps.gz" "$sol")
  rc=$?
  if [[ $rc -ne 0 ]]; then
    echo "INFEASIBLE: $inst"
    echo "$result" | grep -E "VIOL|INTEG|ROW|OBJ:"
  fi
done