HiGHS (via JuMP) decides MIP solution to be infeasible

As part of an optimisation-based control algorithm I solve a series of equally structured problems that only differ by the numerical values. For some “random” instances, I get output such as the following:

MIP has 7 rows; 13 cols; 28 nonzeros; 12 integer variables (12 binary)
Coefficient ranges:
  Matrix  [1e+00, 5e+01]
  Cost    [9e-01, 2e+00]
  Bound   [1e+00, 1e+00]
  RHS     [1e+00, 3e+01]
Presolving model
3 rows, 9 cols, 20 nonzeros  0s
3 rows, 9 cols, 20 nonzeros  0s
Presolve reductions: rows 3(-4); columns 9(-4); nonzeros 20(-8)

Solving MIP model with:
   3 rows
   9 cols (8 binary, 0 integer, 0 implied int., 1 continuous, 0 domain fixed)
   20 nonzeros

Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
     I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
     S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
     Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

        Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work      
Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

 J       0       0         0   0.00%   -inf            5.319081761        Large        0      0      0         0     0.0s
 R       0       0         0   0.00%   0               0.5190817611     100.00%        0      0      0         3     0.0s

87.5% inactive integer columns, restarting
         0       0         0   0.00%   0.5190807611    0.5190807611       0.00%        0      3      0         3     0.0s

Solving report
  Status            Optimal
  Primal bound      0.519080761099
  Dual bound        0.519080761099
  Gap               0% (tolerance: 0.01%)
  P-D integral      0.00566762785555
  Solution status   feasible
                    0.519080761099 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.03
  Max sub-MIP depth 0
  Nodes             0
  Repair LPs        0
  LP iterations     3
                    0 (strong br.)
                    0 (separation)
                    0 (heuristics)
ERROR:   MIP solver claims optimality, but with num/max/sum primal(1/1e-06/1e-06) infeasibilities
ERROR:   Setting model status to Solve error

The last two lines (starting with ERROR) irritate me. HiGHS’s MIP solver solves the problem successfully (optimal and feasible), but then after the fact HiGHS (which “entity”?) judges that the solution actually is NOT feasible?

The thing is that no solution is available in that case, so my algorithm fails.

  • I read some stuff on the internet about related issues, turned “off” presolve. Helped in some cases, but the problem still occurs randomly.
  • Then I also deactivated restarts (mip_allow_restart = false). This helped in many cases. But then I still hit the same problem again.
  • Then I set the integer variables to the starting value of the previous iteration on the control problem. With all three measures, I now, on the test suite, don’t get the issue anymore. But I am concerned that the actual, underlying problem is not solved and for some combination of numerical values I accidentally do not test, the problem will still occur.

I did read on feasibility vs. optimality vs. tolerances. But that specific problem with the “after-the-fact infeasibility detection” is not mentioned anywhere. A also cannot find the error-text string anywhere in the HiGHS codebase. So I’m a bit lost right now…

Thanks for any help in advance!


Details on problem characteristic:

  • There is some switched actuators that may take several discrete values.
  • I model each setting as a binary variable z_i and add a constraint that they sum to 1.
  • There is switching costs, so I add a switching penalty to the objective, which is the sum of all z_i for which the previous value was false, times a penalty factor (that is adapted over time, individually for each actuator).
  • The actual objective is the absolute value of a tracking error (auxiliary variable to model the absolute value). The tracking error is a weighted sum of all actuator’s binary variables (the weights being the output of the respective actuator state), minus the target value.
2 Likes

Why not just provide the MIP formulation? Given it is small.

Perhaps Oscar will have a more detailed answer. But in my experience, such issues are almost always caused by poor scaling of the problem. Can you try MathOptAnalyzer.jl and see if it detects any issues for the problematic “random” instances of your model?

1 Like

Hi @asprionj, can you share the instance?

JuMP.write_to_file(model, "model.mps")

What is the output of solution_summary(model)?

Hey thanks for all the quick replies!

Here an mps file (cannot directly upload it) that leads to said error, irrespective of the settings for presolve and mip_allow_restart.

NAME
ROWS
 N  OBJ
 G  c1_1
 G  c2_1
 E  c1
 E  c2
 E  c3
 E  c4
 E  c5
