From 27c8645a8dbf4ac65b8b0627bf6a853332353226 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 19 Feb 2026 16:56:18 +1300 Subject: [PATCH] Use bridges when passing the optimizer to MathOptIIS --- src/Infeasibility/Infeasibility.jl | 3 +- src/Infeasibility/analyze.jl | 123 ++++++++++++++++------------- test/Project.toml | 8 ++ test/test_Infeasibility.jl | 33 ++++++++ 4 files changed, 112 insertions(+), 55 deletions(-) diff --git a/src/Infeasibility/Infeasibility.jl b/src/Infeasibility/Infeasibility.jl index a1870df..f01d169 100644 --- a/src/Infeasibility/Infeasibility.jl +++ b/src/Infeasibility/Infeasibility.jl @@ -5,8 +5,9 @@ module Infeasibility -import MathOptInterface as MOI import MathOptAnalyzer +import MathOptIIS as MOIIS +import MathOptInterface as MOI include("structs.jl") include("analyze.jl") diff --git a/src/Infeasibility/analyze.jl b/src/Infeasibility/analyze.jl index ce00656..0419867 100644 --- a/src/Infeasibility/analyze.jl +++ b/src/Infeasibility/analyze.jl @@ -3,71 +3,86 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. -import MathOptIIS as MOIIS +function _add_result(out::Data, model, iis, meta::MOIIS.BoundsData) + @assert length(iis.constraints) == 2 + err = InfeasibleBounds{Float64}( + MOI.get(model, MOI.ConstraintFunction(), iis.constraints[1]), + meta.lower_bound, + meta.upper_bound, + ) + push!(out.infeasible_bounds, err) + return +end + +function _add_result(out::Data, model, iis, meta::MOIIS.IntegralityData) + @assert length(iis.constraints) >= 2 + err = InfeasibleIntegrality{Float64}( + MOI.get(model, MOI.ConstraintFunction(), iis.constraints[1]), + meta.lower_bound, + meta.upper_bound, + meta.set, + ) + push!(out.infeasible_integrality, err) + return +end + +function _add_result(out::Data, model, iis, meta::MOIIS.RangeData) + @assert length(iis.constraints) >= 1 + for con in iis.constraints + if con isa MOI.ConstraintIndex{MOI.VariableIndex} + continue + end + err = InfeasibleConstraintRange{Float64}( + con, + meta.lower_bound, + meta.upper_bound, + meta.set, + ) + push!(out.constraint_range, err) + break + end + return +end + +function _add_result(out::Data, model, iis, meta) + push!(out.iis, IrreducibleInfeasibleSubset(iis.constraints)) + return +end + +function _instantiate_with_modify(optimizer, ::Type{T}) where {T} + model = MOI.instantiate(optimizer) + if !MOI.supports_incremental_interface(model) + # Don't use `default_cache` for the cache because, for example, SCS's + # default cache doesn't support modifying coefficients of the constraint + # matrix. JuMP uses the default cache with SCS because it has an outer + # layer of caching; we don't have that here, so we can't use the + # default. + # + # We could revert to using the default cache if we fix this in MOI. + cache = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()) + model = MOI.Utilities.CachingOptimizer(cache, model) + end + return MOI.Bridges.full_bridge_optimizer(model, T) +end function MathOptAnalyzer.analyze( ::Analyzer, model::MOI.ModelLike; optimizer = nothing, ) - out = Data() - T = Float64 - solver = MOIIS.Optimizer() MOI.set(solver, MOIIS.InfeasibleModel(), model) - if optimizer !== nothing - MOI.set(solver, MOIIS.InnerOptimizer(), optimizer) + MOI.set( + solver, + MOIIS.InnerOptimizer(), + () -> _instantiate_with_modify(optimizer, Float64), + ) end - MOI.compute_conflict!(solver) - - data = solver.results - - for iis in data - meta = iis.metadata - if typeof(meta) <: MOIIS.BoundsData - constraints = iis.constraints - @assert length(constraints) == 2 - func = MOI.get(model, MOI.ConstraintFunction(), constraints[1]) - push!( - out.infeasible_bounds, - InfeasibleBounds{T}(func, meta.lower_bound, meta.upper_bound), - ) - elseif typeof(meta) <: MOIIS.IntegralityData - constraints = iis.constraints - @assert length(constraints) >= 2 - func = MOI.get(model, MOI.ConstraintFunction(), constraints[1]) - push!( - out.infeasible_integrality, - InfeasibleIntegrality{T}( - func, - meta.lower_bound, - meta.upper_bound, - meta.set, - ), - ) - elseif typeof(meta) <: MOIIS.RangeData - constraints = iis.constraints - @assert length(constraints) >= 1 - # main_con = nothing - for con in constraints - if !(typeof(con) <: MOI.ConstraintIndex{MOI.VariableIndex}) - push!( - out.constraint_range, - InfeasibleConstraintRange{T}( - con, - meta.lower_bound, - meta.upper_bound, - meta.set, - ), - ) - break - end - end - else - push!(out.iis, IrreducibleInfeasibleSubset(iis.constraints)) - end + out = Data() + for iis in solver.results + _add_result(out, model, iis, iis.metadata) end return out end diff --git a/test/Project.toml b/test/Project.toml index c3b6a41..7367bb5 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,13 @@ [deps] HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +MathOptAnalyzer = "d1179b25-476b-425c-b826-c7787f0fff83" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +HiGHS = "1" +JuMP = "1" +MathOptInterface = "1" +SCS = "1" diff --git a/test/test_Infeasibility.jl b/test/test_Infeasibility.jl index 815333e..03efd5e 100644 --- a/test/test_Infeasibility.jl +++ b/test/test_Infeasibility.jl @@ -7,8 +7,10 @@ module TestInfeasibility using JuMP using Test + import HiGHS import MathOptAnalyzer +import SCS function runtests() for name in names(@__MODULE__; all = true) @@ -591,6 +593,37 @@ function test_iis_spare() return end +function test_iis_bridges() + model = Model(SCS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y <= 20) + @variable(model, 0 <= z <= 20) + @constraint(model, c0, 2z <= 1) + @constraint(model, c00, 3z <= 1) + @constraint(model, c1, x + y <= 1) + @constraint(model, c2, x + y >= 2) + @objective(model, Max, x + y) + optimize!(model) + @test termination_status(model) == INFEASIBLE + data = MathOptAnalyzer.analyze( + MathOptAnalyzer.Infeasibility.Analyzer(), + model, + optimizer = SCS.Optimizer, + ) + list = MathOptAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = MathOptAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test length(ret[].constraint) == 2 + @test Set([ret[].constraint[1], ret[].constraint[2]]) == + Set(JuMP.index.([c2, c1])) + iis = MathOptAnalyzer.constraints(ret[], model) + @test length(iis) == 2 + @test Set(iis) == Set([c2, c1]) + return +end + end # module TestInfeasibility TestInfeasibility.runtests()