HiGHS: Bug in presolve leads to NaN in solution

This is probably user error, but I’d like to know what I’m doing wrong/how the problem is being set up differently.

In Julia, I get

julia> using JuMP, HiGHS

julia> function run_model(verbose = true)
           model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => verbose))
           @variable(model, v_0 >= -1_000_000_000, Int)
           @variable(model, v_1 >= -1_000_000_000, Int)
           @variable(model, v_2 >= -1_000_000_000, Int)
           @variable(model, v_3 >= -1_000_000_000, Int)
           @variable(model, v_4 >= -1_000_000_000, Int)
           @variable(model, v_5 >= 0, Int)
           @variable(model, v_6 >= 0, Int)
           @variable(model, v_7 >= 0, Int)
           @objective(model, Max, 2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7)
           @constraint(model, -v_3 - v_4 - v_5 - 2v_6 - 3v_7 <= 0)
           @constraint(model, 2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7 <= 1)
           @constraint(model, v_0 - v_2 - v_6 <= 0)
           @constraint(model, v_1 - v_3 - v_7 <= 0)
           optimize!(model)
       end
run_model (generic function with 2 methods)

julia> m = run_model()
Presolving model
3 rows, 6 cols, 12 nonzeros
1 rows, 2 cols, 2 nonzeros
0 rows, 1 cols, 0 nonzeros
0 rows, 0 cols, 0 nonzeros
Presolve: Optimal

Solving report
  Status            Optimal
  Primal bound      -0
  Dual bound        -0
  Gap               0% (tolerance: 0.01%)
  Solution status   feasible
                    0 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (postsolve)
  Nodes             0
  LP iterations     0 (total)
                    0 (strong br.)
                    0 (separation)
                    0 (heuristics)

It was able to solve the objective, achieving a value of 0. The point of this problem was to check if one of the inequalities in a system was redundant

-v_5 <= 0
-v_6 <= 0
-v_7 <= 0
-v_3 - v_4 - v_5 - 2v_6 - 3v_7 <= 0
v_0 - v_2 - v_6 <= 0
2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7 <= 0
v_1 - v_3 - v_7 <= 0

I.e., is the second to last constraint redundant? To check, I increment the upper bound on the constraint, and then try to maximize it as an objective. If it is able to achieve the incremented upper bound, then the original constraint was necessary. If not, then it is redundant and can be dropped.

A secondary question I have – is there a better way to do this? Particularly from the C++ API?

Note that the redundant constraint here equals 2times the constraint above it plus the constraint below it, but my problems often are not so trivial.

But failing that, this C++ code does not work:

#include <cassert>

#include "Highs.h"

using std::cout;
using std::endl;

