Skip to content

Slightly improve a tolerance issue in KirlikSayin#198

Merged
odow merged 4 commits intomasterfrom
od/ks
May 5, 2026
Merged

Slightly improve a tolerance issue in KirlikSayin#198
odow merged 4 commits intomasterfrom
od/ks

Conversation

@odow
Copy link
Copy Markdown
Member

@odow odow commented May 5, 2026

From a previous solve, z_k may be a value like z+eps where z in Z and eps is the feasibility tolerance. However, if scalars[k] is integer valued, then presolve may (somewhat reasonably) deduce that this problem is infeasible. Instead of rounding z_k, changing EqualTo to LessThan seemed to work on the instances I have. I don't know why. There is also a weird situation in which Gurobi declared a problem unbounded. I can't reproduce without running the entire thing, but I think it is a mix of presolve proving that there is no finite solution (because its infeasible) and yet there being a "feasible" MIP solution in memory from the previous solve. It comes down to the weird mix of tolerances in MIP starts, presolve, simplex, and this equality constraint. This is most probably a bug in Gurobi, in that it should either report optimal or infeasible. But not unbounded. I've seem something similar in SDDP.jl.

using Revise
using JuMP, Gurobi
import MultiObjectiveAlgorithms as MOA

function build_model(filename)
    data = readlines(filename)
    P = parse(Int, data[1])
    N = parse(Int, data[2])
    C = [
        reduce(vcat, (parse.(Int, split(data[2+p*N+n]))' for n in 1:N))
        for p in 0:(P-1)
    ]
    model = Model()
    @variable(model, x[1:N, 1:N], Bin)
    @constraint(model, [i in 1:N], sum(x[i, :]) == 1)
    @constraint(model, [j in 1:N], sum(x[:, j]) == 1)
    @objective(model, Min, [sum(C[p] .* x) for p in 1:P])
    return model
end

function main()
    filename = "AP_p-3_n-15_ins-1.txt"
    model = build_model(filename)
    set_optimizer(model, () -> MOA.Optimizer(Gurobi.Optimizer))
    set_attribute(model, MOA.Algorithm(), MOA.KirlikSayin())
    optimize!(model)
    return solution_summary(model)
end

AP_p-3_n-15_ins-1.txt

From a previous solve, z_k may be a value like z+eps where z in Z and eps is the
feasibility tolerance. However, if scalars[k] is integer valued, then presolve
may (somewhat reasonably) deduce that this problem is infeasible. Instead of
rounding z_k, changing EqualTo to LessThan seemed to work on the instances I
have. I don't know why. There is also a weird situation in which Gurobi declared
a problem unbounded. I can't reproduce without running the entire thing, but I
think it is a mix of presolve proving that there is no finite solution (because
its infeasible) and yet there being a "feasible" MIP solution in memory from the
previous solve. It comes down to the weird mix of tolerances in MIP starts,
presolve, simplex, and this equality constraint. This is most probably a bug in
Gurobi, in that it should either report optimal or infeasible. But not
unbounded. I've seem something similar in SDDP.jl.
@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

WLS license 722777 - registered to JuMP Development
Optimize a model with 32 rows, 225 columns and 900 nonzeros (Min)
Model fingerprint: 0xda2246a2
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+00, 2e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]

MIP start from previous solve did not produce a new incumbent solution
MIP start from previous solve violates constraint R73 by 1.000000000

Found heuristic solution: objective 178.0000000
Presolve time: 0.00s
Presolved: 32 rows, 225 columns, 842 nonzeros
Variable types: 0 continuous, 225 integer (225 binary)

Root relaxation: objective 5.218182e+01, 41 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0   52.18182    0    6  178.00000   52.18182  70.7%     -    0s
H    0     0                      66.0000000   52.18182  20.9%     -    0s
H    0     0                      56.0000000   52.18182  6.82%     -    0s
*    0     0               0      53.9999981   54.00000  0.00%     -    0s

Cutting planes:
  Gomory: 1
  MIR: 1
  StrongCG: 1
  Mod-K: 4

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

Solution count 4: 54 56 66 178 
No other solutions better than 54

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

User-callback calls 49, time in user-callback 0.00 sec
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0xe081d8a4
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]