COLUMNS
    MARKER    'MARKER'                 'INTORG'
    PV_pv_50_z_0 c1     1
    PV_pv_50_z_1 c1_1   -50
    PV_pv_50_z_1 c2_1   50
    PV_pv_50_z_1 c1     1
    PV_pv_20_z_0 c2     1
    PV_pv_20_z_1 c1_1   -20
    PV_pv_20_z_1 c2_1   20
    PV_pv_20_z_1 c2     1
    PV_pv_20_z_1 OBJ    0.36
    PV_pv_10_z_0 c3     1
    PV_pv_10_z_1 c1_1   -10
    PV_pv_10_z_1 c2_1   10
    PV_pv_10_z_1 c3     1
    PV_pv_10_z_1 OBJ    0.52
    PV_pv_5_z_0 c4      1
    PV_pv_5_z_1 c1_1    -5
    PV_pv_5_z_1 c2_1    5
    PV_pv_5_z_1 c4      1
    PV_pv_5_z_1 OBJ     0.44
    PV_pv_f_25_z_0 c5   1
    PV_pv_f_25_z_0 OBJ  1.64
    PV_pv_f_25_z_2 c1_1 -15
    PV_pv_f_25_z_2 c2_1 15
    PV_pv_f_25_z_2 c5   1
    PV_pv_f_25_z_2 OBJ  1.64
    PV_pv_f_25_z_1 c1_1 -7.5
    PV_pv_f_25_z_1 c2_1 7.5
    PV_pv_f_25_z_1 c5   1
    PV_pv_f_25_z_1 OBJ  1.64
    PV_pv_f_25_z_3 c1_1 -25
    PV_pv_f_25_z_3 c2_1 25
    PV_pv_f_25_z_3 c5   1
    MARKER    'MARKER'                 'INTEND'
    abs_error c1_1      1
    abs_error c2_1      1
    abs_error OBJ       1
RHS
    rhs       c1_1      -25.42358691534256
    rhs       c2_1      25.42358691534256
    rhs       c1        1
    rhs       c2        1
    rhs       c3        1
    rhs       c4        1
    rhs       c5        1
RANGES
BOUNDS
 BV bounds    PV_pv_50_z_0
 BV bounds    PV_pv_50_z_1
 BV bounds    PV_pv_20_z_0
 BV bounds    PV_pv_20_z_1
 BV bounds    PV_pv_10_z_0
 BV bounds    PV_pv_10_z_1
 BV bounds    PV_pv_5_z_0
 BV bounds    PV_pv_5_z_1
 BV bounds    PV_pv_f_25_z_0
 BV bounds    PV_pv_f_25_z_2
 BV bounds    PV_pv_f_25_z_1
 BV bounds    PV_pv_f_25_z_3
 FR bounds    abs_error
ENDATA

Solution summary (basically the same, whether presolve on/off and restarts allowed or now, because there are none in this case anyway):

solution_summary(; result = 1, verbose = false)
├ solver_name          : HiGHS
├ Termination
│ ├ termination_status : OTHER_ERROR
│ ├ result_count       : 0
│ ├ raw_status         : There was an error calling optimize!
│ └ objective_bound    : 0.00000e+00
├ Solution (result = 1)
│ ├ primal_status        : NO_SOLUTION
│ ├ dual_status          : NO_SOLUTION
│ └ relative_gap         : 0.00000e+00
└ Work counters
  ├ solve_time (sec)   : 4.01521e-02
  ├ simplex_iterations : 0
  ├ barrier_iterations : 0
  └ node_count         : 0

BTW, noticed that when serialising the model to file, the possibly specified initial values are not preserved. So it will be had for me to share a case where specifying feasible (and already optimal) initial values causes the error.

This instance is a case when turning off persolve helps:

NAME
ROWS
 N  OBJ
 G  c1_1
 G  c2_1
 E  c1
 E  c2
 E  c3
 E  c4
 E  c5