int main() {
  // Create and populate a HighsModel instance for the LP
  //
  // Maximize 2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7
  //
  // -v_5 <= 0
  // -v_6 <= 0
  // -v_7 <= 0
  // -v_3 - v_4 - v_5 - 2v_6 - 3v_7 <= 0
  // v_0 - v_2 - v_6 <= 0
  // 2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7 <= 1
  // v_1 - v_3 - v_7 <= 0
  //
  //
  HighsModel model;

  model.lp_.num_col_ = 8;
  model.lp_.num_row_ = 4;
  model.lp_.sense_ = ObjSense::kMaximize;
  model.lp_.offset_ = 0;
  model.lp_.col_cost_ = {2, 1, -2, -1, 0, 0, -2, -1};
  model.lp_.col_lower_ = {-1e+30, -1e+30, -1e+30, -1e+30, -1e+30, 0, 0, 0};
  model.lp_.col_upper_ = {1e+30, 1e+30, 1e+30, 1e+30,
                          1e+30, 1e+30, 1e+30, 1e+30};
  model.lp_.row_lower_ = {-1e+30, -1e+30, -1e+30, -1e+30};
  model.lp_.row_upper_ = {0, 0, 1, 0};
  //
  // Here the orientation of the matrix is row-wise
  model.lp_.a_matrix_.format_ = MatrixFormat::kRowwise;
  // a_start_ has num_col_1 entries, and the last entry is the number
  // of nonzeros in A, allowing the number of nonzeros in the last
  // column to be defined
  model.lp_.a_matrix_.start_ = {0, 5, 8, 14, 17};
  model.lp_.a_matrix_.index_ = {3, 4, 5, 6, 7, 0, 2, 6, 0,
                                1, 2, 3, 6, 7, 1, 3, 7};
  model.lp_.a_matrix_.value_ = {-1, -1, -1, -2, -3, 1, -1, -1, 2,
                                1,  -2, -1, -2, -1, 1, -1, -1};
  model.lp_.integrality_.resize(model.lp_.num_col_, HighsVarType::kInteger);
  //
  // Create a Highs instance
  Highs highs;
  HighsStatus return_status;
  //
  // Pass the model to HiGHS
  return_status = highs.passModel(model);
  assert(return_status == HighsStatus::kOk);
  //
  // Get a const reference to the LP data in HiGHS
  const HighsLp& lp = highs.getLp();
  //
  // Solve the model
  return_status = highs.run();
  assert(return_status == HighsStatus::kOk);
  //
  // Get the model status
  const HighsModelStatus& model_status = highs.getModelStatus();
  assert(model_status == HighsModelStatus::kOptimal);
  cout << "Model status: " << highs.modelStatusToString(model_status) << endl;
  //
  // Get the solution information
  const HighsInfo& info = highs.getInfo();
  cout << "Simplex iteration count: " << info.simplex_iteration_count << endl;
  cout << "Objective function value: " << info.objective_function_value << endl;
  cout << "Primal  solution status: "
       << highs.solutionStatusToString(info.primal_solution_status) << endl;
  cout << "Dual    solution status: "
       << highs.solutionStatusToString(info.dual_solution_status) << endl;
  cout << "Basis: " << highs.basisValidityToString(info.basis_validity) << endl;
  const bool has_values = info.primal_solution_status;
  const bool has_duals = info.dual_solution_status;
  const bool has_basis = info.basis_validity;
  //
  // Get the solution values and basis
  const HighsSolution& solution = highs.getSolution();
  const HighsBasis& basis = highs.getBasis();
  //
  // Report the primal and solution values and basis
  for (int col = 0; col < lp.num_col_; col++) {
    cout << "Column " << col;
    if (has_values) cout << "; value = " << solution.col_value[col];
    if (has_duals) cout << "; dual = " << solution.col_dual[col];
    if (has_basis)
      cout << "; status: " << highs.basisStatusToString(basis.col_status[col]);
    cout << endl;
  }
  for (int row = 0; row < lp.num_row_; row++) {
    cout << "Row    " << row;
    if (has_values) cout << "; value = " << solution.row_value[row];
    if (has_duals) cout << "; dual = " << solution.row_dual[row];
    if (has_basis)
      cout << "; status: " << highs.basisStatusToString(basis.row_status[row]);
    cout << endl;
  }

  return 0;
}

This is modified from the call_highs_from_cpp example.

When I run this, I get:

Running HiGHS 1.2.2 [date: 2022-05-25, git hash: 6a9d90cf]
Copyright (c) 2022 ERGO-Code under MIT licence terms
Cols:           5 lower bounds exceeding       -1e+20 are treated as -Infinity
Cols:           8 upper bounds exceeding        1e+20 are treated as +Infinity
Rows:           4 lower bounds exceeding       -1e+20 are treated as -Infinity
Presolving model
3 rows, 6 cols, 12 nonzeros
1 rows, 2 cols, 2 nonzeros
0 rows, 1 cols, 0 nonzeros
0 rows, 0 cols, 0 nonzeros
Presolve: Optimal