Loaded MIP start from previous solve with objective 356

Presolve time: 0.00s

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

Solution count 1: 356 

Model is unbounded
Warning: max constraint violation (1.9158e-06) exceeds tolerance
Best objective 3.560000000000e+02, best bound -, gap -

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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.77%. Comparing base (fcea8f7) to head (58b61f3).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #198   +/-   ##
=======================================
  Coverage   99.77%   99.77%           
=======================================
  Files          12       12           
  Lines        1307     1307           
=======================================
  Hits         1304     1304           
  Misses          3        3           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

@simonbowly here's something of interest. I haven't had any luck with a minimal reproducible example, but look at the second log above. It reports Model is unbounded when there is a constraint violation when it should be infeasible.

Here's the model we're trying to solve: model.mps.txt

Gurobi 13 says it is infeasible:

julia> using Gurobi_jll

julia> run(`$(Gurobi_jll.gurobi_cl()) /tmp/model.mps`)
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
Set parameter LogFile to value "gurobi.log"
Using license file /Users/odow/gurobi.lic
WLS license 722777 - registered to JuMP Development

Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)
Copyright (c) 2025, Gurobi Optimization, LLC

Read MPS format model from file /tmp/model.mps
Reading time = 0.00 seconds
: 33 rows, 225 columns, 1125 nonzeros

Using Gurobi shared library /Users/odow/.julia/artifacts/df29ce14d0a4a14d0ab2ad6233c0c6a9bb76abce/gurobi1300/macos_universal2/lib/libgurobi130.dylib

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0x2aeb4113
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+02]
Presolve time: 0.00s

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

Solution count 0

Model is infeasible
Best objective -, best bound -, gap -

Gurobi 12 says it is feasible

(base) odow@Mac /tmp % gurobi_cl model.mps
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
Set parameter LogFile to value "gurobi.log"
Using license file /Users/odow/gurobi.lic
WLS license 722777 - registered to JuMP Development

Gurobi Optimizer version 12.0.3 build v12.0.3rc0 (mac64[arm] - Darwin 24.6.0 24G419)
Copyright (c) 2025, Gurobi Optimization, LLC

Read MPS format model from file model.mps
Reading time = 0.00 seconds
: 33 rows, 225 columns, 1125 nonzeros

Using Gurobi shared library /Library/gurobi1203/macos_universal2/lib/libgurobi120.dylib

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros
Model fingerprint: 0x2aeb4113
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+02]
Presolve removed 0 rows and 36 columns
Presolve time: 0.01s
Presolved: 33 rows, 189 columns, 868 nonzeros
Variable types: 0 continuous, 189 integer (189 binary)

Root relaxation: objective 3.344286e+02, 67 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  334.42859    0   11          -  334.42859      -     -    0s
H    0     0                     356.0000059  334.63638  6.00%     -    0s

Cutting planes:
  Gomory: 3
  Cover: 3
  GUB cover: 3
  Zero half: 3
  RLT: 2

Explored 1 nodes (67 simplex iterations) in 0.02 seconds (0.01 work units)
Thread count was 10 (of 10 available processors)

Solution count 1: 356 

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

In no case should it be unbounded.

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

Getting somewhere:

julia> using JuMP, Gurobi

julia> model = read_from_file("/tmp/model.mps")
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 225
├ num_constraints: 258
│ ├ AffExpr in MOI.EqualTo{Float64}: 31
│ ├ AffExpr in MOI.LessThan{Float64}: 2
│ └ VariableRef in MOI.ZeroOne: 225
└ Names registered in the model: none

julia> set_optimizer(model, Gurobi.Optimizer)
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
WLS license 722777 - registered to JuMP Development