COLUMNS
    MARKER    'MARKER'                 'INTORG'
    PV_pv_50_z_0 c1     1
    PV_pv_50_z_1 c1_1   -50
    PV_pv_50_z_1 c2_1   50
    PV_pv_50_z_1 c1     1
    PV_pv_20_z_0 c2     1
    PV_pv_20_z_1 c1_1   -20
    PV_pv_20_z_1 c2_1   20
    PV_pv_20_z_1 c2     1
    PV_pv_20_z_1 OBJ    0.76
    PV_pv_10_z_0 c3     1
    PV_pv_10_z_0 OBJ    1.32
    PV_pv_10_z_1 c1_1   -10
    PV_pv_10_z_1 c2_1   10
    PV_pv_10_z_1 c3     1
    PV_pv_5_z_0 c4      1
    PV_pv_5_z_0 OBJ     1.48
    PV_pv_5_z_1 c1_1    -5
    PV_pv_5_z_1 c2_1    5
    PV_pv_5_z_1 c4      1
    PV_pv_f_25_z_0 c5   1
    PV_pv_f_25_z_0 OBJ  1.8
    PV_pv_f_25_z_2 c1_1 -15
    PV_pv_f_25_z_2 c2_1 15
    PV_pv_f_25_z_2 c5   1
    PV_pv_f_25_z_2 OBJ  1.8
    PV_pv_f_25_z_1 c1_1 -7.5
    PV_pv_f_25_z_1 c2_1 7.5
    PV_pv_f_25_z_1 c5   1
    PV_pv_f_25_z_3 c1_1 -25
    PV_pv_f_25_z_3 c2_1 25
    PV_pv_f_25_z_3 c5   1
    PV_pv_f_25_z_3 OBJ  1.8
    MARKER    'MARKER'                 'INTEND'
    abs_error c1_1      1
    abs_error c2_1      1
    abs_error OBJ       1
RHS
    rhs       c1_1      -27.724441628295992
    rhs       c2_1      27.724441628295992
    rhs       c1        1
    rhs       c2        1
    rhs       c3        1
    rhs       c4        1
    rhs       c5        1
RANGES
BOUNDS
 BV bounds    PV_pv_50_z_0
 BV bounds    PV_pv_50_z_1
 BV bounds    PV_pv_20_z_0
 BV bounds    PV_pv_20_z_1
 BV bounds    PV_pv_10_z_0
 BV bounds    PV_pv_10_z_1
 BV bounds    PV_pv_5_z_0
 BV bounds    PV_pv_5_z_1
 BV bounds    PV_pv_f_25_z_0
 BV bounds    PV_pv_f_25_z_2
 BV bounds    PV_pv_f_25_z_1
 BV bounds    PV_pv_f_25_z_3
 FR bounds    abs_error
ENDATA

In contrast, this one is solved when presolve is on, but fails when presolve is off:

NAME
ROWS
 N  OBJ
 G  c1_1
 G  c2_1
 E  c1
 E  c2
 E  c3
 E  c4
 E  c5
COLUMNS
    MARKER    'MARKER'                 'INTORG'
    PV_pv_50_z_0 c1     1
    PV_pv_50_z_1 c1_1   -50
    PV_pv_50_z_1 c2_1   50
    PV_pv_50_z_1 c1     1
    PV_pv_20_z_0 c2     1
    PV_pv_20_z_0 OBJ    0.5999999999999999
    PV_pv_20_z_1 c1_1   -20
    PV_pv_20_z_1 c2_1   20
    PV_pv_20_z_1 c2     1
    PV_pv_10_z_0 c3     1
    PV_pv_10_z_1 c1_1   -10
    PV_pv_10_z_1 c2_1   10
    PV_pv_10_z_1 c3     1
    PV_pv_10_z_1 OBJ    1.08
    PV_pv_5_z_0 c4      1
    PV_pv_5_z_1 c1_1    -5
    PV_pv_5_z_1 c2_1    5
    PV_pv_5_z_1 c4      1
    PV_pv_5_z_1 OBJ     0.9999999999999999
    PV_pv_f_25_z_0 c5   1
    PV_pv_f_25_z_0 OBJ  2.76
    PV_pv_f_25_z_2 c1_1 -15
    PV_pv_f_25_z_2 c2_1 15
    PV_pv_f_25_z_2 c5   1
    PV_pv_f_25_z_2 OBJ  2.76
    PV_pv_f_25_z_1 c1_1 -7.5
    PV_pv_f_25_z_1 c2_1 7.5
    PV_pv_f_25_z_1 c5   1
    PV_pv_f_25_z_1 OBJ  2.76
    PV_pv_f_25_z_3 c1_1 -25
    PV_pv_f_25_z_3 c2_1 25
    PV_pv_f_25_z_3 c5   1
    MARKER    'MARKER'                 'INTEND'
    abs_error c1_1      1
    abs_error c2_1      1
    abs_error OBJ       1
RHS
    rhs       c1_1      -29.812692921401403
    rhs       c2_1      29.812692921401403
    rhs       c1        1
    rhs       c2        1
    rhs       c3        1
    rhs       c4        1
    rhs       c5        1