Solving report
  Status            Optimal
  Primal bound      -0
  Dual bound        -0
  Gap               0% (tolerance: 0.01%)
  Solution status   feasible
                    -nan (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (postsolve)
  Nodes             0
  LP iterations     0 (total)
                    0 (strong br.)
                    0 (separation)
                    0 (heuristics)
Model status: Optimal
Simplex iteration count: -1
Objective function value: -nan
Primal  solution status: Feasible
Dual    solution status: None
Basis: Not valid
Column 0; value = -inf
Column 1; value = -inf
Column 2; value = -nan
Column 3; value = -nan
Column 4; value = -inf
Column 5; value = 0
Column 6; value = 0
Column 7; value = 0
Row    0; value = -nan
Row    1; value = -nan
Row    2; value = -nan
Row    3; value = -nan

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Comments: 15 (6 by maintainers)

Most upvoted comments

And for setting up the model in C++ using the version that fills the HighsLp can avoid copying, but the above code does not. It still needs to call

 highs.passModel(std::move(model));

instead of

 highs.passModel(model);

The implied lower bounds explain this.

JuMP doesn’t have implied lower bounds. The variables are free with -Inf, Inf bounds.

There is a bug in postsolve. I did not handle the rare case of duplicate/parallel columns where the columns are free and integer. In the postsolve case I just used the infinite bounds without checking. For LP this usually is no issue as duplicate columns with infinite bounds usually imply a dominated column which is handled differently. Only when there is integrality the two columns are replaced by one merged column and postsolve needs to split the assigned value into the original columns.

The C++ method Highs::passModel uses std::move to avoid copies, but I guessed this wasn’t possible in C when passing pointers.

As for the hot starting, yes, this comes automatically with HiGHS when you add rows or columns after solving an LP with simplex

Okay, so I ran this with Gurobi as comparison:

julia> using JuMP, HiGHS, Gurobi

julia> function run_model(optimizer)
           model = Model(optimizer)
           @variable(model, v_0, Int)
           @variable(model, v_1, Int)
           @variable(model, v_2, Int)
           @variable(model, v_3, Int)
           @variable(model, v_4, Int)
           @variable(model, v_5 >= 0, Int)
           @variable(model, v_6 >= 0, Int)
           @variable(model, v_7 >= 0, Int)
           @objective(model, Max, 2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7)
           @constraint(model, -v_3 - v_4 - v_5 - 2v_6 - 3v_7 <= 0)
           @constraint(model, 2v_0 + v_1 - 2v_2 - v_3 - 2v_6 - v_7 <= 1)
           @constraint(model, v_0 - v_2 - v_6 <= 0)
           @constraint(model, v_1 - v_3 - v_7 <= 0)
           optimize!(model)
           return solution_summary(model; verbose = true)
       end
run_model (generic function with 2 methods)

julia> run_model(Gurobi.Optimizer)
Set parameter Username
Academic license - for non-commercial use only - expires 2022-06-19
Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (mac64[x86])
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
Optimize a model with 4 rows, 8 columns and 17 nonzeros
Model fingerprint: 0xe6b080f0
Variable types: 0 continuous, 8 integer (0 binary)
Coefficient statistics:
  Matrix range     [1e+00, 3e+00]
  Objective range  [1e+00, 2e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 1e+00]
Found heuristic solution: objective -0.0000000
Presolve removed 4 rows and 8 columns
Presolve time: 0.00s
Presolve: All rows and columns removed

Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
Thread count was 1 (of 8 available processors)

Solution count 1: -0 
No other solutions better than -0

Optimal solution found (tolerance 1.00e-04)
Best objective -0.000000000000e+00, best bound -0.000000000000e+00, gap 0.0000%

User-callback calls 182, time in user-callback 0.00 sec
* Solver : Gurobi

* Status
  Termination status : OPTIMAL
  Primal status      : FEASIBLE_POINT
  Dual status        : NO_SOLUTION
  Result count       : 1
  Has duals          : false
  Message from the solver:
  "Model was solved to optimality (subject to tolerances), and an optimal solution is available."

* Candidate solution
  Objective value      : -0.00000e+00
  Objective bound      : -0.00000e+00
  Relative gap         : 0.00000e+00
  Dual objective value : -0.00000e+00
  Primal solution :
    v_0 : 0.00000e+00
    v_1 : 0.00000e+00
    v_2 : 0.00000e+00
    v_3 : 0.00000e+00
    v_4 : 0.00000e+00
    v_5 : 0.00000e+00
    v_6 : -0.00000e+00
    v_7 : -0.00000e+00

* Work counters
  Solve time (sec)   : 6.02961e-04
  Barrier iterations : 0
  Node count         : 0


julia> run_model(HiGHS.Optimizer)
Presolving model
3 rows, 6 cols, 12 nonzeros
1 rows, 2 cols, 2 nonzeros
0 rows, 1 cols, 0 nonzeros
0 rows, 0 cols, 0 nonzeros
Presolve: Optimal

Solving report
  Status            Optimal
  Primal bound      -0
  Dual bound        -0
  Gap               0% (tolerance: 0.01%)
  Solution status   feasible
                    nan (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (postsolve)
  Nodes             0
  LP iterations     0 (total)
                    0 (strong br.)
                    0 (separation)
                    0 (heuristics)
* Solver : HiGHS

* Status
  Termination status : OPTIMAL
  Primal status      : FEASIBLE_POINT
  Dual status        : NO_SOLUTION
  Result count       : 1
  Has duals          : false
  Message from the solver:
  "kHighsModelStatusOptimal"

* Candidate solution
  Objective value      : NaN
  Objective bound      : NaN
  Relative gap         : 0.00000e+00
  Primal solution :
    v_0 : NaN
    v_1 : NaN
    v_2 : NaN
    v_3 : NaN
    v_4 : 0.00000e+00
    v_5 : 0.00000e+00
    v_6 : 0.00000e+00
    v_7 : 0.00000e+00

* Work counters
  Solve time (sec)   : 5.34238e-04
  Simplex iterations : -1
  Barrier iterations : -1
  Node count         : 0

So even your original case was only working because of the large bounds and thus was returning an incorrect solution (one reason not to use fake bounds).

If we turn off presolve, we get:

julia> run_model(optimizer_with_attributes(HiGHS.Optimizer, "presolve" => "off"))

Presolve is switched off
Objective function is integral with scale 1

Solving MIP model with:
   4 rows
   8 cols (0 binary, 8 integer, 0 implied int., 0 continuous)
   17 nonzeros

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

         0       0         0   0.00%   inf             -inf                 inf        0      0      0         0     0.0s
 T       0       0         0   0.00%   inf             -0                 Large        0      0      0         0     0.0s

Solving report
  Status            Optimal
  Primal bound      0
  Dual bound        0
  Gap               0% (tolerance: 0.01%)
  Solution status   feasible
                    0 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (postsolve)
  Nodes             1
  LP iterations     0 (total)
                    0 (strong br.)
                    0 (separation)
                    0 (heuristics)
* Solver : HiGHS

* Status
  Termination status : OPTIMAL
  Primal status      : FEASIBLE_POINT
  Dual status        : NO_SOLUTION
  Result count       : 1
  Has duals          : false
  Message from the solver:
  "kHighsModelStatusOptimal"

* Candidate solution
  Objective value      : 0.00000e+00
  Objective bound      : 0.00000e+00
  Relative gap         : 0.00000e+00
  Primal solution :
    v_0 : 0.00000e+00
    v_1 : 0.00000e+00
    v_2 : -0.00000e+00
    v_3 : -0.00000e+00
    v_4 : 0.00000e+00
    v_5 : 0.00000e+00
    v_6 : 0.00000e+00
    v_7 : 0.00000e+00

* Work counters
  Solve time (sec)   : 1.15691e-03
  Simplex iterations : -1
  Barrier iterations : -1
  Node count         : 1

So this looks like a bug in presolve, and probably does some / 0 that introduces a NaN into the constraint matrix.