julia> c = constraint_by_name(model, "R74")
R74 : 8 x[1,1] + 7 x[2,1] + 13 x[3,1] + 14 x[4,1] + 5 x[5,1] + 12 x[6,1] + 13 x[7,1] + 14 x[8,1] + 20 x[9,1] + 12 x[10,1] + 20 x[11,1] + 15 x[12,1] + 11 x[13,1] + 3 x[14,1] + 8 x[15,1] + 14 x[1,2] + 14 x[2,2] + 3 x[3,2] + 8 x[4,2] + 10 x[5,2] + 11 x[6,2] + 14 x[7,2] + 18 x[8,2] + 8 x[9,2] + 6 x[10,2] + 4 x[11,2] + 14 x[12,2] + 11 x[13,2] + 12 x[14,2] + 20 x[15,2] + [[...165 terms omitted...]] + 15 x[1,14] + 16 x[2,14] + x[3,14] + 18 x[4,14] + 18 x[5,14] + 9 x[6,14] + 7 x[7,14] + 16 x[8,14] + 12 x[9,14] + 5 x[10,14] + 2 x[11,14] + 11 x[12,14] + 18 x[13,14] + 9 x[14,14] + 16 x[15,14] + 7 x[1,15] + 12 x[2,15] + 10 x[3,15] + 18 x[4,15] + 5 x[5,15] + 9 x[6,15] + 10 x[7,15] + 3 x[8,15] + 6 x[9,15] + 17 x[10,15] + 18 x[11,15] + 4 x[12,15] + 18 x[13,15] + 12 x[14,15] + 16 x[15,15] = 53.99999808424983

julia> old_rhs = normalized_rhs(c)
53.99999808424983

julia> set_normalized_rhs(c, round(Int, old_rhs))

julia> optimize!(model)
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0x82831798
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]
Presolve removed 0 rows and 26 columns
Presolve time: 0.01s
Presolved: 33 rows, 199 columns, 914 nonzeros
Variable types: 0 continuous, 199 integer (199 binary)

Root relaxation: objective 3.344286e+02, 61 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  334.42857    0   11          -  334.42857      -     -    0s
H    0     0                     356.0000000  342.41176  3.82%     -    0s
     0     0  356.00000    0   25  356.00000  356.00000  0.00%     -    0s

Cutting planes:
  Gomory: 3
  Cover: 3
  MIR: 3
  StrongCG: 1
  GUB cover: 3
  Zero half: 3
  RLT: 2

Explored 1 nodes (66 simplex iterations) in 0.01 seconds (0.01 work units)
Thread count was 10 (of 10 available processors)

Solution count 1: 356 

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

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

julia> set_normalized_rhs(c, old_rhs)

julia> optimize!(model)
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0x516d49ba
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]

Loaded MIP start from previous solve with objective 356

Presolve time: 0.00s

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

Solution count 1: 356 

Model is unbounded
Warning: max constraint violation (1.9158e-06) exceeds tolerance
Best objective 3.560000000000e+02, best bound -, gap -

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

@simonbowly
Copy link
Copy Markdown

Regarding the infeasibility, it's right on the edge so can go either way. You can generate an IIS from the model linked above:

gurobi_cl ResultFile=tmp.ilp MOA-198.mps

Looking at tmp.ilp (below) there is one equality constraint where the left-hand side is clearly integral, but the right-hand side is 53.99999808424983. This can't be satisfied with a 1e-6 feasibility tolerance, though it can happen that the infeasibility is spread out across several variables to get a "feasible" solution.

changing EqualTo to LessThan seemed to work on the instances I have

This probably won't work in general. The previous solve found an objective value of 53.999998... and you're now constraining that the next objective (which is also integral) must be less than this value. Gurobi's multi-objective feature applies non-degradation tolerances for exactly this reason. So you probably want MOI.LessThan(zₖ + eps).

I haven't had any luck with a minimal reproducible example

I can't reproduce it either - can you extract a MIP start for the second solve? Setting GURO_PAR_DUMP=1 as a Gurobi parameter will write an mps, prm, and attr file that together should reproduce the solve path using gurobi_cl InputFile=gurobi.prm InputFile=gurobi.attr gurobi.mps.

tmp.ilp contents:

\ 1 rows, 225 columns and 225 nonzeros
\ Variable types: 0 continuous, 225 integer (225 binary)
Minimize
 
