A toy example triggering Gurobi's Warning: Model contains variables with very large bounds participating in product terms

It’s common to find Gurobi logging the following Warning in QCP

Warning: Model contains variables with very large bounds participating 
in product terms.
Presolve was not able to compute smaller bounds for these variables.
Consider bounding these variables or reformulating the model.

In this post I give a simple example which triggers it

julia> import JuMP, Gurobi

julia> begin
           model = JuMP.Model(Gurobi.Optimizer)
           JuMP.@variable(model, x)
           JuMP.@variable(model, xx)
           JuMP.@constraint(model, xx >= x * x)
           JuMP.@objective(model, Min, xx)
           JuMP.optimize!(model)
       end
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0xf65faed0
Model has 1 quadratic constraint
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  QMatrix range    [1e+00, 1e+00]
  QLMatrix range   [1e+00, 1e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]
Presolve removed 0 rows and 2 columns
Presolve time: 0.00s
Presolve: All rows and columns removed

Barrier solved model in 0 iterations and 0.00 seconds (0.00 work units)
Optimal objective 0.00000000e+00

User-callback calls 24, time in user-callback 0.00 sec

julia> JuMP.solution_summary(model)
Warning: to get QCP duals, please set parameter QCPDual to 1 # 🍅
* Solver : Gurobi

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

* Candidate solution (result #1)
  Primal status      : FEASIBLE_POINT
  Dual status        : NO_SOLUTION
  Objective value    : 0.00000e+00
  Objective bound    : 0.00000e+00

* Work counters
  Solve time (sec)   : 0.00000e+00
  Simplex iterations : 0
  Barrier iterations : 0
  Node count         : 0


julia> 

julia> JuMP.@constraint(model, xx <= x * x);

julia> JuMP.optimize!(model)
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0xfe761c6f
Model has 2 quadratic constraints
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  QMatrix range    [1e+00, 1e+00]
  QLMatrix range   [1e+00, 1e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]

Continuous model is non-convex -- solving as a MIP

Found heuristic solution: objective 0.0000000
Presolve time: 0.00s
Presolved: 1 rows, 3 columns, 2 nonzeros
Presolved model has 1 quadratic constraint(s)
Presolved model has 1 bilinear constraint(s)
Warning: Model contains variables with very large bounds participating
         in product terms.
         Presolve was not able to compute smaller bounds for these variables.
         Consider bounding these variables or reformulating the model.

Variable types: 3 continuous, 0 integer (0 binary)

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

Solution count 1: 0

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

User-callback calls 92, time in user-callback 0.00 sec

julia> JuMP.solution_summary(model)
* Solver : Gurobi

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

* Candidate solution (result #1)
  Primal status      : FEASIBLE_POINT
  Dual status        : NO_SOLUTION
  Objective value    : 0.00000e+00
  Objective bound    : 0.00000e+00
  Relative gap       : 0.00000e+00

* Work counters
  Solve time (sec)   : 2.09999e-02
  Simplex iterations : 0
  Barrier iterations : 0
  Node count         : 0

Would it be a good practice to add some Warning info of solvers also to JuMP.solution_summary’s ? Like the :tomato: does. @odow

This is expected behavior, and we won’t be making any changes because we cannot programmatically access the warning print outs from Gurobi.

It is always best practice to add variable bounds if you know they exist.

Yes, this post is not a question post, it’s an example sharing experience.

I see.

For primal variables which has physical meaning, this might be easier. But if it is a dual one, it’s typically harder.

Following the discussion under this topic, if we want to eliminate this Warning.
We can do JuMP.set_upper_bound(xx, 1).

If we want to enforce bounds on x instead of xx, then we have to enforce both lower and upper bounds, like JuMP.@variable(model, -1 <= x <= 1).

Evidence indicates that:
whether Gurobi opts to print this Warning doesn’t depend on the objective_sence.
But objective_function will certainly affect this.
Look at this example, at Time 1, Gurobi reports no Warning (:green_circle:);
But at Time 2, the Warning is triggered (:red_circle:).

import JuMP, Gurobi
model = JuMP.Model(Gurobi.Optimizer)
JuMP.@variable(model, x); JuMP.@variable(model, xx)
JuMP.@constraint(model, xx >= x * x)
JuMP.@constraint(model, xx <= x * x)
print(model) # Time 1
JuMP.optimize!(model) # 🟢 Time 1
JuMP.@objective(model, JuMP.objective_sense(model), xx)
print(model) # Time 2
JuMP.optimize!(model) # 🔴 Time 2

Therefore the model at Time 2 is different from the model at Time 1.

But this difference cannot be observed via Julia’s print(::JuMP.Model).
@odow I guess this can be improved?

julia> JuMP.objective_sense(model)
FEASIBILITY_SENSE::OptimizationSense = 2

You’ve set the objective to FEASIBILITY_SENSE, but you’ve also set an objective function. This doesn’t make sense. This is defined behavior. Solvers can choose to ignore the objective function, or, like Gurobi, they may not support a true objective sense and default to minimization.

The Time 2 print is correct to ignore the objective function.

I reorganize my expression to make it clearer.
Since in my case I use JuMP + Gurobi
My setting at Time 2 is actually (:one:)

(JuMP's default sense)   xx
subject to
 -x² + xx >= 0
 -x² + xx <= 0

But behind the scenes, Gurobi is going to solve (:two:)

Minimize     xx
subject to
 -x² + xx >= 0
 -x² + xx <= 0

Therefore, I think the existing Julia’s printing (JuMP’s realization)

Feasibility
Subject to
 -x² + xx >= 0
 -x² + xx <= 0

does not reflect the true situation, either in view of :one:, or :two:.

It’s not JuMP’s “default” sense. It is MOI.FEASIBILITY_SENSE, which says that the objective does not exist. You cannot have an objective function and FEASIBILITY_SENSE. But this is a soft restriction in the sense that it is enforced by convention, not code.

We allow setting the objective function after setting the objective sense because we never strictly enforced that the objective sense must be set before the objective function. We probably should have enforced this in MOI v1.0.0, but we didn’t so we can’t change now.

This means we allow:

model = Model()
@variable(model, x)
set_objective_function(model, x)  # Even though the sense is current FEASIBILITY_SENSE
set_objective_sense(model, MIN_SENSE)

the lack of a strict order between the sense and the function means that this works:

model = Model()
@variable(model, x)
set_objective(model, FEASIBILITY_SENSE, x)
set_objective_sense(model, MIN_SENSE)
optimize!(model)

As I said above, setting an objective function and keeping FEASIBILITY_SENSE is undefined behavior. You should not do this.

Now Gurobi.jl could choose delete the objective in MOI.optimize!(::Gurobi.Optimizer) if the sense is still MOI.FEASIBILITY_SENSE. But this could also be a breaking change that I don’t have a strong desire to make.

We could also error in JuMP.set_objective if you provide FEASIBILITY_SENSE, but this could also be breaking.

I’ll admit that the current behavior is a bit muddled, but I think that it’s too late to make large clarifying changes to it.

Therefore, I think the existing Julia’s printing (JuMP’s realization)
does not reflect the true situation

It represents the view that we want people to have of the model. You have set FEASIBILITY_SENSE.

1 Like

Okay, I learn it.

Therefore we have to distinguish these 2 needs:

  1. Do an optimization. In this case we have to specify Max or Min, and set a proper objective_function.
  2. Merely get a feasible solution (or, testify the infeasibility). In this case we have to either (i) never call JuMP.@objective, or (ii) set the objective_function to 0.

Typically the second need can be solved faster.