RANGES
BOUNDS
 BV bounds    PV_pv_50_z_0
 BV bounds    PV_pv_50_z_1
 BV bounds    PV_pv_20_z_0
 BV bounds    PV_pv_20_z_1
 BV bounds    PV_pv_10_z_0
 BV bounds    PV_pv_10_z_1
 BV bounds    PV_pv_5_z_0
 BV bounds    PV_pv_5_z_1
 BV bounds    PV_pv_f_25_z_0
 BV bounds    PV_pv_f_25_z_2
 BV bounds    PV_pv_f_25_z_1
 BV bounds    PV_pv_f_25_z_3
 FR bounds    abs_error
ENDATA

Note: in all cases, the objective is exactly the same whether the solution is actually found or then deemed to be infeasible. So, I presume the obtained solution (i.e. values of the primal variables) also is the same – which I cannot check because they are not assigned when the solution is deemed infeasible.

Edit: Just found another corner case, while I was running thousands of instances over lunchtime, with fallbacks to turn off presolve, then MIP restart, etc. The following problem fails with standard settings, without presolve, etc. BUT, I saw that the first thing the solver does is a feasibility jump… and it successfully solves when I just set mip_heuristic_run_feasibility_jump to false. Also here, all “failed” solutions have the exactly same objective though.

NAME
ROWS
 N  OBJ
 G  c1_1
 G  c2_1
 E  c1
 E  c2
 E  c3
 E  c4
 E  c5
COLUMNS
    MARKER    'MARKER'                 'INTORG'
    PV_pv_50_z_0 c1     1
    PV_pv_50_z_1 c1_1   -50
    PV_pv_50_z_1 c2_1   50
    PV_pv_50_z_1 c1     1
    PV_pv_20_z_0 c2     1
    PV_pv_20_z_0 OBJ    1.2
    PV_pv_20_z_1 c1_1   -20
    PV_pv_20_z_1 c2_1   20
    PV_pv_20_z_1 c2     1
    PV_pv_10_z_0 c3     1
    PV_pv_10_z_0 OBJ    1.9200000000000002
    PV_pv_10_z_1 c1_1   -10
    PV_pv_10_z_1 c2_1   10
    PV_pv_10_z_1 c3     1
    PV_pv_5_z_0 c4      1
    PV_pv_5_z_0 OBJ     1.96
    PV_pv_5_z_1 c1_1    -5
    PV_pv_5_z_1 c2_1    5
    PV_pv_5_z_1 c4      1
    PV_pv_f_25_z_0 c5   1
    PV_pv_f_25_z_0 OBJ  3.4
    PV_pv_f_25_z_2 c1_1 -15
    PV_pv_f_25_z_2 c2_1 15
    PV_pv_f_25_z_2 c5   1
    PV_pv_f_25_z_2 OBJ  3.4
    PV_pv_f_25_z_1 c1_1 -7.5
    PV_pv_f_25_z_1 c2_1 7.5
    PV_pv_f_25_z_1 c5   1
    PV_pv_f_25_z_3 c1_1 -25
    PV_pv_f_25_z_3 c2_1 25
    PV_pv_f_25_z_3 c5   1
    PV_pv_f_25_z_3 OBJ  3.4
    MARKER    'MARKER'                 'INTEND'
    abs_error c1_1      1
    abs_error c2_1      1
    abs_error OBJ       1
RHS
    rhs       c1_1      -28.715601699751804
    rhs       c2_1      28.715601699751804
    rhs       c1        1
    rhs       c2        1
    rhs       c3        1
    rhs       c4        1
    rhs       c5        1
RANGES
BOUNDS
 BV bounds    PV_pv_50_z_0
 BV bounds    PV_pv_50_z_1
 BV bounds    PV_pv_20_z_0
 BV bounds    PV_pv_20_z_1
 BV bounds    PV_pv_10_z_0
 BV bounds    PV_pv_10_z_1
 BV bounds    PV_pv_5_z_0
 BV bounds    PV_pv_5_z_1
 BV bounds    PV_pv_f_25_z_0
 BV bounds    PV_pv_f_25_z_2
 BV bounds    PV_pv_f_25_z_1
 BV bounds    PV_pv_f_25_z_3
 FR bounds    abs_error
ENDATA

FWIW, I re-ran hundred-thousends for sample cases using SCIP.jl, without any issues. It also is perfectly able to solve the cases I provided above. So it seems that the problem formulation and scaling are sound…?

Did you try MathOptAnalyzer.jl?

1 Like

Can you do write_to_file(model, "model.mof.json") then please. I’ll DM you my email so you can share the file.

From the looks of it, there’s nothing too bad about the model. This is both expected solver behaviour, but also arguably a bug in HiGHS that we should fix.

Actually, don’t worry, I can reproduce:

julia> using JuMP, HiGHS
model = re
julia> model = read_from_file("/tmp/bug.mps")
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 13
├ num_constraints: 19
│ ├ AffExpr in MOI.EqualTo{Float64}: 5
│ ├ AffExpr in MOI.GreaterThan{Float64}: 2
│ └ VariableRef in MOI.ZeroOne: 12
└ Names registered in the model: none

julia> set_optimizer(model, HiGHS.Optimizer)

julia> optimize!(model)
Running HiGHS 1.12.0 (git hash: 755a8e027): Copyright (c) 2025 HiGHS under MIT licence terms
MIP has 7 rows; 13 cols; 28 nonzeros; 12 integer variables (12 binary)
Coefficient ranges:
  Matrix  [1e+00, 5e+01]
  Cost    [1e+00, 3e+00]
  Bound   [1e+00, 1e+00]
  RHS     [1e+00, 3e+01]
Presolving model
3 rows, 9 cols, 20 nonzeros  0s
3 rows, 9 cols, 20 nonzeros  0s
Presolve reductions: rows 3(-4); columns 9(-4); nonzeros 20(-8) 

Solving MIP model with:
   3 rows
   9 cols (8 binary, 0 integer, 0 implied int., 1 continuous, 0 domain fixed)
   20 nonzeros

Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
     I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
     S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
     Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

        Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work      
Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

 J       0       0         0   0.00%   -inf            10.2356017         Large        0      0      0         0     0.0s
         0       0         0   0.00%   0.827063898     10.2356017        91.92%        0      0      1         4     0.0s
 C       0       0         0   0.00%   3.312658574     7.4156017         55.33%        6      2      1         5     0.0s
 L       0       0         0   0.00%   3.312658574     5.0956007         34.99%        6      2      1         5     0.0s
         1       0         1 100.00%   5.0956007       5.0956007          0.00%        6      2      1         9     0.0s

Solving report
  Status            Optimal
  Primal bound      5.09560069975
  Dual bound        5.09560069975
  Gap               0% (tolerance: 0.01%)
  P-D integral      0.00492059478195
  Solution status   feasible
                    5.09560069975 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.01
  Max sub-MIP depth 1
  Nodes             1
  Repair LPs        0
  LP iterations     9
                    0 (strong br.)
                    4 (separation)
                    1 (heuristics)
ERROR:   MIP solver claims optimality, but with num/max/sum primal(1/1e-06/1e-06) infeasibilities
ERROR:   Setting model status to Solve error

I’ll take a look.

1 Like

This is just a straight up bug in HiGHS. I’ll talk to the team. Thanks for the reproducible example!

Upstream issue: MIP fails with Solve Error despite finding optimal solution · Issue #2808 · ERGO-Code/HiGHS · GitHub

To work-around this problem, you can relax the feasibility tolerance: set_attribute(model, "mip_feasibility_tolerance", 2e-6).

Perhaps something like this:

using JuMP, HiGHS
model = read_from_file("/tmp/bug.mps")
set_optimizer(model, HiGHS.Optimizer)
set_silent(model)
optimize!(model)
if termination_status(model) == OTHER_ERROR
    set_attribute(model, "mip_feasibility_tolerance", 2e-6)
    optimize!(model)
end
assert_is_solved_and_feasible(model)

Edit: Just an update that this had already been fixed upstream. I’m working on a new release of HiGHS.jl, so it should be out in a day or so.

1 Like

Hey thanks a lot!

I already played around with fallbacks, e.g. turn off presolve, then also feasibility jump, etc.
Feasibility tolerance could be another one, yes. However, if I generally set it to 2e-6, then just other problem instances fail the same way.

Let’s see, once the new version of HiGHS.jl is available, I will run some hundred thousand instances overnight :wink:. Also, I’ll probably keep both SCIP and HiGHS as solvers, one backing up the other.

And no, haven’t given MathOptAnalyzer.jl a shot, but thanks for the hint. I did become aware of it some time ago, then forgot about it again…

For now, since there seems to be just a bug in the solver, I’ll spare me the effort, but keep the tool in the back of my head :+1:.

Yeah it’s super useful but not very well known yet. Solver bugs are quite rare, I always blame my model…

1 Like

Just tested with the new 1.21.0 release of HiGHS.jl, which uses HiGHS_jll.jl (and thus HiGHS) v1.13.0. Ran 10k test cases, no hickups whatsoever (never needed to fall back to try again with changed settings).

Thanks a lot, awesome to have such quick iterations! :rocket::star_struck:

3 Likes