Subject To
 R74: 8 x[1,1] + 7 x[2,1] + 13 x[3,1] + 14 x[4,1] + 5 x[5,1] + 12 x[6,1]
   + 13 x[7,1] + 14 x[8,1] + 20 x[9,1] + 12 x[10,1] + 20 x[11,1]
   + 15 x[12,1] + 11 x[13,1] + 3 x[14,1] + 8 x[15,1] + 14 x[1,2]
   + 14 x[2,2] + 3 x[3,2] + 8 x[4,2] + 10 x[5,2] + 11 x[6,2] + 14 x[7,2]
   + 18 x[8,2] + 8 x[9,2] + 6 x[10,2] + 4 x[11,2] + 14 x[12,2] + 11 x[13,2]
   + 12 x[14,2] + 20 x[15,2] + 11 x[1,3] + 9 x[2,3] + 6 x[3,3] + 3 x[4,3]
   + 20 x[5,3] + 12 x[6,3] + 19 x[7,3] + x[8,3] + 16 x[9,3] + 4 x[10,3]
   + 4 x[11,3] + 4 x[12,3] + 7 x[13,3] + 11 x[14,3] + 3 x[15,3] + 6 x[1,4]
   + 17 x[2,4] + 3 x[3,4] + 2 x[4,4] + 10 x[5,4] + 15 x[6,4] + 12 x[7,4]
   + 6 x[8,4] + 18 x[9,4] + 13 x[10,4] + 16 x[11,4] + 18 x[12,4]
   + 15 x[13,4] + 6 x[14,4] + 15 x[15,4] + 20 x[1,5] + 18 x[2,5] + 9 x[3,5]
   + 4 x[4,5] + 13 x[5,5] + 13 x[6,5] + 3 x[7,5] + 12 x[8,5] + 20 x[9,5]
   + 11 x[10,5] + 2 x[11,5] + 6 x[12,5] + 7 x[13,5] + 10 x[14,5]
   + 14 x[15,5] + 16 x[1,6] + 11 x[2,6] + 5 x[3,6] + x[4,6] + 9 x[5,6]
   + 16 x[6,6] + 11 x[7,6] + 14 x[8,6] + 19 x[9,6] + 15 x[10,6] + 3 x[11,6]
   + 14 x[12,6] + 8 x[13,6] + 17 x[14,6] + 9 x[15,6] + 8 x[1,7] + 20 x[2,7]
   + 19 x[3,7] + 12 x[4,7] + 15 x[5,7] + 15 x[6,7] + 20 x[7,7] + 2 x[8,7]
   + 9 x[9,7] + 6 x[10,7] + 14 x[11,7] + 13 x[12,7] + 18 x[13,7]
   + 20 x[14,7] + 2 x[15,7] + 8 x[1,8] + 20 x[2,8] + 16 x[3,8] + 4 x[4,8]
   + 12 x[5,8] + 5 x[6,8] + 17 x[7,8] + 7 x[8,8] + 20 x[9,8] + 13 x[10,8]
   + 10 x[11,8] + 11 x[12,8] + x[13,8] + 2 x[14,8] + 11 x[15,8] + x[1,9]
   + 20 x[2,9] + 13 x[3,9] + 20 x[4,9] + 4 x[5,9] + 20 x[6,9] + 2 x[7,9]
   + 18 x[8,9] + 16 x[9,9] + 2 x[10,9] + 3 x[11,9] + 6 x[12,9] + 18 x[13,9]
   + 7 x[14,9] + 9 x[15,9] + 5 x[1,10] + 8 x[2,10] + 20 x[3,10]
   + 12 x[4,10] + 8 x[5,10] + 14 x[6,10] + 4 x[7,10] + x[8,10] + 10 x[9,10]
   + 4 x[10,10] + 10 x[11,10] + 14 x[12,10] + 20 x[13,10] + 6 x[14,10]
   + 20 x[15,10] + 3 x[1,11] + 2 x[2,11] + x[3,11] + 11 x[4,11]
   + 11 x[5,11] + 17 x[6,11] + 5 x[7,11] + 20 x[8,11] + 4 x[9,11]
   + 13 x[10,11] + 20 x[11,11] + 14 x[12,11] + 10 x[13,11] + 15 x[14,11]
   + 11 x[15,11] + 8 x[1,12] + x[2,12] + 15 x[3,12] + 2 x[4,12] + 5 x[5,12]
   + 11 x[6,12] + 13 x[7,12] + 14 x[8,12] + 20 x[9,12] + 2 x[10,12]
   + 6 x[11,12] + 19 x[12,12] + 17 x[13,12] + x[14,12] + 18 x[15,12]
   + 20 x[1,13] + 7 x[2,13] + 8 x[3,13] + 13 x[4,13] + 2 x[5,13]
   + 16 x[6,13] + 9 x[7,13] + 12 x[8,13] + 3 x[9,13] + 18 x[10,13]
   + 9 x[11,13] + 15 x[12,13] + 6 x[13,13] + 5 x[14,13] + 16 x[15,13]
   + 15 x[1,14] + 16 x[2,14] + x[3,14] + 18 x[4,14] + 18 x[5,14]
   + 9 x[6,14] + 7 x[7,14] + 16 x[8,14] + 12 x[9,14] + 5 x[10,14]
   + 2 x[11,14] + 11 x[12,14] + 18 x[13,14] + 9 x[14,14] + 16 x[15,14]
   + 7 x[1,15] + 12 x[2,15] + 10 x[3,15] + 18 x[4,15] + 5 x[5,15]
   + 9 x[6,15] + 10 x[7,15] + 3 x[8,15] + 6 x[9,15] + 17 x[10,15]
   + 18 x[11,15] + 4 x[12,15] + 18 x[13,15] + 12 x[14,15] + 16 x[15,15]
   = 53.99999808424983
Bounds
Binaries
 x[1,1] x[2,1] x[3,1] x[4,1] x[5,1] x[6,1] x[7,1] x[8,1] x[9,1] x[10,1]
 x[11,1] x[12,1] x[13,1] x[14,1] x[15,1] x[1,2] x[2,2] x[3,2] x[4,2] x[5,2]
 x[6,2] x[7,2] x[8,2] x[9,2] x[10,2] x[11,2] x[12,2] x[13,2] x[14,2]
 x[15,2] x[1,3] x[2,3] x[3,3] x[4,3] x[5,3] x[6,3] x[7,3] x[8,3] x[9,3]
 x[10,3] x[11,3] x[12,3] x[13,3] x[14,3] x[15,3] x[1,4] x[2,4] x[3,4]
 x[4,4] x[5,4] x[6,4] x[7,4] x[8,4] x[9,4] x[10,4] x[11,4] x[12,4] x[13,4]
 x[14,4] x[15,4] x[1,5] x[2,5] x[3,5] x[4,5] x[5,5] x[6,5] x[7,5] x[8,5]
 x[9,5] x[10,5] x[11,5] x[12,5] x[13,5] x[14,5] x[15,5] x[1,6] x[2,6]
 x[3,6] x[4,6] x[5,6] x[6,6] x[7,6] x[8,6] x[9,6] x[10,6] x[11,6] x[12,6]
 x[13,6] x[14,6] x[15,6] x[1,7] x[2,7] x[3,7] x[4,7] x[5,7] x[6,7] x[7,7]
 x[8,7] x[9,7] x[10,7] x[11,7] x[12,7] x[13,7] x[14,7] x[15,7] x[1,8]
 x[2,8] x[3,8] x[4,8] x[5,8] x[6,8] x[7,8] x[8,8] x[9,8] x[10,8] x[11,8]
 x[12,8] x[13,8] x[14,8] x[15,8] x[1,9] x[2,9] x[3,9] x[4,9] x[5,9] x[6,9]
 x[7,9] x[8,9] x[9,9] x[10,9] x[11,9] x[12,9] x[13,9] x[14,9] x[15,9]
 x[1,10] x[2,10] x[3,10] x[4,10] x[5,10] x[6,10] x[7,10] x[8,10] x[9,10]
 x[10,10] x[11,10] x[12,10] x[13,10] x[14,10] x[15,10] x[1,11] x[2,11]
 x[3,11] x[4,11] x[5,11] x[6,11] x[7,11] x[8,11] x[9,11] x[10,11] x[11,11]
 x[12,11] x[13,11] x[14,11] x[15,11] x[1,12] x[2,12] x[3,12] x[4,12]
 x[5,12] x[6,12] x[7,12] x[8,12] x[9,12] x[10,12] x[11,12] x[12,12]
 x[13,12] x[14,12] x[15,12] x[1,13] x[2,13] x[3,13] x[4,13] x[5,13] x[6,13]
 x[7,13] x[8,13] x[9,13] x[10,13] x[11,13] x[12,13] x[13,13] x[14,13]
 x[15,13] x[1,14] x[2,14] x[3,14] x[4,14] x[5,14] x[6,14] x[7,14] x[8,14]
 x[9,14] x[10,14] x[11,14] x[12,14] x[13,14] x[14,14] x[15,14] x[1,15]
 x[2,15] x[3,15] x[4,15] x[5,15] x[6,15] x[7,15] x[8,15] x[9,15] x[10,15]
 x[11,15] x[12,15] x[13,15] x[14,15] x[15,15]
End

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

Yeah no issue with the optimal/infeasible decision. Understandable and expected. The issue is the "unbounded" status.

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

This is consistently reproducible for me:

model.mps.txt

julia> using JuMP, Gurobi

julia> model = read_from_file("/tmp/model.mps")
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 225
├ num_constraints: 258
│ ├ AffExpr in MOI.EqualTo{Float64}: 31
│ ├ AffExpr in MOI.LessThan{Float64}: 2
│ └ VariableRef in MOI.ZeroOne: 225
└ Names registered in the model: none

julia> set_optimizer(model, Gurobi.Optimizer)
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
WLS license 722777 - registered to JuMP Development

julia> c = constraint_by_name(model, "R74")
R74 : 8 x[1,1] + 7 x[2,1] + 13 x[3,1] + 14 x[4,1] + 5 x[5,1] + 12 x[6,1] + 13 x[7,1] + 14 x[8,1] + 20 x[9,1] + 12 x[10,1] + 20 x[11,1] + 15 x[12,1] + 11 x[13,1] + 3 x[14,1] + 8 x[15,1] + 14 x[1,2] + 14 x[2,2] + 3 x[3,2] + 8 x[4,2] + 10 x[5,2] + 11 x[6,2] + 14 x[7,2] + 18 x[8,2] + 8 x[9,2] + 6 x[10,2] + 4 x[11,2] + 14 x[12,2] + 11 x[13,2] + 12 x[14,2] + 20 x[15,2] + [[...165 terms omitted...]] + 15 x[1,14] + 16 x[2,14] + x[3,14] + 18 x[4,14] + 18 x[5,14] + 9 x[6,14] + 7 x[7,14] + 16 x[8,14] + 12 x[9,14] + 5 x[10,14] + 2 x[11,14] + 11 x[12,14] + 18 x[13,14] + 9 x[14,14] + 16 x[15,14] + 7 x[1,15] + 12 x[2,15] + 10 x[3,15] + 18 x[4,15] + 5 x[5,15] + 9 x[6,15] + 10 x[7,15] + 3 x[8,15] + 6 x[9,15] + 17 x[10,15] + 18 x[11,15] + 4 x[12,15] + 18 x[13,15] + 12 x[14,15] + 16 x[15,15] = 53.99999808424983

julia> old_rhs = normalized_rhs(c)
53.99999808424983

julia> set_normalized_rhs(c, round(Int, old_rhs))

julia> optimize!(model)
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0x82831798
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]
Presolve removed 0 rows and 26 columns
Presolve time: 0.00s
Presolved: 33 rows, 199 columns, 914 nonzeros
Variable types: 0 continuous, 199 integer (199 binary)

Root relaxation: objective 3.344286e+02, 61 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  334.42857    0   11          -  334.42857      -     -    0s
H    0     0                     356.0000000  342.41176  3.82%     -    0s
     0     0  356.00000    0   25  356.00000  356.00000  0.00%     -    0s

Cutting planes:
  Gomory: 3
  Cover: 3
  MIR: 3
  StrongCG: 1
  GUB cover: 3
  Zero half: 3
  RLT: 2

Explored 1 nodes (66 simplex iterations) in 0.01 seconds (0.01 work units)
Thread count was 10 (of 10 available processors)

Solution count 1: 356 

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

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

julia> set_normalized_rhs(c, old_rhs)

julia> optimize!(model)
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0x516d49ba
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]

Loaded MIP start from previous solve with objective 356

Presolve time: 0.00s

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

Solution count 1: 356 

Model is unbounded
Warning: max constraint violation (1.9158e-06) exceeds tolerance
Best objective 3.560000000000e+02, best bound -, gap -

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

julia> solution_summary(model)
solution_summary(; result = 1, verbose = false)
├ solver_name          : Gurobi
├ Termination
│ ├ termination_status : DUAL_INFEASIBLE
│ ├ result_count       : 1
│ └ raw_status         : Model was proven to be unbounded. Important note: an unbounded status indicates the presence of an unbounded ray that allows the objective to improve without limit. It says nothing about whether the model has a feasible solution. If you require information on feasibility, you should set the objective to zero and reoptimize.
├ Solution (result = 1)
│ ├ primal_status        : UNKNOWN_RESULT_STATUS
│ ├ dual_status          : NO_SOLUTION
│ └ objective_value      : 3.56000e+02
└ Work counters
  ├ solve_time (sec)   : 1.69039e-04
  ├ simplex_iterations : 0
  ├ barrier_iterations : 0
  └ node_count         : 0

Comment thread src/algorithms/KirlikSayin.jl
@simonbowly
Copy link
Copy Markdown

simonbowly commented May 5, 2026

Thanks, I can reproduce it, but not outside of Julia/JuMP. I'll see what I can find.

Any chance our MPS readers are disagreeing again (the initial model fingerprints are different)?

> julia --version
julia version 1.12.1

> cat Project.toml
[deps]
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

> cat repro.jl
using JuMP, Gurobi
model = read_from_file("MOA-198.mps")
set_optimizer(model, Gurobi.Optimizer)
c = constraint_by_name(model, "R74")
old_rhs = normalized_rhs(c)
set_normalized_rhs(c, round(Int, old_rhs))
optimize!(model)
set_normalized_rhs(c, old_rhs)
optimize!(model)
solution_summary(model)

> julia --project=. repro.jl
...
Model fingerprint: 0x82831798
...
Model fingerprint: 0x516d49ba
...
Model is unbounded
Warning: max constraint violation (1.9158e-06) exceeds tolerance
Best objective 3.560000000000e+02, best bound -, gap -

User-callback calls 28, time in user-callback 0.00 sec
> cat repro.py
import gurobipy as gp

with gp.Env() as env, gp.read("MOA-198.mps", env=env) as model:
    c = model.getConstrByName("R74")
    old_rhs = c.RHS
    c.RHS = int(round(old_rhs))
    model.optimize()
    c.RHS = old_rhs
    model.optimize()
> uv run --python 3.13 --with gurobipy==13.0.0 repro.py
...
Model fingerprint: 0x592fbae3
...
Model fingerprint: 0x2aeb4113
...
Optimal solution found (tolerance 1.00e-04)
Warning: max constraint violation (1.9158e-06) exceeds tolerance
Best objective 3.560000000000e+02, best bound 3.560000000000e+02, gap 0.0000%

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

Any chance our MPS readers are disagreeing again (the initial model fingerprints are different)?

Yes. JuMP annoyingly doesn't preserve row order when reading MPS files.

Let me make you Gurobi's MPS:

using JuMP, Gurobi
model = read_from_file("/tmp/model.mps")
set_optimizer(model, Gurobi.Optimizer)
MOI.Utilities.attach_optimizer(model)
GRBwrite(unsafe_backend(model), "/tmp/model-grb.mps")

model-grb.mps.txt

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

Something is very unusual. GRBwrite and then read doesn't seem to preserve the model either.

julia> model = read_from_file("/tmp/model.mps")
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 225
├ num_constraints: 258
│ ├ AffExpr in MOI.EqualTo{Float64}: 31
│ ├ AffExpr in MOI.LessThan{Float64}: 2
│ └ VariableRef in MOI.ZeroOne: 225
└ Names registered in the model: none

julia> set_optimizer(model, Gurobi.Optimizer)
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
WLS license 722777 - registered to JuMP Development

julia> optimize!(model)
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0x516d49ba
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 3e+02]
Presolve time: 0.00s

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

Solution count 0

Model is infeasible or unbounded
Best objective -, best bound -, gap -

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

julia> GRBwrite(unsafe_backend(model), "/tmp/bug-198.mps")
0

julia> env = Gurobi.Env()
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
WLS license 722777 - registered to JuMP Development
Gurobi.Env(Ptr{Nothing}(0x000000012ce4e200), false, 0)

julia> modelP = Ref{Ptr{Cvoid}}(C_NULL)
Base.RefValue{Ptr{Nothing}}(Ptr{Nothing}(0x0000000000000000))

julia> @assert GRBreadmodel(env, "/tmp/bug-198.mps", modelP) == 0
Read MPS format model from file /tmp/bug-198.mps
Reading time = 0.00 seconds
: 33 rows, 225 columns, 1125 nonzeros

julia> model = modelP[]
Ptr{Nothing}(0x000000012c3f9370)

julia> GRBoptimize(model)
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 24.6.0 24G419)

CPU model: Apple M4
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads

WLS license 722777 - registered to JuMP Development
Optimize a model with 33 rows, 225 columns and 1125 nonzeros (Min)
Model fingerprint: 0xe6a04753
Model has 225 linear objective coefficients
Variable types: 0 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+01, 6e+01]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+02]
Presolve time: 0.00s

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

Solution count 0

Model is infeasible
Best objective -, best bound -, gap -
0

julia> GRBfree(model)

@odow odow mentioned this pull request May 5, 2026
Comment thread src/algorithms/KirlikSayin.jl Outdated
inner,
scalars[k],
MOI.EqualTo(zₖ),
MOI.LessThan(zₖ + 1e-5),
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So with this and my benchmark, I now get a few extra solutions. For example, there are these mostly equivalent solutions:

julia> Y[68], Y[62]
([54.00000098502844, 128.9999926496984, 127.00000464334889], [54.0, 129.0, 127.0])

I really need to go away and rethink how we handle tolerances across all of the algorithms. Things are much more complicated when we want to return multiple solutions than the hierarchical algorithm implemented by Gurobi.

Comment thread src/algorithms/KirlikSayin.jl Outdated
@torressa
Copy link
Copy Markdown

torressa commented May 5, 2026

The inconsistency is coming from Gurobi.jl defining binary variables to be "free" (this is excluded when writing an MPS file from Gurobi, it seems). This leads to wrong conclusion of unboundedness. I set bounds when adding binary variables see: jump-dev/Gurobi.jl@17aa158 this seems to work. We will also fix the inconsistency on our side.

@odow odow merged commit 2f9eb7d into master May 5, 2026
6 checks passed
@odow odow deleted the od/ks branch May 5, 2026 20:53
@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

We will also fix the inconsistency on our side

The bounds when writing MPS files and/or the bug in reporting unbounded?

I've opened a PR to Gurobi.jl with your change. It probably needs some changes to catch the intersection with other bounds.

@odow
Copy link
Copy Markdown
Member Author

odow commented May 5, 2026

Inspired by @torressa's PR, another way to "fix" this was to add variable bounds to the binary variables:

function build_model(filename)
    data = readlines(filename)
    P = parse(Int, data[1])
    N = parse(Int, data[2])
    C = [
        reduce(vcat, (parse.(Int, split(data[2+p*N+n]))' for n in 1:N))
        for p in 0:(P-1)
    ]
    model = Model()
    @variable(model, 0 <= x[1:N, 1:N] <= 1, Bin)
    @constraint(model, [i in 1:N], sum(x[i, :]) == 1)
    @constraint(model, [j in 1:N], sum(x[:, j]) == 1)
    @objective(model, Min, [sum(C[p] .* x) for p in 1:P])
    return model
end

Then, even without this PR, we get the expected 878 solutions:

solution_summary(; result = 1, verbose = false)
├ solver_name          : MOA[algorithm=MultiObjectiveAlgorithms.KirlikSayin, optimizer=Gurobi]
├ Termination
│ ├ termination_status : OPTIMAL
│ ├ result_count       : 878
│ ├ raw_status         : Solve complete. Found 878 solution(s)
│ └ objective_bound    : [4.30000e+01,4.70000e+01,3.60000e+01]
├ Solution (result = 1)
│ ├ primal_status        : FEASIBLE_POINT
│ ├ dual_status          : NO_SOLUTION
│ └ objective_value      : [4.30000e+01,1.28000e+02,1.76000e+02]
└ Work counters
  └ solve_time (sec)   : 2.44321e+01

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants