From c7e5d106abd3a0c45afc5a787c601fe4b277b9fb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 20 Feb 2026 16:04:12 +1300 Subject: [PATCH 1/2] [breaking] a large refactoring of MathOptIIS This commit is breaking because it: * Removes SkipFeasibilityCheck * Removes StopIfInfeasibleBounds * Removes StopIfInfeasibleRanges * Removes DeletionFilter * Removes ElasticFilterTolerance * Removes ElasticFilterIgnoreIntegrality In addition, this commit will likely result in a different IIS being returned for many models because it now exploits an infeasibility certificate if one is present. --- README.md | 14 +- src/MathOptIIS.jl | 1083 +++++++++++++++++++++++++++++++++++++++- src/bound.jl | 149 ------ src/iis.jl | 380 -------------- src/interval.jl | 28 ++ src/range.jl | 141 ------ src/solver.jl | 229 --------- test/Project.toml | 6 + test/data/enlight4.mps | 154 ++++++ test/data/enlight9.mps | 784 +++++++++++++++++++++++++++++ test/runtests.jl | 948 +++++++++++++++++++++++++---------- 11 files changed, 2735 insertions(+), 1181 deletions(-) delete mode 100644 src/bound.jl delete mode 100644 src/iis.jl create mode 100644 src/interval.jl delete mode 100644 src/range.jl delete mode 100644 src/solver.jl create mode 100755 test/data/enlight4.mps create mode 100755 test/data/enlight9.mps diff --git a/README.md b/README.md index f03629d..1fe65eb 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Pkg.add("MathOptIIS") ## Usage -This package is not intended to be called directly from user-code. Instead, it +MathOptIIS is not intended to be called directly from user-code. Instead, it should be added as a dependency to solver wrappers that want to provide an IIS. To add to an existing wrapper, add a new field: @@ -41,6 +41,11 @@ function MOI.compute_conflict!(model::Optimizer) solver = MathOptIIS.Optimizer() MOI.set(solver, MathOptIIS.InfeasibleModel(), model) MOI.set(solver, MathOptIIS.InnerOptimizer(), Optimizer) + # Optional: set the solver's silent to true or false. In this example, + # follow the outer attribute. + MOI.set(solver, MOI.Silent(), MOI.get(model, MOI.Silent())) + # Optional: set a time limit. In this case, 60 seconds. + MOI.set(solver, MOI.TimeLimitSec(), 60.0) MOI.compute_conflict!(solver) model.conflict_solver = solver return @@ -53,6 +58,13 @@ function MOI.get(optimizer::Optimizer, attr::MOI.ConflictStatus) return MOI.get(optimizer.conflict_solver, attr) end +function MOI.get(optimizer::Optimizer, attr::MOI.ConflictCount) + if optimizer.conflict_solver === nothing + return 0 + end + return MOI.get(optimizer.conflict_solver, attr) +end + function MOI.get( optimizer::Optimizer, attr::MOI.ConstraintConflictStatus, diff --git a/src/MathOptIIS.jl b/src/MathOptIIS.jl index 0faa6fb..8df0606 100644 --- a/src/MathOptIIS.jl +++ b/src/MathOptIIS.jl @@ -7,12 +7,1083 @@ module MathOptIIS import MathOptInterface as MOI -include("iis.jl") -include("bound.jl") -include("range.jl") -include("solver.jl") +include("interval.jl") -@deprecate ConflictCount MOI.ConflictCount -@deprecate ConstraintConflictStatus MOI.ConstraintConflictStatus +# ============================================================================== +# User-facing API +# ============================================================================== + +""" + struct Metadata{T,S} + lower_bound::T + upper_bound::T + set::S + end + +A struct that provides additional information to help interpret an IIS. See the +docstring of `IIS` for details. +""" +struct Metadata{T,S} + lower_bound::T + upper_bound::T + set::S +end + +""" + IIS{M<:Union{Nothing,Metadata}} + +A struct that stores information describing an IIS. It has the following fields: + + * `constraints::Vector{MOI.ConstraintIndex}`: a list of constraint indices for + which the status is `MOI.IN_CONFLICT`. + + * `maybe_constraints::Vector{MOI.ConstraintIndex}`: a list of constraint + indices for which the status is `MOI.MAYBE_IN_CONFLICT`. + + * `metadata::M`: additional metadata that may be useful. There are four cases: + + 1. `metadata::Nothing`: a variable contains conflicting bounds. `constraints` + contains the offending bound constraints. + + 2. `metadata::Metadata{T,MOI.Integer}`: a variable that is integer has bounds + that conflict with the constraint that it is integer. `constraints` + contains the offending bound constraints and the integrality constraint + + 3. `metadata::Metadata{T,MOI.ZeroOne}`: a variable that is binary has bounds + that conflict with the constraint that it is binary. `constraints` + contains the offending bound constraints and the zero-one constraint + + 4. `metadata::Metadata{T,S<:MOI.AbstractSet}`: a constraint cannot be + satisfied based on the variable bounds. `constraints` contains the + offending constraint and all associated variable bounds. +""" +struct IIS{M} + constraints::Vector{MOI.ConstraintIndex} + maybe_constraints::Vector{MOI.ConstraintIndex} + metadata::M + + function IIS( + constraints::Vector{MOI.ConstraintIndex}; + maybe_constraints::Vector{MOI.ConstraintIndex} = MOI.ConstraintIndex[], + metadata::M = nothing, + ) where {M<:Union{Nothing,Metadata}} + return new{M}(constraints, maybe_constraints, metadata) + end +end + +""" + Optimizer() + +Create a new optimizer object that supports `MOI.compute_conflict!`. + +## Attributes + +Before calling `MOI.compute_conflict!`, you must set the following two +attributes: + + * `MathOptIIS.InfeasibleModel` + * `MathOptIIS.InnerOptimizer` + +There are also two optional attributes that control the behavior of the +algorithm: + + * `MOI.TimeLimitSec` + * `MOI.Silent` + +After calling `MOI.compute_conflict!`, access the IIS (if present) using the +following attributes: + + * `MOI.ConflictCount` + * `MOI.ConflictStatus` + * `MOI.ConstraintConflictStatus` + * `MathOptIIS.ListOfConstraintIndicesInConflict` + +## Advanced + +Some applications may need to directly interact with the result objects that are +stored in the `.results::Vector{IIS}` field. There is one `IIS` for each +conflict. See the `IIS` docstring for more details. + +## Embedding the Optimizer in another package + +MathOptIIS is not intended to be called directly from user-code. Instead, it +should be added as a dependency to solver wrappers that want to provide an IIS. + +To add to an existing wrapper, add a new field: +```julia +conflict_solver::Union{Nothing,MathOptIIS.Optimizer} +``` + +Then, add the following methods: +```julia +function MOI.compute_conflict!(model::Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), model) + MOI.set(solver, MathOptIIS.InnerOptimizer(), Optimizer) + # Optional: set the solver's silent to true or false. In this example, + # follow the outer attribute. + MOI.set(solver, MOI.Silent(), MOI.get(model, MOI.Silent())) + # Optional: set a time limit. In this case, 60 seconds. + MOI.set(solver, MOI.TimeLimitSec(), 60.0) + MOI.compute_conflict!(solver) + model.conflict_solver = solver + return +end + +function MOI.get(optimizer::Optimizer, attr::MOI.ConflictStatus) + if optimizer.conflict_solver === nothing + return MOI.COMPUTE_CONFLICT_NOT_CALLED + end + return MOI.get(optimizer.conflict_solver, attr) +end + +function MOI.get(optimizer::Optimizer, attr::MOI.ConflictCount) + if optimizer.conflict_solver === nothing + return 0 + end + return MOI.get(optimizer.conflict_solver, attr) +end + +function MOI.get( + optimizer::Optimizer, + attr::MOI.ConstraintConflictStatus, + con::MOI.ConstraintIndex, +) + return MOI.get(optimizer.conflict_solver, attr, con) +end +``` +""" +mutable struct Optimizer <: MOI.AbstractOptimizer + infeasible_model::Union{MOI.ModelLike,Nothing} + inner_optimizer::Any + # iis attributes + time_limit::Float64 + verbose::Bool + # result data + start_time::Float64 + status::MOI.ConflictStatusCode + results::Vector{IIS} + + function Optimizer() + return new( + nothing, + nothing, + Inf, + false, + NaN, + MOI.COMPUTE_CONFLICT_NOT_CALLED, + IIS[], + ) + end +end + +# MathOptIIS.InfeasibleModel + +""" + InfeasibleModel() <: MOI.AbstractModelAttribute + +A model attribute for passing the infeasible model to the IIS solver. + +## Example + +```julia +import MathOptIIS +solver = MathOptIIS.Optimizer() +MOI.set(solver, MathOptIIS.InfeasibleModel(), model) +MOI.set(solver, MathOptIIS.InnerOptimizer(), Optimizer) +MOI.compute_conflict!(solver) +``` +""" +struct InfeasibleModel <: MOI.AbstractModelAttribute end + +function MOI.set(optimizer::Optimizer, ::InfeasibleModel, model::MOI.ModelLike) + optimizer.infeasible_model = model + empty!(optimizer.results) + optimizer.status = MOI.COMPUTE_CONFLICT_NOT_CALLED + return +end + +# MathOptIIS.InnerOptimizer + +""" + InnerOptimizer() <: MOI.AbstractOptimizerAttribute + +A optimizer attribute for passing the inner optimizer to the IIS solver. + +## Example + +```julia +import MathOptIIS +solver = MathOptIIS.Optimizer() +MOI.set(solver, MathOptIIS.InfeasibleModel(), model) +MOI.set(solver, MathOptIIS.InnerOptimizer(), Optimizer) +MOI.compute_conflict!(solver) +``` +""" +struct InnerOptimizer <: MOI.AbstractOptimizerAttribute end + +function MOI.set(optimizer::Optimizer, ::InnerOptimizer, inner_optimizer::Any) + optimizer.inner_optimizer = inner_optimizer + return +end + +# MOI.TimeLimitSec + +function MOI.set(optimizer::Optimizer, ::MOI.TimeLimitSec, value::Float64) + optimizer.time_limit = value + return +end + +MOI.get(optimizer::Optimizer, ::MOI.TimeLimitSec) = optimizer.time_limit + +# MOI.Silent + +function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool) + optimizer.verbose = !value + return +end + +MOI.get(optimizer::Optimizer, ::MOI.Silent) = !optimizer.verbose + +# MOI.ConflictStatus + +MOI.get(optimizer::Optimizer, ::MOI.ConflictStatus) = optimizer.status + +# MOI.ConflictCount + +MOI.get(optimizer::Optimizer, ::MOI.ConflictCount) = length(optimizer.results) + +# MOI.ConstraintConflictStatus + +function MOI.get( + optimizer::Optimizer, + attr::MOI.ConstraintConflictStatus, + con::MOI.ConstraintIndex, +) + if !(1 <= attr.conflict_index <= length(optimizer.results)) + return MOI.NOT_IN_CONFLICT + elseif con in optimizer.results[attr.conflict_index].constraints + return MOI.IN_CONFLICT + elseif con in optimizer.results[attr.conflict_index].maybe_constraints + return MOI.MAYBE_IN_CONFLICT + end + return MOI.NOT_IN_CONFLICT +end + +# MathOptIIS.ListOfConstraintIndicesInConflict + +""" + ListOfConstraintIndicesInConflict(conflict_index = 1) + +An `MOI.AbstractModelAttribute` for querying the list of constraints that appear +in the IIS at index `conflict_index`. + +The return value is a `Vector{MOI.ConstraintIndex}`. Note how this is not +type-stable. +""" +struct ListOfConstraintIndicesInConflict <: MOI.AbstractModelAttribute + conflict_index::Int + + function ListOfConstraintIndicesInConflict(conflict_index::Integer = 1) + return new(conflict_index) + end +end + +function MOI.get(optimizer::Optimizer, attr::ListOfConstraintIndicesInConflict) + if !(1 <= attr.conflict_index <= length(optimizer.results)) + return MOI.ConstraintIndex[] + end + # TODO(odow): this doesn't include maybe constraints? + return optimizer.results[attr.conflict_index].constraints +end + +# MOI.compute_conflict! + +function _check_interrupt(f::F) where {F} + try + return reenable_sigint(f) + catch ex + if !(ex isa InterruptException) + rethrow(ex) + end + return true + end +end + +function _check_premature_termination(optimizer::Optimizer) + return _check_interrupt() do + return time() >= optimizer.start_time + optimizer.time_limit + end +end + +function _update_time_limit(optimizer::Optimizer, model::MOI.ModelLike) + time_remaining = optimizer.start_time + optimizer.time_limit - time() + if isfinite(time_remaining) && MOI.supports(model, MOI.TimeLimitSec()) + MOI.set(model, MOI.TimeLimitSec(), time_remaining) + end + return +end + +function _optimize!(optimizer::Optimizer, model::MOI.ModelLike) + _update_time_limit(optimizer, model) + MOI.optimize!(model) + return +end + +function MOI.compute_conflict!(optimizer::Optimizer) + disable_sigint() do + return _compute_conflict!( + optimizer, + optimizer.inner_optimizer, + optimizer.infeasible_model, + ) + end + return +end + +function _compute_conflict!( + optimizer::Optimizer, + inner_optimizer::Any, + infeasible_model::MOI.ModelLike, +) + optimizer.status = MOI.NO_CONFLICT_FOUND + empty!(optimizer.results) + optimizer.start_time = time() + if optimizer.verbose + println("[MathOptIIS] starting compute_conflict!") + end + if _feasibility_check(optimizer, infeasible_model) + optimizer.status = MOI.NO_CONFLICT_EXISTS + return optimizer.results + end + # Step 1: check for inconsistent variable bounds + variable_info = _bound_infeasibility!(optimizer, infeasible_model, Float64) + if !isempty(optimizer.results) + optimizer.status = MOI.CONFLICT_FOUND + end + if _check_premature_termination(optimizer) + return + end + # Step 2: check for inconsistent constraints based on variable bounds + _range_infeasibility!(optimizer, infeasible_model, variable_info) + if !isempty(optimizer.results) + optimizer.status = MOI.CONFLICT_FOUND + # Now it's safe to return: there are some trivial things for the user to + # fix before we attempt the elastic filter + return + end + if _check_premature_termination(optimizer) + return + end + # Step 3: elastic filter + _elastic_filter(optimizer, inner_optimizer, infeasible_model, variable_info) + if !isempty(optimizer.results) + optimizer.status = MOI.CONFLICT_FOUND + end + if optimizer.verbose + println( + "[MathOptIIS] elastic filter found $(length(optimizer.results)) infeasible subsets", + ) + end + return +end + +# ============================================================================== +# Step 0: feasibility check +# ============================================================================== + +function _feasibility_check( + optimizer::Optimizer, + infeasible_model::MOI.ModelLike, +) + termination_status = MOI.get(infeasible_model, MOI.TerminationStatus()) + if optimizer.verbose + println( + "[MathOptIIS] model termination status: $(termination_status)", + ) + end + if termination_status in + (MOI.OTHER_ERROR, MOI.INVALID_MODEL, MOI.OPTIMIZE_NOT_CALLED) + return false # because we can assert it is feasible + end + primal_status = MOI.get(infeasible_model, MOI.PrimalStatus()) + if optimizer.verbose + println("[MathOptIIS] model primal status: $(primal_status)") + end + if primal_status in (MOI.FEASIBLE_POINT, MOI.NEARLY_FEASIBLE_POINT) && !( + termination_status in + (MOI.INFEASIBLE, MOI.ALMOST_INFEASIBLE, MOI.LOCALLY_INFEASIBLE) + ) + return true + end + return false +end + +# ============================================================================== +# Step 1: check variable bounds and integrality restrictions for violations +# ============================================================================== + +mutable struct _VariableInfo{T} + lower::Union{Nothing,MOI.AbstractScalarSet} + upper::Union{Nothing,MOI.AbstractScalarSet} + integer::Bool + zero_one::Bool + + _VariableInfo{T}() where {T} = new{T}(nothing, nothing, false, false) +end + +_update_info!(info::_VariableInfo, s::MOI.LessThan) = (info.upper = s) + +_update_info!(info::_VariableInfo, s::MOI.GreaterThan) = (info.lower = s) + +function _update_info!(info::_VariableInfo, s::Union{MOI.EqualTo,MOI.Interval}) + info.lower = info.upper = s + return +end + +_update_info!(info::_VariableInfo, ::MOI.Integer) = (info.integer = true) + +_update_info!(info::_VariableInfo, ::MOI.ZeroOne) = (info.zero_one = true) + +function _update_info!( + info::Dict{MOI.VariableIndex,<:_VariableInfo}, + model::MOI.ModelLike, + ::Type{S}, +) where {S<:MOI.AbstractScalarSet} + for ci in MOI.get(model, MOI.ListOfConstraintIndices{MOI.VariableIndex,S}()) + f = MOI.get(model, MOI.ConstraintFunction(), ci) + s = MOI.get(model, MOI.ConstraintSet(), ci) + _update_info!(info[f], s) + end + return +end + +function _ci(x::MOI.VariableIndex, ::S) where {S<:MOI.AbstractScalarSet} + return MOI.ConstraintIndex{MOI.VariableIndex,S}(x.value) +end + +_lower(::Type{T}, ::Nothing) where {T} = typemin(T) +_lower(::Type{T}, s::Union{MOI.GreaterThan,MOI.Interval}) where {T} = s.lower +_lower(::Type{T}, s::MOI.EqualTo) where {T} = s.value + +_upper(::Type{T}, ::Nothing) where {T} = typemax(T) +_upper(::Type{T}, s::Union{MOI.LessThan,MOI.Interval}) where {T} = s.upper +_upper(::Type{T}, s::MOI.EqualTo) where {T} = s.value + +function _check_conflict(x::MOI.VariableIndex, info::_VariableInfo{T}) where {T} + lb, ub = _lower(T, info.lower), _upper(T, info.upper) + if ub < lb + return IIS( + MOI.ConstraintIndex[_ci(x, info.lower), _ci(x, info.upper)]; + metadata = Metadata(lb, ub, nothing), + ) + elseif info.integer + # I don't think this `if` is correct? + if abs(ub - lb) < 1 && ceil(Int, ub) == ceil(Int, lb) + con = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}(x.value) + c = MOI.ConstraintIndex[con, _ci(x, info.lower), _ci(x, info.upper)] + return IIS(c; metadata = Metadata(lb, ub, MOI.Integer())) + end + elseif info.zero_one + con = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value) + if 0 < lb && ub < 1 + c = MOI.ConstraintIndex[con, _ci(x, info.lower), _ci(x, info.upper)] + return IIS(c; metadata = Metadata(lb, ub, MOI.ZeroOne())) + elseif 1 < lb + c = MOI.ConstraintIndex[con, _ci(x, info.lower)] + return IIS(c; metadata = Metadata(lb, typemax(T), MOI.ZeroOne())) + elseif ub < 0 + c = MOI.ConstraintIndex[con, _ci(x, info.upper)] + return IIS(c; metadata = Metadata(typemin(T), ub, MOI.ZeroOne())) + end + end + return +end + +function _bound_infeasibility!( + optimizer::Optimizer, + infeasible_model::MOI.ModelLike, + ::Type{T}, +) where {T} + if optimizer.verbose + println("[MathOptIIS] starting bound analysis") + end + variable_info = Dict( + x => _VariableInfo{T}() for + x in MOI.get(infeasible_model, MOI.ListOfVariableIndices()) + ) + _update_info!(variable_info, infeasible_model, MOI.LessThan{T}) + _update_info!(variable_info, infeasible_model, MOI.GreaterThan{T}) + _update_info!(variable_info, infeasible_model, MOI.EqualTo{T}) + _update_info!(variable_info, infeasible_model, MOI.Interval{T}) + _update_info!(variable_info, infeasible_model, MOI.Integer) + _update_info!(variable_info, infeasible_model, MOI.ZeroOne) + results = IIS[] + for (x, info) in variable_info + if (conflict = _check_conflict(x, info)) !== nothing + push!(results, conflict) + end + end + append!(optimizer.results, results) + if optimizer.verbose + println( + "[MathOptIIS] bound analysis found $(length(results)) infeasible subsets", + ) + end + return variable_info +end + +# ============================================================================== +# Step 2: propagate variable bounds through functions to detect obvious errors +# ============================================================================== + +_supports_interval(::Type{T}) where {T} = false + +function _range_infeasibility!( + optimizer::Optimizer, + infeasible_model::MOI.ModelLike, + variable_info::Dict{MOI.VariableIndex,_VariableInfo{T}}, +) where {T} + if optimizer.verbose + println("[MathOptIIS] starting range analysis") + end + variables = Dict{MOI.VariableIndex,_Interval{T}}() + for (x, info) in variable_info + # It may be the case that lo > hi. In which case, the bound analysis + # will have already flagged the issue, but we might learn something from + # the range propagation. + lo, hi = _lower(T, info.lower), _upper(T, info.upper) + variables[x] = _Interval(lo, max(lo, hi)) + end + results = IIS[] + for (F, S) in MOI.get(infeasible_model, MOI.ListOfConstraintTypesPresent()) + if _supports_interval(F) && _supports_interval(S) + _range_infeasibility!( + optimizer, + infeasible_model, + variable_info, + variables, + F, + S, + results, + ) + end + end + append!(optimizer.results, results) + if optimizer.verbose + println( + "[MathOptIIS] range analysis found $(length(results)) infeasible subsets", + ) + end + return +end + +function _range_infeasibility!( + optimizer::Optimizer, + infeasible_model::MOI.ModelLike, + variable_info::Dict{MOI.VariableIndex,_VariableInfo{T}}, + variables::Dict{MOI.VariableIndex,_Interval{T}}, + ::Type{F}, + ::Type{S}, + results::Vector{IIS}, +) where {T,F,S} + if optimizer.verbose + println( + "[MathOptIIS] analyzing ", + sprint(MOI.Utilities.print_with_acronym, "$F -in- $S"), + ) + end + for con in MOI.get(infeasible_model, MOI.ListOfConstraintIndices{F,S}()) + if _check_premature_termination(optimizer) + return + end + func = MOI.get(infeasible_model, MOI.ConstraintFunction(), con) + cons = Set{MOI.ConstraintIndex}() + interval = _compute_interval(variables, func, variable_info, cons) + set = MOI.get(infeasible_model, MOI.ConstraintSet(), con)::S + if !_valid_range(set, interval) + push!(cons, con) + metadata = Metadata(interval.lo, interval.hi, set) + push!(results, IIS(collect(cons); metadata)) + end + end + return +end + +_supports_interval(::Type{MOI.ScalarAffineFunction{T}}) where {T} = true + +function _compute_interval( + variables::Dict{MOI.VariableIndex,_Interval{T}}, + f::MOI.ScalarAffineFunction, + variable_info::Dict{MOI.VariableIndex,_VariableInfo{T}}, + cons::Set{MOI.ConstraintIndex}, +) where {T} + out = _Interval(f.constant, f.constant) + for t in f.terms + out += t.coefficient * variables[t.variable] + if (s = variable_info[t.variable].lower) !== nothing + push!(cons, _ci(t.variable, s)) + end + if (s = variable_info[t.variable].upper) !== nothing + push!(cons, _ci(t.variable, s)) + end + end + return out +end + +_supports_interval(::Type{MOI.EqualTo{T}}) where {T} = true + +_valid_range(set::MOI.EqualTo, x::_Interval) = x.lo <= set.value <= x.hi + +_supports_interval(::Type{MOI.LessThan{T}}) where {T} = true + +_valid_range(set::MOI.LessThan, x::_Interval) = set.upper >= x.lo + +_supports_interval(::Type{MOI.GreaterThan{T}}) where {T} = true + +_valid_range(set::MOI.GreaterThan, x::_Interval) = x.hi >= set.lower + +_supports_interval(::Type{MOI.Interval{T}}) where {T} = true + +function _valid_range(set::MOI.Interval, x::_Interval) + return set.upper >= x.lo && set.lower <= x.hi +end + +# ============================================================================== +# Step 3: compute a proper IIS using an elastic filter +# ============================================================================== + +function _fix_slack( + model::MOI.ModelLike, + func::MOI.ScalarAffineFunction{T}, +) where {T} + for term in func.terms + MOI.add_constraint(model, term.variable, MOI.LessThan(zero(T))) + end + return +end + +function _unfix_slack( + model::MOI.ModelLike, + func::MOI.ScalarAffineFunction{T}, +) where {T} + for term in func.terms + x = term.variable + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{T}}(x.value) + @assert MOI.is_valid(model, ci) + MOI.delete(model, ci) + end + return +end + +function _instantiate(optimizer::F, ::Type{T}) where {F,T} + cache = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()), + MOI.instantiate(optimizer), + ) + return MOI.Bridges.full_bridge_optimizer(cache, T) +end + +function _elastic_filter( + optimizer::Optimizer, + inner_optimizer::Any, + infeasible_model::MOI.ModelLike, + variable_info::Dict{MOI.VariableIndex,_VariableInfo{T}}, +) where {T} + if optimizer.verbose + println("[MathOptIIS] starting elastic filter") + end + if inner_optimizer === nothing + println( + "[MathOptIIS] elastic filter cannot continue because no optimizer was provided", + ) + return + end + # We instantiate with a cache, even if the solver supports the incremental + # interface, because we don't know if the `inner_optimizer` supports all of + # the modifications we're going to do here. + model = _instantiate(inner_optimizer, T) + if MOI.supports(model, MOI.Silent()) + MOI.set(model, MOI.Silent(), true) + end + index_map = MOI.copy_to(model, infeasible_model) + # ========================================================================== + # Step 1: relax integrality and solve. + # ========================================================================== + if optimizer.verbose + println("[MathOptIIS] relaxing integrality if required") + end + relax_info = _relax_integrality(model, variable_info) + # It's a bit wasteful to re-solve this problem when we know it is + # infeasible, but: + # * we might have relaxed the integrality + # * we're going to be modifying and re-solving anyway, so it doesn't hurt + # to cache some work. It's only one extra solve. + _optimize!(optimizer, model) + if _is_feasible(model) + # The relaxed problem is feasible. This is significantly more difficult + # to deal with because we need to add back in the integrality + # restrictions. + for x in relax_info.integer + MOI.add_constraint(model, x, MOI.Integer()) + end + for (ci, x) in relax_info.binary + MOI.delete(model, ci) + MOI.add_constraint(model, x, MOI.ZeroOne()) + end + end + # ========================================================================== + # Step 2: additive filter. Construct a superset of the IIS + # ========================================================================== + dual_certificate = MOI.ConstraintIndex[] + if MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE + # If there is an infeasibility certificate, the non-zero rows are a + # superset of the IIS + _dual_certificate!(dual_certificate, model, relax_info) + end + # ========================================================================== + # Step 3: relax constraints to build the penalized problem + # ========================================================================== + if optimizer.verbose + println("[MathOptIIS] constructing the penalty relaxation") + end + constraint_to_affine = MOI.modify( + model, + MOI.Utilities.PenaltyRelaxation(; default = one(T), warn = false), + ) + slack_obj = zero(MOI.ScalarAffineFunction{T}) + for v in values(constraint_to_affine) + MOI.Utilities.operate!(+, T, slack_obj, v) + end + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{typeof(slack_obj)}(), slack_obj) + iis_candidate = Set{MOI.ConstraintIndex}() + # In the following solves we never actually need a proof of global + # optimality. We care only about feasibility. I assume that solvers employ a + # stopping heuristic here: better solutions mean a smaller candidate set, + # but come at the expense of increased solve time. + if MOI.supports(model, MOI.ObjectiveLimit()) + # As one attempt, set an objective limit. In practice this needs tuning. + MOI.set(model, MOI.ObjectiveLimit(), 2.1) + end + # ========================================================================== + # Step 4: if we didn't find a certificate, build a super set via the + # additive method. + # ========================================================================== + if !isempty(dual_certificate) + if optimizer.verbose + println( + "[MathOptIIS] using INFEASIBILITY_CERTIFICATE to construct candidate set", + ) + end + for ci in dual_certificate + if (slack = get(constraint_to_affine, ci, nothing)) !== nothing + push!(iis_candidate, ci) + _fix_slack(model, slack) + end + end + if optimizer.verbose + println( + "[MathOptIIS] size of the candidate set: $(length(iis_candidate))", + ) + end + else + if optimizer.verbose + println("[MathOptIIS] starting the additive method") + end + constraints_to_check = Set(keys(constraint_to_affine)) + constraints_to_fix = Set{MOI.ConstraintIndex}() + while !isempty(constraints_to_check) + if _check_premature_termination(optimizer) + if optimizer.verbose + println("[MathOptIIS] early termination requested") + end + return nothing + end + _optimize!(optimizer, model) + if !_is_feasible(model) + break + end + for con in constraints_to_check + func = constraint_to_affine[con] + for t in func.terms + if MOI.get(model, MOI.VariablePrimal(), t.variable) > 0 + push!(constraints_to_fix, con) + break + end + end + end + for con in constraints_to_fix + _fix_slack(model, constraint_to_affine[con]) + push!(iis_candidate, con) + delete!(constraints_to_check, con) + end + empty!(constraints_to_fix) + if optimizer.verbose + println( + "[MathOptIIS] size of the candidate set: $(length(iis_candidate))", + ) + end + end + end + # ========================================================================== + # Step 5: a deletion filter + # ========================================================================== + if optimizer.verbose + println("[MathOptIIS] starting the deletion filter") + end + # We maintain two sets of constraints: + # * iis_in_conflict is the set of constraints we have proven are in the + # conflict + # * iis_maybe_in_conflict is the set of constraints that were in the + # candidate set and we could not rule out. This set is filled only if we + # terminate the deletion filter before finishing. + iis_in_conflict = Set{MOI.ConstraintIndex}() + iis_maybe_in_conflict = Set{MOI.ConstraintIndex}() + candidate_size = length(iis_candidate) + for subset in Iterators.partition(iis_candidate, 10) + if _check_premature_termination(optimizer) + union!(iis_maybe_in_conflict, subset) + continue + end + iis_subset = _iterative_deletion_filter( + optimizer, + model, + constraint_to_affine, + subset, + ) + union!(iis_in_conflict, iis_subset) + if optimizer.verbose + candidate_size -= length(subset) - length(iis_subset) + println( + "[MathOptIIS] size of the candidate set: $candidate_size", + ) + end + end + # ========================================================================== + # Step 6: clean up and get out of here + # ========================================================================== + new_to_old_index_map = _reverse(index_map) + # First, we need to map constraints from `model` back to the original + # `optimizer`. There's the added complication that we may have replaced a + # `x in {0, 1}` by `1.0 * x in [0, 1]`: + iis = MOI.ConstraintIndex[] + for c in iis_in_conflict + if (x = get(relax_info.binary, c, nothing)) !== nothing + c = _ci(x, MOI.ZeroOne()) + end + push!(iis, new_to_old_index_map[c]) + end + maybe_constraints = MOI.ConstraintIndex[] + for c in iis_maybe_in_conflict + if (x = get(relax_info.binary, c, nothing)) !== nothing + c = _ci(x, MOI.ZeroOne()) + end + push!(maybe_constraints, new_to_old_index_map[c]) + end + # Next, we need to add all of the constraints that _might_ be in the IIS and + # which we didn't test because they couldn't be relaxed. If there was a dual + # certificate, this is easy. + if !isempty(dual_certificate) + for ci in dual_certificate + # No need to check `relax_info.binary` here because it can't be part + # of the infeasibility certificate. + if !haskey(constraint_to_affine, ci) + push!(maybe_constraints, new_to_old_index_map[ci]) + end + end + push!(optimizer.results, IIS(iis; maybe_constraints)) + return + end + # If there wasn't a dual certificate, we need to add all constraints that + # weren't relaxed, and we also need to add any variable bounds of variables + # that appear in the constraints. + # + # First loop: get all non-relaxed constraints that are not variable bounds. + for (F, S) in MOI.get(infeasible_model, MOI.ListOfConstraintTypesPresent()) + if F == MOI.VariableIndex + continue + end + for ci in MOI.get(infeasible_model, MOI.ListOfConstraintIndices{F,S}()) + if haskey(constraint_to_affine, index_map[ci]) + break # If this constraint is, all other F-in-S are too + end + push!(maybe_constraints, ci) + end + end + # Now, get all variables that appear in the constraints. + variables = Set{MOI.VariableIndex}() + for c in [iis; maybe_constraints] + f = MOI.get(infeasible_model, MOI.ConstraintFunction(), c) + _get_variables!(variables, f) + end + # Second loop: get all variable constraints as they appear. + for (F, S) in MOI.get(infeasible_model, MOI.ListOfConstraintTypesPresent()) + if !(F <: MOI.VariableIndex) + continue + end + for ci in MOI.get(infeasible_model, MOI.ListOfConstraintIndices{F,S}()) + x = MOI.get(infeasible_model, MOI.ConstraintFunction(), ci) + if x in variables + push!(maybe_constraints, ci) + end + end + end + push!(optimizer.results, IIS(iis; maybe_constraints)) + return +end + +function _reverse(index_map::MOI.IndexMap) + ret = MOI.IndexMap() + for (k, v) in index_map.var_map + ret[v] = k + end + for (k, v) in index_map.con_map + ret[v] = k + end + return ret +end + +function _iterative_deletion_filter( + optimizer::Optimizer, + model::MOI.ModelLike, + constraint_to_affine::Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}, + candidates::Vector{MOI.ConstraintIndex}, +) where {T} + # Optimization: unfix all and see if we need to iterate-through one-by-one + for con in candidates + _unfix_slack(model, constraint_to_affine[con]) + end + _optimize!(optimizer, model) + if !_is_feasible(model) + return Set{MOI.ConstraintIndex}() + end + for con in candidates + _fix_slack(model, constraint_to_affine[con]) + end + # Here's the iterative part + ret = Set(candidates) + for con in candidates + if _check_premature_termination(optimizer) + break + end + _unfix_slack(model, constraint_to_affine[con]) + _optimize!(optimizer, model) + if _is_feasible(model) + _fix_slack(model, constraint_to_affine[con]) + else + delete!(ret, con) + end + end + return ret +end + +function _is_feasible(model::MOI.ModelLike) + return MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT +end + +function _dual_certificate!( + dual_certificate::Vector{MOI.ConstraintIndex}, + model::MOI.ModelLike, + relax_info, +) + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + if !iszero(MOI.get(model, MOI.ConstraintDual(), ci)) + push!(dual_certificate, ci) + end + end + end + return +end + +function _relax_integrality( + model::MOI.ModelLike, + variable_info::Dict{MOI.VariableIndex,_VariableInfo{T}}, +) where {T} + F = MOI.VariableIndex + ret = (; + integer = MOI.VariableIndex[], + binary = Dict{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.Interval{T}}, + MOI.VariableIndex, + }(), + ) + for (x, info) in variable_info + if info.integer + MOI.delete(model, MOI.ConstraintIndex{F,MOI.Integer}(x.value)) + push!(ret.integer, x) + elseif info.zero_one + MOI.delete(model, MOI.ConstraintIndex{F,MOI.ZeroOne}(x.value)) + zero_one_set = MOI.Interval(zero(T), one(T)) + ci = MOI.add_constraint(model, one(T) * x, zero_one_set) + ret.binary[ci] = x + end + end + return ret +end + +_get_variables!(::Set{MOI.VariableIndex}, ::Number) = nothing + +function _get_variables!(x::Set{MOI.VariableIndex}, f::MOI.VariableIndex) + push!(x, f) + return +end + +function _get_variables!(x::Set{MOI.VariableIndex}, f::MOI.ScalarAffineTerm) + push!(x, f.variable) + return +end + +function _get_variables!(x::Set{MOI.VariableIndex}, f::MOI.ScalarAffineFunction) + map(Base.Fix1(_get_variables!, x), f.terms) + return +end + +function _get_variables!(x::Set{MOI.VariableIndex}, f::MOI.ScalarQuadraticTerm) + push!(x, f.variable_1) + push!(x, f.variable_2) + return +end + +function _get_variables!( + x::Set{MOI.VariableIndex}, + f::MOI.ScalarQuadraticFunction, +) + map(Base.Fix1(_get_variables!, x), f.affine_terms) + map(Base.Fix1(_get_variables!, x), f.quadratic_terms) + return +end + +function _get_variables!( + x::Set{MOI.VariableIndex}, + f::MOI.ScalarNonlinearFunction, +) + stack = Any[f] + while !isempty(stack) + arg = pop!(stack) + if arg isa MOI.ScalarNonlinearFunction + for a in arg.args + push!(stack, a) + end + else + _get_variables!(x, arg) + end + end + return +end + +# This function currently doesn't get called directly because MOI doesn't +# support relaxing vector functions. However, @odow plans to, so this is a +# future safeguard. For now, it gets tested explicitly in the tests. +function _get_variables!( + x::Set{MOI.VariableIndex}, + f::MOI.AbstractVectorFunction, +) + for g in MOI.Utilities.eachscalar(f) + _get_variables!(x, g) + end + return +end end # module MathOptIIS diff --git a/src/bound.jl b/src/bound.jl deleted file mode 100644 index 8497bbf..0000000 --- a/src/bound.jl +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2025: Joaquim Dias Garcia, Oscar Dowson and contributors -# -# 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. - -function _bound_infeasibility!(optimizer::Optimizer, ::Type{T}) where {T} - return _bound_infeasibility!(optimizer.original_model, optimizer.results, T) -end - -function _bound_infeasibility!( - original_model::MOI.ModelLike, - results::Vector{InfeasibilityData}, - ::Type{T}, -) where {T} - variables = Dict{MOI.VariableIndex,Interval{T}}() - variable_indices = MOI.get(original_model, MOI.ListOfVariableIndices()) - lb = Dict{MOI.VariableIndex,T}() - lb_con = Dict{MOI.VariableIndex,MOI.ConstraintIndex}() - ub = Dict{MOI.VariableIndex,T}() - ub_con = Dict{MOI.VariableIndex,MOI.ConstraintIndex}() - for con in MOI.get( - original_model, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.EqualTo{T}}(), - ) - set = MOI.get(original_model, MOI.ConstraintSet(), con) - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - lb[func] = set.value - lb_con[func] = con - ub[func] = set.value - ub_con[func] = con - end - for con in MOI.get( - original_model, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{T}}(), - ) - set = MOI.get(original_model, MOI.ConstraintSet(), con) - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - # lb[func] = typemin(T) - ub[func] = set.upper - ub_con[func] = con - end - for con in MOI.get( - original_model, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(), - ) - set = MOI.get(original_model, MOI.ConstraintSet(), con) - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - lb[func] = set.lower - lb_con[func] = con - # ub[func] = typemax(T) - end - for con in MOI.get( - original_model, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Interval{T}}(), - ) - set = MOI.get(original_model, MOI.ConstraintSet(), con) - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - lb[func] = set.lower - lb_con[func] = con - ub[func] = set.upper - ub_con[func] = con - end - # for con in MOI.get(original_model, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.SemiContinuous{T}}()) - # set = MOI.get(original_model, MOI.ConstraintSet(), con) - # func = MOI.get(original_model, MOI.ConstraintFunction(), con) - # lb[func] = 0 # set.lower - # ub[func] = set.upper - # end - # for con in MOI.get(original_model, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.SemiInteger{T}}()) - # set = MOI.get(original_model, MOI.ConstraintSet(), con) - # func = MOI.get(original_model, MOI.ConstraintFunction(), con) - # lb[func] = 0 #set.lower - # ub[func] = set.upper - # end - bounds_consistent = true - for con in MOI.get( - original_model, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(), - ) - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - _lb, _ub = get(lb, func, typemin(T)), get(ub, func, typemax(T)) - if abs(_ub - _lb) < 1 && ceil(_ub) == ceil(_lb) - push!( - results, - InfeasibilityData( - MOI.ConstraintIndex[con, lb_con[func], ub_con[func]], - true, - IntegralityData(_lb, _ub, MOI.Integer()), - ), - ) - bounds_consistent = false - end - end - for con in MOI.get( - original_model, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(), - ) - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - _lb, _ub = get(lb, func, typemin(T)), get(ub, func, typemax(T)) - if 0 < _lb && _ub < 1 - push!( - results, - InfeasibilityData( - MOI.ConstraintIndex[con, lb_con[func], ub_con[func]], - true, - IntegralityData(_lb, _ub, MOI.ZeroOne()), - ), - ) - bounds_consistent = false - elseif _lb > 1 - push!( - results, - InfeasibilityData( - MOI.ConstraintIndex[con, lb_con[func]], - true, - IntegralityData(_lb, typemax(T), MOI.ZeroOne()), - ), - ) - bounds_consistent = false - elseif _ub < 0 - push!( - results, - InfeasibilityData( - MOI.ConstraintIndex[con, ub_con[func]], - true, - IntegralityData(typemin(T), _ub, MOI.ZeroOne()), - ), - ) - bounds_consistent = false - end - end - for var in variable_indices - _lb, _ub = get(lb, var, typemin(T)), get(ub, var, typemax(T)) - if _lb > _ub - push!( - results, - InfeasibilityData( - MOI.ConstraintIndex[lb_con[var], ub_con[var]], - true, - BoundsData(_lb, _ub), - ), - ) - bounds_consistent = false - else - variables[var] = Interval(_lb, _ub) - end - end - return bounds_consistent, variables, lb_con, ub_con -end diff --git a/src/iis.jl b/src/iis.jl deleted file mode 100644 index 75254ee..0000000 --- a/src/iis.jl +++ /dev/null @@ -1,380 +0,0 @@ -# Copyright (c) 2025: Joaquim Dias Garcia, Oscar Dowson and contributors -# -# 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. - -abstract type AbstractAdditionalData end - -struct InfeasibilityData - # IIS constraints set - constraints::Vector{MOI.ConstraintIndex} - # variable-set constraints only for NoData IIS (from the iis solver) - # this will be an empty vector for most types of IIS - maybe_constraints::Vector{MOI.ConstraintIndex} - # indicates if the IIS is irreducible - irreducible::Bool - # additional data - metadata::AbstractAdditionalData - function InfeasibilityData( - constraints::Vector{<:MOI.ConstraintIndex}, - irreducible::Bool, - metadata::AbstractAdditionalData; - maybe_constraints::Vector{MOI.ConstraintIndex} = MOI.ConstraintIndex[], - ) - return new(constraints, maybe_constraints, irreducible, metadata) - end -end - -struct BoundsData <: AbstractAdditionalData - lower_bound::Float64 - upper_bound::Float64 -end - -struct IntegralityData <: AbstractAdditionalData - lower_bound::Float64 - upper_bound::Float64 - set::Union{MOI.Integer,MOI.ZeroOne}#, MOI.Semicontinuous{T}, MOI.Semiinteger{T}} -end - -struct RangeData <: AbstractAdditionalData - lower_bound::Float64 - upper_bound::Float64 - set::Union{<:MOI.EqualTo,<:MOI.LessThan,<:MOI.GreaterThan} -end - -struct NoData <: AbstractAdditionalData end - -Base.@kwdef mutable struct Optimizer - original_model::Union{MOI.ModelLike,Nothing} = nothing - # - # iterative solver data - optimizer::Any = nothing # MOI.ModelLike - optimizer_attributes::Dict{Any,Any} = Dict{Any,Any}() - # - # iis attributes - time_limit::Float64 = Inf - verbose::Bool = false - skip_feasibility_check::Bool = false - stop_if_infeasible_bounds::Bool = true - stop_if_infeasible_ranges::Bool = true - deletion_filter::Bool = true - elastic_filter_tolerance::Float64 = 1e-5 - ignore_integrality::Bool = false - # - # result data - start_time::Float64 = NaN - status::MOI.ConflictStatusCode = MOI.COMPUTE_CONFLICT_NOT_CALLED - results::Vector{InfeasibilityData} = InfeasibilityData[] -end - -struct InfeasibleModel end - -function MOI.set(optimizer::Optimizer, ::InfeasibleModel, model::MOI.ModelLike) - optimizer.original_model = model - empty!(optimizer.results) - optimizer.status = MOI.COMPUTE_CONFLICT_NOT_CALLED - return -end - -# function MOI.get(optimizer::Optimizer, ::InfeasibleModel) -# return optimizer.original_model -# end - -struct InnerOptimizer end - -function MOI.set(optimizer::Optimizer, ::InnerOptimizer, solver) - optimizer.optimizer = solver - return -end - -# function MOI.get(optimizer::Optimizer, ::InnerOptimizer) -# return optimizer.optimizer -# end - -struct InnerOptimizerAttribute - attr::MOI.AbstractOptimizerAttribute -end - -function MOI.set(optimizer::Optimizer, attr::InnerOptimizerAttribute, value) - optimizer.optimizer_attributes[attr.attr] = value - return -end - -# function MOI.get( -# optimizer::Optimizer, -# attr::InnerOptimizerAttribute, -# ) -# return optimizer.optimizer_attributes[attr.attr] -# end - -function MOI.set(optimizer::Optimizer, ::MOI.TimeLimitSec, value::Float64) - optimizer.time_limit = value - return -end - -function MOI.get(optimizer::Optimizer, ::MOI.TimeLimitSec) - return optimizer.time_limit -end - -function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool) - optimizer.verbose = !value - return -end - -function MOI.get(optimizer::Optimizer, ::MOI.Silent) - return !optimizer.verbose -end - -struct SkipFeasibilityCheck <: MOI.AbstractOptimizerAttribute end - -function MOI.set(optimizer::Optimizer, ::SkipFeasibilityCheck, value::Bool) - optimizer.skip_feasibility_check = value - return -end - -function MOI.get(optimizer::Optimizer, ::SkipFeasibilityCheck) - return optimizer.skip_feasibility_check -end - -struct StopIfInfeasibleBounds end - -function MOI.set(optimizer::Optimizer, ::StopIfInfeasibleBounds, value::Bool) - optimizer.stop_if_infeasible_bounds = value - return -end - -function MOI.get(optimizer::Optimizer, ::StopIfInfeasibleBounds) - return optimizer.stop_if_infeasible_bounds -end - -struct StopIfInfeasibleRanges end - -function MOI.set(optimizer::Optimizer, ::StopIfInfeasibleRanges, value::Bool) - optimizer.stop_if_infeasible_ranges = value - return -end - -function MOI.get(optimizer::Optimizer, ::StopIfInfeasibleRanges) - return optimizer.stop_if_infeasible_ranges -end - -struct DeletionFilter end - -function MOI.set(optimizer::Optimizer, ::DeletionFilter, value::Bool) - optimizer.deletion_filter = value - return -end - -function MOI.get(optimizer::Optimizer, ::DeletionFilter) - return optimizer.deletion_filter -end - -struct ElasticFilterTolerance end - -function MOI.set(optimizer::Optimizer, ::ElasticFilterTolerance, value::Float64) - optimizer.elastic_filter_tolerance = value - return -end - -function MOI.get(optimizer::Optimizer, ::ElasticFilterTolerance) - return optimizer.elastic_filter_tolerance -end - -struct ElasticFilterIgnoreIntegrality end - -function MOI.set( - optimizer::Optimizer, - ::ElasticFilterIgnoreIntegrality, - value::Bool, -) - optimizer.ignore_integrality = value - return -end - -function MOI.get(optimizer::Optimizer, ::ElasticFilterIgnoreIntegrality) - return optimizer.ignore_integrality -end - -MOI.get(optimizer::Optimizer, ::MOI.ConflictStatus) = optimizer.status - -MOI.get(optimizer::Optimizer, ::MOI.ConflictCount) = length(optimizer.results) - -function MOI.get( - optimizer::Optimizer, - attr::MOI.ConstraintConflictStatus, - con::MOI.ConstraintIndex, -) - if attr.conflict_index > length(optimizer.results) - return MOI.NOT_IN_CONFLICT # or error - end - if con in optimizer.results[attr.conflict_index].constraints - return MOI.IN_CONFLICT - elseif con in optimizer.results[attr.conflict_index].maybe_constraints - return MOI.MAYBE_IN_CONFLICT - end - return MOI.NOT_IN_CONFLICT -end - -struct ListOfConstraintIndicesInConflict <: MOI.AbstractModelAttribute - conflict_index::Int - ListOfConstraintIndicesInConflict(conflict_index = 1) = new(conflict_index) -end - -function MOI.get(optimizer::Optimizer, attr::ListOfConstraintIndicesInConflict) - if attr.conflict_index > length(optimizer.results) - return MOI.ConstraintIndex[] - end - return optimizer.results[attr.conflict_index].constraints -end - -function _in_time(optimizer::Optimizer) - @assert optimizer.start_time != NaN - return time() - optimizer.start_time < optimizer.time_limit -end - -function MOI.compute_conflict!(optimizer::Optimizer) - optimizer.status = MOI.NO_CONFLICT_FOUND - empty!(optimizer.results) - optimizer.start_time = time() - if optimizer.verbose - println("Starting MathOptIIS IIS search.") - end - T = Float64 - is_feasible = _feasibility_check(optimizer, optimizer.original_model) - if is_feasible && !optimizer.skip_feasibility_check - optimizer.status = MOI.NO_CONFLICT_EXISTS - return optimizer.results - end - if optimizer.verbose - println("Starting bound analysis.") - end - bounds_consistent, variables, lb_con, ub_con = - _bound_infeasibility!(optimizer, T) - bound_infeasibilities = length(optimizer.results) - if optimizer.verbose - println( - "Complete bound analysis found $bound_infeasibilities infeasibilities.", - ) - end - if !isempty(optimizer.results) - optimizer.status = MOI.CONFLICT_FOUND - end - # check PSD diagonal >= 0 ? - # other cones? - if (!bounds_consistent && optimizer.stop_if_infeasible_bounds) || - !_in_time(optimizer) - return - end - # second layer of infeasibility analysis is constraint range analysis - if optimizer.verbose - println("Starting range analysis.") - end - range_consistent = - _range_infeasibility!(optimizer, T, variables, lb_con, ub_con) - range_infeasibilities = length(optimizer.results) - bound_infeasibilities - if optimizer.verbose - println( - "Complete range analysis found $range_infeasibilities infeasibilities.", - ) - end - if !isempty(optimizer.results) - optimizer.status = MOI.CONFLICT_FOUND - end - if (!range_consistent && optimizer.stop_if_infeasible_ranges) || - !_in_time(optimizer) - return - end - # third layer is an IIS resolver - if optimizer.optimizer === nothing - println( - "IIS resolver cannot continue because no optimizer was provided", - ) - return - end - if optimizer.verbose - println("Starting elastic filter solver.") - end - iis = _elastic_filter(optimizer) - # for now, only one iis is computed - if iis !== nothing - maybe_constraints = - _get_variables_in_constraints(optimizer.original_model, iis) - push!( - optimizer.results, - InfeasibilityData(iis, true, NoData(); maybe_constraints), - ) - optimizer.status = MOI.CONFLICT_FOUND - end - if optimizer.verbose - iis_infeasibilities = iis === nothing ? 0 : 1 - println( - "Complete elastic filter solver found $iis_infeasibilities infeasibilities.", - ) - end - - return -end - -function _get_variables_in_constraints( - model::MOI.ModelLike, - con::Vector{MOI.ConstraintIndex}, -) - variables = Set{MOI.VariableIndex}() - for c in con - _get_variables_in_constraints!(model, c, variables) - end - variable_constraints = MOI.ConstraintIndex[] - for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - if !(F <: MOI.VariableIndex) - continue - end - for con in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - if MOI.get(model, MOI.ConstraintFunction(), con) in variables - push!(variable_constraints, con) - end - end - end - return variable_constraints -end - -function _get_variables_in_constraints!( - model::MOI.ModelLike, - con::MOI.ConstraintIndex{<:MOI.ScalarAffineFunction}, - variables::Set{MOI.VariableIndex}, -) - f = MOI.get(model, MOI.ConstraintFunction(), con) - for term in f.terms - push!(variables, term.variable) - end - return -end - -function _get_variables_in_constraints!( - ::MOI.ModelLike, - ::MOI.ConstraintIndex, - ::Set{MOI.VariableIndex}, -) - return # skip -end - -function _feasibility_check(optimizer::Optimizer, original_model::MOI.ModelLike) - termination_status = MOI.get(original_model, MOI.TerminationStatus()) - if optimizer.verbose - println("Original model termination status: $(termination_status)") - end - if termination_status in - (MOI.OTHER_ERROR, MOI.INVALID_MODEL, MOI.OPTIMIZE_NOT_CALLED) - return false # because we can assert it is feasible - end - primal_status = MOI.get(original_model, MOI.PrimalStatus()) - if optimizer.verbose - println("Original model primal status: $(primal_status)") - end - if primal_status in (MOI.FEASIBLE_POINT, MOI.NEARLY_FEASIBLE_POINT) && !( - termination_status in - (MOI.INFEASIBLE, MOI.ALMOST_INFEASIBLE, MOI.LOCALLY_INFEASIBLE) - ) - return true - end - return false -end diff --git a/src/interval.jl b/src/interval.jl new file mode 100644 index 0000000..63eef34 --- /dev/null +++ b/src/interval.jl @@ -0,0 +1,28 @@ +# Copyright (c) 2025: Joaquim Dias Garcia, Oscar Dowson and contributors +# Copyright (c) 2014-2021: David P. Sanders & Luis Benet +# +# 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. + +# This type and the associated functions were inspired by IntervalArithmetic.jl + +struct _Interval{T<:Real} + lo::T + hi::T + + function _Interval(lo::T, hi::T) where {T<:Real} + @assert lo <= hi + return new{T}(lo, hi) + end +end + +function Base.:+(a::_Interval{T}, b::_Interval{T}) where {T<:Real} + return _Interval(a.lo + b.lo, a.hi + b.hi) +end + +function Base.:*(x::T, a::_Interval{T}) where {T<:Real} + if x >= zero(T) + return _Interval(a.lo * x, a.hi * x) + end + return _Interval(a.hi * x, a.lo * x) +end diff --git a/src/range.jl b/src/range.jl deleted file mode 100644 index f6a309b..0000000 --- a/src/range.jl +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright (c) 2025: Joaquim Dias Garcia, Oscar Dowson and contributors -# -# 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. - -# This type and the associated function were inspired by IntervalArithmetic.jl -# Copyright (c) 2014-2021: David P. Sanders & Luis Benet - -struct Interval{T<:Real} - lo::T - hi::T - - function Interval(lo::T, hi::T) where {T<:Real} - @assert lo <= hi - return new{T}(lo, hi) - end -end - -Base.convert(::Type{Interval{T}}, x::T) where {T<:Real} = Interval(x, x) - -Base.zero(::Type{Interval{T}}) where {T<:Real} = Interval(zero(T), zero(T)) - -Base.iszero(a::Interval) = iszero(a.hi) && iszero(a.lo) - -function Base.:+(a::Interval{T}, b::Interval{T}) where {T<:Real} - return Interval(a.lo + b.lo, a.hi + b.hi) -end - -function Base.:*(x::T, a::Interval{T}) where {T<:Real} - if iszero(a) || iszero(x) - return Interval(zero(T), zero(T)) - elseif x >= zero(T) - return Interval(a.lo * x, a.hi * x) - end - return Interval(a.hi * x, a.lo * x) -end - -# Back to functions written for MathOptIIS.jl - -_supports_range(::Type{MOI.ScalarAffineFunction{T}}) where {T} = true -_supports_range(::Type{MOI.EqualTo{T}}) where {T} = true -_supports_range(::Type{MOI.GreaterThan{T}}) where {T} = true -_supports_range(::Type{MOI.LessThan{T}}) where {T} = true -_supports_range(::Type{T}) where {T} = false - -function _range_infeasibility!( - optimizer::Optimizer, - ::Type{T}, - variables::Dict{MOI.VariableIndex,Interval{T}}, - lb_con::Dict{MOI.VariableIndex,MOI.ConstraintIndex}, - ub_con::Dict{MOI.VariableIndex,MOI.ConstraintIndex}, -) where {T} - range_consistent = true - for (F, S) in - MOI.get(optimizer.original_model, MOI.ListOfConstraintTypesPresent()) - if !_supports_range(F) || !_supports_range(S) - continue - end - range_consistent &= _range_infeasibility!( - optimizer, - optimizer.original_model, - T, - variables, - lb_con, - ub_con, - F, - S, - ) - end - return range_consistent -end - -function _range_infeasibility!( - optimizer::Optimizer, - original_model::MOI.ModelLike, - ::Type{T}, - variables::Dict{MOI.VariableIndex,Interval{T}}, - lb_con::Dict{MOI.VariableIndex,MOI.ConstraintIndex}, - ub_con::Dict{MOI.VariableIndex,MOI.ConstraintIndex}, - ::Type{F}, - ::Type{S}, -) where {T,F,S} - range_consistent = true - for con in MOI.get(original_model, MOI.ListOfConstraintIndices{F,S}()) - if !_in_time(optimizer) - return range_consistent - end - func = MOI.get(original_model, MOI.ConstraintFunction(), con) - interval = _eval_variables(variables, func) - if interval === nothing - continue - end - set = MOI.get(original_model, MOI.ConstraintSet(), con)::S - if !_invalid_range(set, interval) - continue - end - cons = Set{MOI.ConstraintIndex}() - push!(cons, con) - for t in func.terms - if (c = get(lb_con, t.variable, nothing)) !== nothing - push!(cons, c) - end - if (c = get(ub_con, t.variable, nothing)) !== nothing - push!(cons, c) - end - end - push!( - optimizer.results, - InfeasibilityData( - collect(cons), - true, # strictly speaking, we might need the proper "sides" - RangeData(interval.lo, interval.hi, set), - ), - ) - range_consistent = false - end - return range_consistent -end - -function _eval_variables( - map::AbstractDict{MOI.VariableIndex,U}, - f::MOI.ScalarAffineFunction, -) where {U} - out = convert(U, f.constant) - for t in f.terms - v = get(map, t.variable, nothing) - if v === nothing - return - end - out += t.coefficient * v - end - return out -end - -function _invalid_range(set::MOI.EqualTo, interval) - return !(interval.lo <= set.value <= interval.hi) -end - -_invalid_range(set::MOI.LessThan, interval) = set.upper < interval.lo - -_invalid_range(set::MOI.GreaterThan, interval) = interval.hi < set.lower diff --git a/src/solver.jl b/src/solver.jl deleted file mode 100644 index 952a11e..0000000 --- a/src/solver.jl +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright (c) 2025: Joaquim Dias Garcia, Oscar Dowson and contributors -# -# 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. - -function _fix_slack(model, variable::MOI.VariableIndex, ::Type{T}) where {T} - lb_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{T}}( - variable.value, - ) - @assert MOI.is_valid(model, lb_idx) - MOI.delete(model, lb_idx) - MOI.add_constraint(model, variable, MOI.EqualTo{T}(zero(T))) - return -end - -function _unfix_slack(model, variable::MOI.VariableIndex, ::Type{T}) where {T} - eq_idx = - MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}}(variable.value) - @assert MOI.is_valid(model, eq_idx) - MOI.delete(model, eq_idx) - MOI.add_constraint(model, variable, MOI.GreaterThan{T}(zero(T))) - return -end - -function _elastic_filter(optimizer::Optimizer) - T = Float64 - - model = MOI.instantiate(optimizer.optimizer) - MOI.set(model, MOI.Silent(), true) - for (k, v) in optimizer.optimizer_attributes - MOI.set(model, k, v) - end - reference_map = MOI.copy_to(model, optimizer.original_model) - - if optimizer.ignore_integrality - _cp_constraint_types = - MOI.get(model, MOI.ListOfConstraintTypesPresent()) - # _removed_constraints = Tuple[] - for (F, S) in _cp_constraint_types - con_list = [] - if S in ( - MOI.ZeroOne, - MOI.Integer, - MOI.Semicontinuous{T}, - MOI.Semiinteger{T}, - MOI.SOS1{T}, - MOI.SOS2{T}, - ) || S <: MOI.Indicator - con_list = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - end - for con in con_list - MOI.delete(model, con) - # # save removed constraints - # func = MOI.get(model, MOI.ConstraintFunction(), con) - # set = MOI.get(model, MOI.ConstraintSet(), con) - # push!(_removed_constraints, (func, set)) - end - end - end - - obj_sense = MOI.get(model, MOI.ObjectiveSense()) - base_obj_type = MOI.get(model, MOI.ObjectiveFunctionType()) - base_obj_func = MOI.get(model, MOI.ObjectiveFunction{base_obj_type}()) - - constraint_to_affine = MOI.modify( - model, - MOI.Utilities.PenaltyRelaxation(; default = one(T), warn = false), - ) - # all slack variables added are of type ">= 0" - # might need to do something related to integers / binary - relaxed_obj_type = MOI.get(model, MOI.ObjectiveFunctionType()) - relaxed_obj_func = MOI.get(model, MOI.ObjectiveFunction{relaxed_obj_type}()) - pure_relaxed_obj_func = relaxed_obj_func - base_obj_func - - MOI.set( - model, - MOI.ObjectiveFunction{relaxed_obj_type}(), - pure_relaxed_obj_func, - ) - - max_iterations = length(constraint_to_affine) - - tolerance = optimizer.elastic_filter_tolerance - - de_elastisized = [] - - changed_obj = false - - # all (affine, non-bound) constraints are relaxed at this point - # we will try to set positive slacks to zero until the model infeasible - # the constraints of the fixed slacks are a IIS candidate - - for i in 1:max_iterations - if !_in_time(optimizer) - return nothing - end - MOI.optimize!(model) - status = MOI.get(model, MOI.TerminationStatus()) - if status in ( # possibily primal unbounded statuses - MOI.INFEASIBLE_OR_UNBOUNDED, - MOI.DUAL_INFEASIBLE, - MOI.ALMOST_DUAL_INFEASIBLE, - ) - # - end - if status in - (MOI.INFEASIBLE, MOI.ALMOST_INFEASIBLE, MOI.LOCALLY_INFEASIBLE) - break - end - for (con, func) in constraint_to_affine - @assert length(func.terms) <= 2 - if length(func.terms) == 1 - var = func.terms[1].variable - value = MOI.get(model, MOI.VariablePrimal(), var) - if value > tolerance - _fix_slack(model, var, T) - delete!(constraint_to_affine, con) - push!(de_elastisized, (con, var)) - end - elseif length(func.terms) == 2 - var1 = func.terms[1].variable - var2 = func.terms[2].variable - value1 = MOI.get(model, MOI.VariablePrimal(), var1) - value2 = MOI.get(model, MOI.VariablePrimal(), var2) - if value1 > tolerance && value2 > tolerance - error("IIS failed due numerical instability") - elseif value1 > tolerance - # TODO: coef is always 1.0 - _fix_slack(model, var1, T) - delete!(constraint_to_affine, con) - constraint_to_affine[con] = one(T) * var2 - push!(de_elastisized, (con, var1)) - elseif value2 > tolerance - _fix_slack(model, var2, T) - delete!(constraint_to_affine, con) - constraint_to_affine[con] = one(T) * var1 - push!(de_elastisized, (con, var2)) - end - end - end - end - - # consider deleting all no iis constraints - # be careful with intervals - - obj_type = MOI.get(model, MOI.ObjectiveFunctionType()) - obj_func = MOI.get(model, MOI.ObjectiveFunction{obj_type}()) - obj_sense = MOI.get(model, MOI.ObjectiveSense()) - - candidates = MOI.ConstraintIndex[] - if !optimizer.deletion_filter - for (con, var) in de_elastisized - push!(candidates, con) - end - empty!(de_elastisized) - end - - # deletion filter - for (con, var) in de_elastisized - if !_in_time(optimizer) - return nothing - end - _unfix_slack(model, var, T) - MOI.optimize!(model) - status = MOI.get(model, MOI.TerminationStatus()) - if status in - (MOI.INFEASIBLE, MOI.ALMOST_INFEASIBLE, MOI.LOCALLY_INFEASIBLE) - # this constraint is not in IIS - # hence it remains unfixed - elseif status in ( - MOI.OPTIMAL, - MOI.ALMOST_OPTIMAL, - MOI.LOCALLY_SOLVED, - MOI.ALMOST_LOCALLY_SOLVED, - ) - push!(candidates, con) - _fix_slack(model, var, T) - elseif status in ( # possibily primal unbounded statuses - MOI.INFEASIBLE_OR_UNBOUNDED, - MOI.DUAL_INFEASIBLE, - MOI.ALMOST_DUAL_INFEASIBLE, - ) - MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) - MOI.optimize!(model) - primal_status = MOI.get(model, MOI.PrimalStatus()) - # the unbounded case: - if primal_status in (MOI.FEASIBLE_POINT, MOI.NEARLY_FEASIBLE_POINT) - # this constraint is not in IIS - push!(candidates, con) - _fix_slack(model, var, T) - MOI.set(model, MOI.ObjectiveSense(), obj_sense) - MOI.set( - model, - MOI.ObjectiveFunction{relaxed_obj_type}(), - pure_relaxed_obj_func, - ) - # the both primal and dual infeasible case: - # else - # nothing - end - else - error("IIS failed due numerical instability, got status $status") - end - end - - if isempty(candidates) - return nothing - end - - pre_iis = Set(candidates) - iis = MOI.ConstraintIndex[] - for (F, S) in - MOI.get(optimizer.original_model, MOI.ListOfConstraintTypesPresent()) - if F == MOI.VariableIndex - continue - end - for con in MOI.get( - optimizer.original_model, - MOI.ListOfConstraintIndices{F,S}(), - ) - new_con = reference_map[con] - if new_con in pre_iis - push!(iis, con) - end - end - end - - return iis -end diff --git a/test/Project.toml b/test/Project.toml index c83381b..1b6b87e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,9 +1,15 @@ [deps] HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +MathOptIIS = "8c4f8055-bd93-4160-a86b-a0c04941dbff" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] HiGHS = "1" +Ipopt = "1" JuMP = "1" +SCS = "2" +Test = "1" diff --git a/test/data/enlight4.mps b/test/data/enlight4.mps new file mode 100755 index 0000000..e132f90 --- /dev/null +++ b/test/data/enlight4.mps @@ -0,0 +1,154 @@ +NAME enlight4 +ROWS + N moves + E inner_area_1 + E inner_area_2 + E inner_area_3 + E inner_area_4 + E upper_border_1 + E upper_border_2 + E lower_border_1 + E lower_border_2 + E left_border_1 + E left_border_2 + E right_border_1 + E right_border_2 + E left_upper_cor@c + E left_lower_cor@d + E right_upper_co@e + E right_lower_co@f +COLUMNS + MARK0000 'MARKER' 'INTORG' + x#1#1 moves 1 + x#1#1 upper_border_1 1 + x#1#1 left_border_1 1 + x#1#1 left_upper_cor@c 1 + x#1#2 moves 1 + x#1#2 inner_area_1 1 + x#1#2 upper_border_1 1 + x#1#2 upper_border_2 1 + x#1#2 left_upper_cor@c 1 + x#1#3 moves 1 + x#1#3 inner_area_2 1 + x#1#3 upper_border_1 1 + x#1#3 upper_border_2 1 + x#1#3 right_upper_co@e 1 + x#1#4 moves 1 + x#1#4 upper_border_2 1 + x#1#4 right_border_1 1 + x#1#4 right_upper_co@e 1 + x#2#1 moves 1 + x#2#1 inner_area_1 1 + x#2#1 left_border_1 1 + x#2#1 left_border_2 1 + x#2#1 left_upper_cor@c 1 + x#2#2 moves 1 + x#2#2 inner_area_1 1 + x#2#2 inner_area_2 1 + x#2#2 inner_area_3 1 + x#2#2 upper_border_1 1 + x#2#2 left_border_1 1 + x#2#3 moves 1 + x#2#3 inner_area_1 1 + x#2#3 inner_area_2 1 + x#2#3 inner_area_4 1 + x#2#3 upper_border_2 1 + x#2#3 right_border_1 1 + x#2#4 moves 1 + x#2#4 inner_area_2 1 + x#2#4 right_border_1 1 + x#2#4 right_border_2 1 + x#2#4 right_upper_co@e 1 + x#3#1 moves 1 + x#3#1 inner_area_3 1 + x#3#1 left_border_1 1 + x#3#1 left_border_2 1 + x#3#1 left_lower_cor@d 1 + x#3#2 moves 1 + x#3#2 inner_area_1 1 + x#3#2 inner_area_3 1 + x#3#2 inner_area_4 1 + x#3#2 lower_border_1 1 + x#3#2 left_border_2 1 + x#3#3 moves 1 + x#3#3 inner_area_2 1 + x#3#3 inner_area_3 1 + x#3#3 inner_area_4 1 + x#3#3 lower_border_2 1 + x#3#3 right_border_2 1 + x#3#4 moves 1 + x#3#4 inner_area_4 1 + x#3#4 right_border_1 1 + x#3#4 right_border_2 1 + x#3#4 right_lower_co@f 1 + x#4#1 moves 1 + x#4#1 lower_border_1 1 + x#4#1 left_border_2 1 + x#4#1 left_lower_cor@d 1 + x#4#2 moves 1 + x#4#2 inner_area_3 1 + x#4#2 lower_border_1 1 + x#4#2 lower_border_2 1 + x#4#2 left_lower_cor@d 1 + x#4#3 moves 1 + x#4#3 inner_area_4 1 + x#4#3 lower_border_1 1 + x#4#3 lower_border_2 1 + x#4#3 right_lower_co@f 1 + x#4#4 moves 1 + x#4#4 lower_border_2 1 + x#4#4 right_border_2 1 + x#4#4 right_lower_co@f 1 + y#2#2 inner_area_1 -2 + y#2#3 inner_area_2 -2 + y#3#2 inner_area_3 -2 + y#3#3 inner_area_4 -2 + y#1#2 upper_border_1 -2 + y#1#3 upper_border_2 -2 + y#4#2 lower_border_1 -2 + y#4#3 lower_border_2 -2 + y#2#1 left_border_1 -2 + y#3#1 left_border_2 -2 + y#2#4 right_border_1 -2 + y#3#4 right_border_2 -2 + y#1#1 left_upper_cor@c -2 + y#4#1 left_lower_cor@d -2 + y#1#4 right_upper_co@e -2 + y#4#4 right_lower_co@f -2 + MARK0001 'MARKER' 'INTEND' +RHS + rhs left_upper_cor@c -1 +BOUNDS + UP bnd x#1#1 1 + UP bnd x#1#2 1 + UP bnd x#1#3 1 + UP bnd x#1#4 1 + UP bnd x#2#1 1 + UP bnd x#2#2 1 + UP bnd x#2#3 1 + UP bnd x#2#4 1 + UP bnd x#3#1 1 + UP bnd x#3#2 1 + UP bnd x#3#3 1 + UP bnd x#3#4 1 + UP bnd x#4#1 1 + UP bnd x#4#2 1 + UP bnd x#4#3 1 + UP bnd x#4#4 1 + LI bnd y#2#2 0 + LI bnd y#2#3 0 + LI bnd y#3#2 0 + LI bnd y#3#3 0 + LI bnd y#1#2 0 + LI bnd y#1#3 0 + LI bnd y#4#2 0 + LI bnd y#4#3 0 + LI bnd y#2#1 0 + LI bnd y#3#1 0 + LI bnd y#2#4 0 + LI bnd y#3#4 0 + LI bnd y#1#1 0 + LI bnd y#4#1 0 + LI bnd y#1#4 0 + LI bnd y#4#4 0 +ENDATA diff --git a/test/data/enlight9.mps b/test/data/enlight9.mps new file mode 100755 index 0000000..03e6b40 --- /dev/null +++ b/test/data/enlight9.mps @@ -0,0 +1,784 @@ +NAME enlight9 +ROWS + N moves + E inner_area_1 + E inner_area_2 + E inner_area_3 + E inner_area_4 + E inner_area_5 + E inner_area_6 + E inner_area_7 + E inner_area_8 + E inner_area_9 + E inner_area_10 + E inner_area_11 + E inner_area_12 + E inner_area_13 + E inner_area_14 + E inner_area_15 + E inner_area_16 + E inner_area_17 + E inner_area_18 + E inner_area_19 + E inner_area_20 + E inner_area_21 + E inner_area_22 + E inner_area_23 + E inner_area_24 + E inner_area_25 + E inner_area_26 + E inner_area_27 + E inner_area_28 + E inner_area_29 + E inner_area_30 + E inner_area_31 + E inner_area_32 + E inner_area_33 + E inner_area_34 + E inner_area_35 + E inner_area_36 + E inner_area_37 + E inner_area_38 + E inner_area_39 + E inner_area_40 + E inner_area_41 + E inner_area_42 + E inner_area_43 + E inner_area_44 + E inner_area_45 + E inner_area_46 + E inner_area_47 + E inner_area_48 + E inner_area_49 + E upper_border_1 + E upper_border_2 + E upper_border_3 + E upper_border_4 + E upper_border_5 + E upper_border_6 + E upper_border_7 + E lower_border_1 + E lower_border_2 + E lower_border_3 + E lower_border_4 + E lower_border_5 + E lower_border_6 + E lower_border_7 + E left_border_1 + E left_border_2 + E left_border_3 + E left_border_4 + E left_border_5 + E left_border_6 + E left_border_7 + E right_border_1 + E right_border_2 + E right_border_3 + E right_border_4 + E right_border_5 + E right_border_6 + E right_border_7 + E left_upper_co@4d + E left_lower_co@4e + E right_upper_c@4f + E right_lower_c@50 +COLUMNS + MARK0000 'MARKER' 'INTORG' + x#1#1 moves 1 + x#1#1 upper_border_1 1 + x#1#1 left_border_1 1 + x#1#1 left_upper_co@4d 1 + x#1#2 moves 1 + x#1#2 inner_area_1 1 + x#1#2 upper_border_1 1 + x#1#2 upper_border_2 1 + x#1#2 left_upper_co@4d 1 + x#1#3 moves 1 + x#1#3 inner_area_2 1 + x#1#3 upper_border_1 1 + x#1#3 upper_border_2 1 + x#1#3 upper_border_3 1 + x#1#4 moves 1 + x#1#4 inner_area_3 1 + x#1#4 upper_border_2 1 + x#1#4 upper_border_3 1 + x#1#4 upper_border_4 1 + x#1#5 moves 1 + x#1#5 inner_area_4 1 + x#1#5 upper_border_3 1 + x#1#5 upper_border_4 1 + x#1#5 upper_border_5 1 + x#1#6 moves 1 + x#1#6 inner_area_5 1 + x#1#6 upper_border_4 1 + x#1#6 upper_border_5 1 + x#1#6 upper_border_6 1 + x#1#7 moves 1 + x#1#7 inner_area_6 1 + x#1#7 upper_border_5 1 + x#1#7 upper_border_6 1 + x#1#7 upper_border_7 1 + x#1#8 moves 1 + x#1#8 inner_area_7 1 + x#1#8 upper_border_6 1 + x#1#8 upper_border_7 1 + x#1#8 right_upper_c@4f 1 + x#1#9 moves 1 + x#1#9 upper_border_7 1 + x#1#9 right_border_1 1 + x#1#9 right_upper_c@4f 1 + x#2#1 moves 1 + x#2#1 inner_area_1 1 + x#2#1 left_border_1 1 + x#2#1 left_border_2 1 + x#2#1 left_upper_co@4d 1 + x#2#2 moves 1 + x#2#2 inner_area_1 1 + x#2#2 inner_area_2 1 + x#2#2 inner_area_8 1 + x#2#2 upper_border_1 1 + x#2#2 left_border_1 1 + x#2#3 moves 1 + x#2#3 inner_area_1 1 + x#2#3 inner_area_2 1 + x#2#3 inner_area_3 1 + x#2#3 inner_area_9 1 + x#2#3 upper_border_2 1 + x#2#4 moves 1 + x#2#4 inner_area_2 1 + x#2#4 inner_area_3 1 + x#2#4 inner_area_4 1 + x#2#4 inner_area_10 1 + x#2#4 upper_border_3 1 + x#2#5 moves 1 + x#2#5 inner_area_3 1 + x#2#5 inner_area_4 1 + x#2#5 inner_area_5 1 + x#2#5 inner_area_11 1 + x#2#5 upper_border_4 1 + x#2#6 moves 1 + x#2#6 inner_area_4 1 + x#2#6 inner_area_5 1 + x#2#6 inner_area_6 1 + x#2#6 inner_area_12 1 + x#2#6 upper_border_5 1 + x#2#7 moves 1 + x#2#7 inner_area_5 1 + x#2#7 inner_area_6 1 + x#2#7 inner_area_7 1 + x#2#7 inner_area_13 1 + x#2#7 upper_border_6 1 + x#2#8 moves 1 + x#2#8 inner_area_6 1 + x#2#8 inner_area_7 1 + x#2#8 inner_area_14 1 + x#2#8 upper_border_7 1 + x#2#8 right_border_1 1 + x#2#9 moves 1 + x#2#9 inner_area_7 1 + x#2#9 right_border_1 1 + x#2#9 right_border_2 1 + x#2#9 right_upper_c@4f 1 + x#3#1 moves 1 + x#3#1 inner_area_8 1 + x#3#1 left_border_1 1 + x#3#1 left_border_2 1 + x#3#1 left_border_3 1 + x#3#2 moves 1 + x#3#2 inner_area_1 1 + x#3#2 inner_area_8 1 + x#3#2 inner_area_9 1 + x#3#2 inner_area_15 1 + x#3#2 left_border_2 1 + x#3#3 moves 1 + x#3#3 inner_area_2 1 + x#3#3 inner_area_8 1 + x#3#3 inner_area_9 1 + x#3#3 inner_area_10 1 + x#3#3 inner_area_16 1 + x#3#4 moves 1 + x#3#4 inner_area_3 1 + x#3#4 inner_area_9 1 + x#3#4 inner_area_10 1 + x#3#4 inner_area_11 1 + x#3#4 inner_area_17 1 + x#3#5 moves 1 + x#3#5 inner_area_4 1 + x#3#5 inner_area_10 1 + x#3#5 inner_area_11 1 + x#3#5 inner_area_12 1 + x#3#5 inner_area_18 1 + x#3#6 moves 1 + x#3#6 inner_area_5 1 + x#3#6 inner_area_11 1 + x#3#6 inner_area_12 1 + x#3#6 inner_area_13 1 + x#3#6 inner_area_19 1 + x#3#7 moves 1 + x#3#7 inner_area_6 1 + x#3#7 inner_area_12 1 + x#3#7 inner_area_13 1 + x#3#7 inner_area_14 1 + x#3#7 inner_area_20 1 + x#3#8 moves 1 + x#3#8 inner_area_7 1 + x#3#8 inner_area_13 1 + x#3#8 inner_area_14 1 + x#3#8 inner_area_21 1 + x#3#8 right_border_2 1 + x#3#9 moves 1 + x#3#9 inner_area_14 1 + x#3#9 right_border_1 1 + x#3#9 right_border_2 1 + x#3#9 right_border_3 1 + x#4#1 moves 1 + x#4#1 inner_area_15 1 + x#4#1 left_border_2 1 + x#4#1 left_border_3 1 + x#4#1 left_border_4 1 + x#4#2 moves 1 + x#4#2 inner_area_8 1 + x#4#2 inner_area_15 1 + x#4#2 inner_area_16 1 + x#4#2 inner_area_22 1 + x#4#2 left_border_3 1 + x#4#3 moves 1 + x#4#3 inner_area_9 1 + x#4#3 inner_area_15 1 + x#4#3 inner_area_16 1 + x#4#3 inner_area_17 1 + x#4#3 inner_area_23 1 + x#4#4 moves 1 + x#4#4 inner_area_10 1 + x#4#4 inner_area_16 1 + x#4#4 inner_area_17 1 + x#4#4 inner_area_18 1 + x#4#4 inner_area_24 1 + x#4#5 moves 1 + x#4#5 inner_area_11 1 + x#4#5 inner_area_17 1 + x#4#5 inner_area_18 1 + x#4#5 inner_area_19 1 + x#4#5 inner_area_25 1 + x#4#6 moves 1 + x#4#6 inner_area_12 1 + x#4#6 inner_area_18 1 + x#4#6 inner_area_19 1 + x#4#6 inner_area_20 1 + x#4#6 inner_area_26 1 + x#4#7 moves 1 + x#4#7 inner_area_13 1 + x#4#7 inner_area_19 1 + x#4#7 inner_area_20 1 + x#4#7 inner_area_21 1 + x#4#7 inner_area_27 1 + x#4#8 moves 1 + x#4#8 inner_area_14 1 + x#4#8 inner_area_20 1 + x#4#8 inner_area_21 1 + x#4#8 inner_area_28 1 + x#4#8 right_border_3 1 + x#4#9 moves 1 + x#4#9 inner_area_21 1 + x#4#9 right_border_2 1 + x#4#9 right_border_3 1 + x#4#9 right_border_4 1 + x#5#1 moves 1 + x#5#1 inner_area_22 1 + x#5#1 left_border_3 1 + x#5#1 left_border_4 1 + x#5#1 left_border_5 1 + x#5#2 moves 1 + x#5#2 inner_area_15 1 + x#5#2 inner_area_22 1 + x#5#2 inner_area_23 1 + x#5#2 inner_area_29 1 + x#5#2 left_border_4 1 + x#5#3 moves 1 + x#5#3 inner_area_16 1 + x#5#3 inner_area_22 1 + x#5#3 inner_area_23 1 + x#5#3 inner_area_24 1 + x#5#3 inner_area_30 1 + x#5#4 moves 1 + x#5#4 inner_area_17 1 + x#5#4 inner_area_23 1 + x#5#4 inner_area_24 1 + x#5#4 inner_area_25 1 + x#5#4 inner_area_31 1 + x#5#5 moves 1 + x#5#5 inner_area_18 1 + x#5#5 inner_area_24 1 + x#5#5 inner_area_25 1 + x#5#5 inner_area_26 1 + x#5#5 inner_area_32 1 + x#5#6 moves 1 + x#5#6 inner_area_19 1 + x#5#6 inner_area_25 1 + x#5#6 inner_area_26 1 + x#5#6 inner_area_27 1 + x#5#6 inner_area_33 1 + x#5#7 moves 1 + x#5#7 inner_area_20 1 + x#5#7 inner_area_26 1 + x#5#7 inner_area_27 1 + x#5#7 inner_area_28 1 + x#5#7 inner_area_34 1 + x#5#8 moves 1 + x#5#8 inner_area_21 1 + x#5#8 inner_area_27 1 + x#5#8 inner_area_28 1 + x#5#8 inner_area_35 1 + x#5#8 right_border_4 1 + x#5#9 moves 1 + x#5#9 inner_area_28 1 + x#5#9 right_border_3 1 + x#5#9 right_border_4 1 + x#5#9 right_border_5 1 + x#6#1 moves 1 + x#6#1 inner_area_29 1 + x#6#1 left_border_4 1 + x#6#1 left_border_5 1 + x#6#1 left_border_6 1 + x#6#2 moves 1 + x#6#2 inner_area_22 1 + x#6#2 inner_area_29 1 + x#6#2 inner_area_30 1 + x#6#2 inner_area_36 1 + x#6#2 left_border_5 1 + x#6#3 moves 1 + x#6#3 inner_area_23 1 + x#6#3 inner_area_29 1 + x#6#3 inner_area_30 1 + x#6#3 inner_area_31 1 + x#6#3 inner_area_37 1 + x#6#4 moves 1 + x#6#4 inner_area_24 1 + x#6#4 inner_area_30 1 + x#6#4 inner_area_31 1 + x#6#4 inner_area_32 1 + x#6#4 inner_area_38 1 + x#6#5 moves 1 + x#6#5 inner_area_25 1 + x#6#5 inner_area_31 1 + x#6#5 inner_area_32 1 + x#6#5 inner_area_33 1 + x#6#5 inner_area_39 1 + x#6#6 moves 1 + x#6#6 inner_area_26 1 + x#6#6 inner_area_32 1 + x#6#6 inner_area_33 1 + x#6#6 inner_area_34 1 + x#6#6 inner_area_40 1 + x#6#7 moves 1 + x#6#7 inner_area_27 1 + x#6#7 inner_area_33 1 + x#6#7 inner_area_34 1 + x#6#7 inner_area_35 1 + x#6#7 inner_area_41 1 + x#6#8 moves 1 + x#6#8 inner_area_28 1 + x#6#8 inner_area_34 1 + x#6#8 inner_area_35 1 + x#6#8 inner_area_42 1 + x#6#8 right_border_5 1 + x#6#9 moves 1 + x#6#9 inner_area_35 1 + x#6#9 right_border_4 1 + x#6#9 right_border_5 1 + x#6#9 right_border_6 1 + x#7#1 moves 1 + x#7#1 inner_area_36 1 + x#7#1 left_border_5 1 + x#7#1 left_border_6 1 + x#7#1 left_border_7 1 + x#7#2 moves 1 + x#7#2 inner_area_29 1 + x#7#2 inner_area_36 1 + x#7#2 inner_area_37 1 + x#7#2 inner_area_43 1 + x#7#2 left_border_6 1 + x#7#3 moves 1 + x#7#3 inner_area_30 1 + x#7#3 inner_area_36 1 + x#7#3 inner_area_37 1 + x#7#3 inner_area_38 1 + x#7#3 inner_area_44 1 + x#7#4 moves 1 + x#7#4 inner_area_31 1 + x#7#4 inner_area_37 1 + x#7#4 inner_area_38 1 + x#7#4 inner_area_39 1 + x#7#4 inner_area_45 1 + x#7#5 moves 1 + x#7#5 inner_area_32 1 + x#7#5 inner_area_38 1 + x#7#5 inner_area_39 1 + x#7#5 inner_area_40 1 + x#7#5 inner_area_46 1 + x#7#6 moves 1 + x#7#6 inner_area_33 1 + x#7#6 inner_area_39 1 + x#7#6 inner_area_40 1 + x#7#6 inner_area_41 1 + x#7#6 inner_area_47 1 + x#7#7 moves 1 + x#7#7 inner_area_34 1 + x#7#7 inner_area_40 1 + x#7#7 inner_area_41 1 + x#7#7 inner_area_42 1 + x#7#7 inner_area_48 1 + x#7#8 moves 1 + x#7#8 inner_area_35 1 + x#7#8 inner_area_41 1 + x#7#8 inner_area_42 1 + x#7#8 inner_area_49 1 + x#7#8 right_border_6 1 + x#7#9 moves 1 + x#7#9 inner_area_42 1 + x#7#9 right_border_5 1 + x#7#9 right_border_6 1 + x#7#9 right_border_7 1 + x#8#1 moves 1 + x#8#1 inner_area_43 1 + x#8#1 left_border_6 1 + x#8#1 left_border_7 1 + x#8#1 left_lower_co@4e 1 + x#8#2 moves 1 + x#8#2 inner_area_36 1 + x#8#2 inner_area_43 1 + x#8#2 inner_area_44 1 + x#8#2 lower_border_1 1 + x#8#2 left_border_7 1 + x#8#3 moves 1 + x#8#3 inner_area_37 1 + x#8#3 inner_area_43 1 + x#8#3 inner_area_44 1 + x#8#3 inner_area_45 1 + x#8#3 lower_border_2 1 + x#8#4 moves 1 + x#8#4 inner_area_38 1 + x#8#4 inner_area_44 1 + x#8#4 inner_area_45 1 + x#8#4 inner_area_46 1 + x#8#4 lower_border_3 1 + x#8#5 moves 1 + x#8#5 inner_area_39 1 + x#8#5 inner_area_45 1 + x#8#5 inner_area_46 1 + x#8#5 inner_area_47 1 + x#8#5 lower_border_4 1 + x#8#6 moves 1 + x#8#6 inner_area_40 1 + x#8#6 inner_area_46 1 + x#8#6 inner_area_47 1 + x#8#6 inner_area_48 1 + x#8#6 lower_border_5 1 + x#8#7 moves 1 + x#8#7 inner_area_41 1 + x#8#7 inner_area_47 1 + x#8#7 inner_area_48 1 + x#8#7 inner_area_49 1 + x#8#7 lower_border_6 1 + x#8#8 moves 1 + x#8#8 inner_area_42 1 + x#8#8 inner_area_48 1 + x#8#8 inner_area_49 1 + x#8#8 lower_border_7 1 + x#8#8 right_border_7 1 + x#8#9 moves 1 + x#8#9 inner_area_49 1 + x#8#9 right_border_6 1 + x#8#9 right_border_7 1 + x#8#9 right_lower_c@50 1 + x#9#1 moves 1 + x#9#1 lower_border_1 1 + x#9#1 left_border_7 1 + x#9#1 left_lower_co@4e 1 + x#9#2 moves 1 + x#9#2 inner_area_43 1 + x#9#2 lower_border_1 1 + x#9#2 lower_border_2 1 + x#9#2 left_lower_co@4e 1 + x#9#3 moves 1 + x#9#3 inner_area_44 1 + x#9#3 lower_border_1 1 + x#9#3 lower_border_2 1 + x#9#3 lower_border_3 1 + x#9#4 moves 1 + x#9#4 inner_area_45 1 + x#9#4 lower_border_2 1 + x#9#4 lower_border_3 1 + x#9#4 lower_border_4 1 + x#9#5 moves 1 + x#9#5 inner_area_46 1 + x#9#5 lower_border_3 1 + x#9#5 lower_border_4 1 + x#9#5 lower_border_5 1 + x#9#6 moves 1 + x#9#6 inner_area_47 1 + x#9#6 lower_border_4 1 + x#9#6 lower_border_5 1 + x#9#6 lower_border_6 1 + x#9#7 moves 1 + x#9#7 inner_area_48 1 + x#9#7 lower_border_5 1 + x#9#7 lower_border_6 1 + x#9#7 lower_border_7 1 + x#9#8 moves 1 + x#9#8 inner_area_49 1 + x#9#8 lower_border_6 1 + x#9#8 lower_border_7 1 + x#9#8 right_lower_c@50 1 + x#9#9 moves 1 + x#9#9 lower_border_7 1 + x#9#9 right_border_7 1 + x#9#9 right_lower_c@50 1 + y#2#2 inner_area_1 -2 + y#2#3 inner_area_2 -2 + y#2#4 inner_area_3 -2 + y#2#5 inner_area_4 -2 + y#2#6 inner_area_5 -2 + y#2#7 inner_area_6 -2 + y#2#8 inner_area_7 -2 + y#3#2 inner_area_8 -2 + y#3#3 inner_area_9 -2 + y#3#4 inner_area_10 -2 + y#3#5 inner_area_11 -2 + y#3#6 inner_area_12 -2 + y#3#7 inner_area_13 -2 + y#3#8 inner_area_14 -2 + y#4#2 inner_area_15 -2 + y#4#3 inner_area_16 -2 + y#4#4 inner_area_17 -2 + y#4#5 inner_area_18 -2 + y#4#6 inner_area_19 -2 + y#4#7 inner_area_20 -2 + y#4#8 inner_area_21 -2 + y#5#2 inner_area_22 -2 + y#5#3 inner_area_23 -2 + y#5#4 inner_area_24 -2 + y#5#5 inner_area_25 -2 + y#5#6 inner_area_26 -2 + y#5#7 inner_area_27 -2 + y#5#8 inner_area_28 -2 + y#6#2 inner_area_29 -2 + y#6#3 inner_area_30 -2 + y#6#4 inner_area_31 -2 + y#6#5 inner_area_32 -2 + y#6#6 inner_area_33 -2 + y#6#7 inner_area_34 -2 + y#6#8 inner_area_35 -2 + y#7#2 inner_area_36 -2 + y#7#3 inner_area_37 -2 + y#7#4 inner_area_38 -2 + y#7#5 inner_area_39 -2 + y#7#6 inner_area_40 -2 + y#7#7 inner_area_41 -2 + y#7#8 inner_area_42 -2 + y#8#2 inner_area_43 -2 + y#8#3 inner_area_44 -2 + y#8#4 inner_area_45 -2 + y#8#5 inner_area_46 -2 + y#8#6 inner_area_47 -2 + y#8#7 inner_area_48 -2 + y#8#8 inner_area_49 -2 + y#1#2 upper_border_1 -2 + y#1#3 upper_border_2 -2 + y#1#4 upper_border_3 -2 + y#1#5 upper_border_4 -2 + y#1#6 upper_border_5 -2 + y#1#7 upper_border_6 -2 + y#1#8 upper_border_7 -2 + y#9#2 lower_border_1 -2 + y#9#3 lower_border_2 -2 + y#9#4 lower_border_3 -2 + y#9#5 lower_border_4 -2 + y#9#6 lower_border_5 -2 + y#9#7 lower_border_6 -2 + y#9#8 lower_border_7 -2 + y#2#1 left_border_1 -2 + y#3#1 left_border_2 -2 + y#4#1 left_border_3 -2 + y#5#1 left_border_4 -2 + y#6#1 left_border_5 -2 + y#7#1 left_border_6 -2 + y#8#1 left_border_7 -2 + y#2#9 right_border_1 -2 + y#3#9 right_border_2 -2 + y#4#9 right_border_3 -2 + y#5#9 right_border_4 -2 + y#6#9 right_border_5 -2 + y#7#9 right_border_6 -2 + y#8#9 right_border_7 -2 + y#1#1 left_upper_co@4d -2 + y#9#1 left_lower_co@4e -2 + y#1#9 right_upper_c@4f -2 + y#9#9 right_lower_c@50 -2 + MARK0001 'MARKER' 'INTEND' +RHS + rhs left_upper_co@4d -1 +BOUNDS + UP bnd x#1#1 1 + UP bnd x#1#2 1 + UP bnd x#1#3 1 + UP bnd x#1#4 1 + UP bnd x#1#5 1 + UP bnd x#1#6 1 + UP bnd x#1#7 1 + UP bnd x#1#8 1 + UP bnd x#1#9 1 + UP bnd x#2#1 1 + UP bnd x#2#2 1 + UP bnd x#2#3 1 + UP bnd x#2#4 1 + UP bnd x#2#5 1 + UP bnd x#2#6 1 + UP bnd x#2#7 1 + UP bnd x#2#8 1 + UP bnd x#2#9 1 + UP bnd x#3#1 1 + UP bnd x#3#2 1 + UP bnd x#3#3 1 + UP bnd x#3#4 1 + UP bnd x#3#5 1 + UP bnd x#3#6 1 + UP bnd x#3#7 1 + UP bnd x#3#8 1 + UP bnd x#3#9 1 + UP bnd x#4#1 1 + UP bnd x#4#2 1 + UP bnd x#4#3 1 + UP bnd x#4#4 1 + UP bnd x#4#5 1 + UP bnd x#4#6 1 + UP bnd x#4#7 1 + UP bnd x#4#8 1 + UP bnd x#4#9 1 + UP bnd x#5#1 1 + UP bnd x#5#2 1 + UP bnd x#5#3 1 + UP bnd x#5#4 1 + UP bnd x#5#5 1 + UP bnd x#5#6 1 + UP bnd x#5#7 1 + UP bnd x#5#8 1 + UP bnd x#5#9 1 + UP bnd x#6#1 1 + UP bnd x#6#2 1 + UP bnd x#6#3 1 + UP bnd x#6#4 1 + UP bnd x#6#5 1 + UP bnd x#6#6 1 + UP bnd x#6#7 1 + UP bnd x#6#8 1 + UP bnd x#6#9 1 + UP bnd x#7#1 1 + UP bnd x#7#2 1 + UP bnd x#7#3 1 + UP bnd x#7#4 1 + UP bnd x#7#5 1 + UP bnd x#7#6 1 + UP bnd x#7#7 1 + UP bnd x#7#8 1 + UP bnd x#7#9 1 + UP bnd x#8#1 1 + UP bnd x#8#2 1 + UP bnd x#8#3 1 + UP bnd x#8#4 1 + UP bnd x#8#5 1 + UP bnd x#8#6 1 + UP bnd x#8#7 1 + UP bnd x#8#8 1 + UP bnd x#8#9 1 + UP bnd x#9#1 1 + UP bnd x#9#2 1 + UP bnd x#9#3 1 + UP bnd x#9#4 1 + UP bnd x#9#5 1 + UP bnd x#9#6 1 + UP bnd x#9#7 1 + UP bnd x#9#8 1 + UP bnd x#9#9 1 + LI bnd y#2#2 0 + LI bnd y#2#3 0 + LI bnd y#2#4 0 + LI bnd y#2#5 0 + LI bnd y#2#6 0 + LI bnd y#2#7 0 + LI bnd y#2#8 0 + LI bnd y#3#2 0 + LI bnd y#3#3 0 + LI bnd y#3#4 0 + LI bnd y#3#5 0 + LI bnd y#3#6 0 + LI bnd y#3#7 0 + LI bnd y#3#8 0 + LI bnd y#4#2 0 + LI bnd y#4#3 0 + LI bnd y#4#4 0 + LI bnd y#4#5 0 + LI bnd y#4#6 0 + LI bnd y#4#7 0 + LI bnd y#4#8 0 + LI bnd y#5#2 0 + LI bnd y#5#3 0 + LI bnd y#5#4 0 + LI bnd y#5#5 0 + LI bnd y#5#6 0 + LI bnd y#5#7 0 + LI bnd y#5#8 0 + LI bnd y#6#2 0 + LI bnd y#6#3 0 + LI bnd y#6#4 0 + LI bnd y#6#5 0 + LI bnd y#6#6 0 + LI bnd y#6#7 0 + LI bnd y#6#8 0 + LI bnd y#7#2 0 + LI bnd y#7#3 0 + LI bnd y#7#4 0 + LI bnd y#7#5 0 + LI bnd y#7#6 0 + LI bnd y#7#7 0 + LI bnd y#7#8 0 + LI bnd y#8#2 0 + LI bnd y#8#3 0 + LI bnd y#8#4 0 + LI bnd y#8#5 0 + LI bnd y#8#6 0 + LI bnd y#8#7 0 + LI bnd y#8#8 0 + LI bnd y#1#2 0 + LI bnd y#1#3 0 + LI bnd y#1#4 0 + LI bnd y#1#5 0 + LI bnd y#1#6 0 + LI bnd y#1#7 0 + LI bnd y#1#8 0 + LI bnd y#9#2 0 + LI bnd y#9#3 0 + LI bnd y#9#4 0 + LI bnd y#9#5 0 + LI bnd y#9#6 0 + LI bnd y#9#7 0 + LI bnd y#9#8 0 + LI bnd y#2#1 0 + LI bnd y#3#1 0 + LI bnd y#4#1 0 + LI bnd y#5#1 0 + LI bnd y#6#1 0 + LI bnd y#7#1 0 + LI bnd y#8#1 0 + LI bnd y#2#9 0 + LI bnd y#3#9 0 + LI bnd y#4#9 0 + LI bnd y#5#9 0 + LI bnd y#6#9 0 + LI bnd y#7#9 0 + LI bnd y#8#9 0 + LI bnd y#1#1 0 + LI bnd y#9#1 0 + LI bnd y#1#9 0 + LI bnd y#9#9 0 +ENDATA diff --git a/test/runtests.jl b/test/runtests.jl index df156e1..98789dc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,7 +9,9 @@ using JuMP using Test import HiGHS -import MathOptIIS as MOIIS +import Ipopt +import MathOptIIS +import SCS function runtests() for name in names(@__MODULE__; all = true) @@ -26,22 +28,18 @@ function test_bounds() model = Model() @variable(model, 0 <= x <= 1) @variable(model, 2 <= y <= 1) - @constraint(model, c, x + y <= 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() + solver = MathOptIIS.Optimizer() @test MOI.get(solver, MOI.ConflictStatus()) == MOI.COMPUTE_CONFLICT_NOT_CALLED - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @test data[].constraints == [index(LowerBoundRef(y)), index(UpperBoundRef(y))] - @test data[].irreducible - @test data[].metadata == MOIIS.BoundsData(2.0, 1.0) + @test data[].metadata == MathOptIIS.Metadata(2.0, 1.0, nothing) @test MOI.get(solver, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND - @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(c)) == - MOI.NOT_IN_CONFLICT @test MOI.get( solver, MOI.ConstraintConflictStatus(), @@ -63,8 +61,6 @@ function test_bounds() MOI.ConstraintConflictStatus(1), index(UpperBoundRef(y)), ) == MOI.IN_CONFLICT - @test MOI.get(solver, MOI.ConstraintConflictStatus(1), index(c)) == - MOI.NOT_IN_CONFLICT # the next two could be errors @test MOI.get( solver, @@ -77,9 +73,11 @@ function test_bounds() index(UpperBoundRef(y)), ) == MOI.NOT_IN_CONFLICT # - @test MOI.get(solver, MOIIS.ListOfConstraintIndicesInConflict(1)) == + @test MOI.get(solver, MathOptIIS.ListOfConstraintIndicesInConflict(1)) == [index(LowerBoundRef(y)), index(UpperBoundRef(y))] - @test isempty(MOI.get(solver, MOIIS.ListOfConstraintIndicesInConflict(2))) + @test isempty( + MOI.get(solver, MathOptIIS.ListOfConstraintIndicesInConflict(2)), + ) return end @@ -87,10 +85,8 @@ function test_integrality() model = Model() @variable(model, 0 <= x <= 1, Int) @variable(model, 2.2 <= y <= 2.9, Int) - @constraint(model, x + y <= 1) - @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -99,8 +95,7 @@ function test_integrality() index(LowerBoundRef(y)), index(UpperBoundRef(y)), ] - @test data[].irreducible - @test data[].metadata == MOIIS.IntegralityData(2.2, 2.9, MOI.Integer()) + @test data[].metadata == MathOptIIS.Metadata(2.2, 2.9, MOI.Integer()) return end @@ -110,8 +105,8 @@ function test_binary_inner() @variable(model, 0 <= y <= 1, Bin) @constraint(model, x + y <= 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -120,8 +115,7 @@ function test_binary_inner() index(LowerBoundRef(x)), index(UpperBoundRef(x)), ] - @test data[].irreducible - @test data[].metadata == MOIIS.IntegralityData(0.5, 0.8, MOI.ZeroOne()) + @test data[].metadata == MathOptIIS.Metadata(0.5, 0.8, MOI.ZeroOne()) return end @@ -131,14 +125,13 @@ function test_binary_lower() @variable(model, 0 <= y <= 1, Bin) @constraint(model, x + y <= 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results - @test length(data) == 1 - @test data[].constraints == [index(BinaryRef(x)), index(LowerBoundRef(x))] - @test data[].irreducible - @test data[].metadata == MOIIS.IntegralityData(1.5, Inf, MOI.ZeroOne()) + @test length(data) == 2 + @test data[1].constraints == [index(BinaryRef(x)), index(LowerBoundRef(x))] + @test data[1].metadata == MathOptIIS.Metadata(1.5, Inf, MOI.ZeroOne()) return end @@ -148,14 +141,13 @@ function test_binary_upper() @variable(model, 0 <= y <= 1, Bin) @constraint(model, x + y <= 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @test data[].constraints == [index(BinaryRef(x)), index(UpperBoundRef(x))] - @test data[].irreducible - @test data[].metadata == MOIIS.IntegralityData(-Inf, -1.8, MOI.ZeroOne()) + @test data[].metadata == MathOptIIS.Metadata(-Inf, -1.8, MOI.ZeroOne()) return end @@ -169,8 +161,8 @@ function test_range() @variable(model, 1 <= y <= 11) @constraint(model, c, x + y <= 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -184,9 +176,8 @@ function test_range() index(LowerBoundRef(y)), ], ) - @test data[].irreducible @test data[].metadata == - MOIIS.RangeData(11.0, 22.0, MOI.LessThan{Float64}(1.0)) + MathOptIIS.Metadata(11.0, 22.0, MOI.LessThan{Float64}(1.0)) return end @@ -195,10 +186,8 @@ function test_range_and_bound() @variable(model, 10 <= x <= 11) @variable(model, 1 <= y <= 11) @variable(model, 1 <= z <= 0) - @constraint(model, c, x + y <= 1) - @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -206,36 +195,6 @@ function test_range_and_bound() data[].constraints, [index(LowerBoundRef(z)), index(UpperBoundRef(z))], ) - @test MOI.get(solver, MOIIS.StopIfInfeasibleBounds()) == true - MOI.set(solver, MOIIS.StopIfInfeasibleBounds(), false) - @test MOI.get(solver, MOIIS.StopIfInfeasibleBounds()) == false - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 2 - @test _isequal_unordered( - data[1].constraints, - [index(LowerBoundRef(z)), index(UpperBoundRef(z))], - ) - @test _isequal_unordered( - data[2].constraints, - [ - index(c), - index(UpperBoundRef(x)), - index(LowerBoundRef(x)), - index(UpperBoundRef(y)), - index(LowerBoundRef(y)), - ], - ) - @test data[2].irreducible - @test data[2].metadata == - MOIIS.RangeData(11.0, 22.0, MOI.LessThan{Float64}(1.0)) - @test MOI.get(solver, MOIIS.StopIfInfeasibleRanges()) == true - MOI.set(solver, MOIIS.StopIfInfeasibleRanges(), false) - @test MOI.get(solver, MOIIS.StopIfInfeasibleRanges()) == false - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 2 return end @@ -244,10 +203,8 @@ function test_range_and_bound_2() @variable(model, 10 <= x <= 11) @variable(model, 1 <= y <= 11) @variable(model, 1 <= z <= 0) - @constraint(model, c, x + y + z <= 1) - @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -255,17 +212,6 @@ function test_range_and_bound_2() data[].constraints, [index(LowerBoundRef(z)), index(UpperBoundRef(z))], ) - @test MOI.get(solver, MOIIS.StopIfInfeasibleBounds()) == true - MOI.set(solver, MOIIS.StopIfInfeasibleBounds(), false) - @test MOI.get(solver, MOIIS.StopIfInfeasibleBounds()) == false - MOI.compute_conflict!(solver) - data = solver.results - # the result is only one conflic again because the range fail cant be computed - @test length(data) == 1 - @test _isequal_unordered( - data[1].constraints, - [index(LowerBoundRef(z)), index(UpperBoundRef(z))], - ) return end @@ -276,8 +222,8 @@ function test_range_neg() @constraint(model, c, x - y <= 1) @objective(model, Max, x + y) # - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -291,9 +237,8 @@ function test_range_neg() index(LowerBoundRef(y)), ], ) - @test data[].irreducible @test data[].metadata == - MOIIS.RangeData(11.0, 22.0, MOI.LessThan{Float64}(1.0)) + MathOptIIS.Metadata(11.0, 22.0, MOI.LessThan{Float64}(1.0)) return end @@ -303,8 +248,8 @@ function test_range_equalto() @variable(model, y == 2) @constraint(model, c, x + y == 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -312,9 +257,8 @@ function test_range_equalto() data[].constraints, [index(c), index(FixRef(x)), index(FixRef(y))], ) - @test data[].irreducible @test data[].metadata == - MOIIS.RangeData(3.0, 3.0, MOI.EqualTo{Float64}(1.0)) + MathOptIIS.Metadata(3.0, 3.0, MOI.EqualTo{Float64}(1.0)) return end @@ -324,8 +268,8 @@ function test_range_equalto_2() @variable(model, y == 2) @constraint(model, c, 3x + 2y == 1) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -333,9 +277,8 @@ function test_range_equalto_2() data[].constraints, [index(c), index(FixRef(x)), index(FixRef(y))], ) - @test data[].irreducible @test data[].metadata == - MOIIS.RangeData(7.0, 7.0, MOI.EqualTo{Float64}(1.0)) + MathOptIIS.Metadata(7.0, 7.0, MOI.EqualTo{Float64}(1.0)) return end @@ -345,8 +288,8 @@ function test_range_greaterthan() @variable(model, 1 <= y <= 11) @constraint(model, c, x + y >= 100) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -360,9 +303,8 @@ function test_range_greaterthan() index(LowerBoundRef(y)), ], ) - @test data[].irreducible @test data[].metadata == - MOIIS.RangeData(11.0, 22.0, MOI.GreaterThan{Float64}(100.0)) + MathOptIIS.Metadata(11.0, 22.0, MOI.GreaterThan{Float64}(100.0)) return end @@ -372,8 +314,8 @@ function test_range_equalto_3() @variable(model, 1 <= y <= 11) @constraint(model, c, x + y == 100) @objective(model, Max, x + y) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 @@ -387,9 +329,8 @@ function test_range_equalto_3() index(LowerBoundRef(y)), ], ) - @test data[].irreducible @test data[].metadata == - MOIIS.RangeData(11.0, 22.0, MOI.EqualTo{Float64}(100.0)) + MathOptIIS.Metadata(11.0, 22.0, MOI.EqualTo{Float64}(100.0)) return end @@ -401,19 +342,12 @@ function test_interval() @constraint(model, c1, x + y <= 1) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 0 - @test !MOI.get(solver, MOIIS.SkipFeasibilityCheck()) - MOI.set(solver, MOIIS.SkipFeasibilityCheck(), true) - @test MOI.get(solver, MOIIS.SkipFeasibilityCheck()) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 0 - # TODO check status return end @@ -425,23 +359,14 @@ function test_pass_attribute() @constraint(model, c1, x + y <= 1) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.set(solver, MOI.TimeLimitSec(), 5.0) @test MOI.get(solver, MOI.TimeLimitSec()) == 5.0 @test MOI.get(solver, MOI.Silent()) == true MOI.set(solver, MOI.Silent(), false) @test MOI.get(solver, MOI.Silent()) == false - @test MOI.get(solver, MOIIS.ElasticFilterTolerance()) == 1e-5 - MOI.set(solver, MOIIS.ElasticFilterTolerance(), 1e-3) - @test MOI.get(solver, MOIIS.ElasticFilterTolerance()) == 1e-3 - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 0 - MOI.set(solver, MOIIS.SkipFeasibilityCheck(), true) - @test MOI.get(solver, MOIIS.SkipFeasibilityCheck()) - MOI.compute_conflict!(solver) data = solver.results @test length(data) == 0 return @@ -455,8 +380,8 @@ function test_iis_feasible() @constraint(model, c1, x + y <= 1) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 0 @@ -472,84 +397,25 @@ function test_iis() @constraint(model, c2, x + y >= 2) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 0 - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) @test isempty(solver.results) @test solver.status == MOI.COMPUTE_CONFLICT_NOT_CALLED return end -function test_iis_no_deletion_filter() - model = Model(HiGHS.Optimizer) - set_silent(model) - @variable(model, 0 <= x <= 10) - @variable(model, 0 <= y <= 20) - @constraint(model, c1, x + y <= 1) - @constraint(model, c2, x + y >= 2) - @objective(model, Max, x + y) - optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 0 - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) - @test MOI.get(solver, MOIIS.DeletionFilter()) == true - MOI.set(solver, MOIIS.DeletionFilter(), false) - @test MOI.get(solver, MOIIS.DeletionFilter()) == false - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() - @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) - return -end - -function test_iis_ignore_integrality() - model = Model(HiGHS.Optimizer) - set_silent(model) - @variable(model, 0 <= x <= 10) - @variable(model, 0 <= y <= 20, Bin) - @constraint(model, c1, x + y <= 1) - @constraint(model, c2, x + y >= 2) - @objective(model, Max, x + y) - optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 0 - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) - @test MOI.get(solver, MOIIS.ElasticFilterIgnoreIntegrality()) == false - MOI.set(solver, MOIIS.ElasticFilterIgnoreIntegrality(), true) - @test MOI.get(solver, MOIIS.ElasticFilterIgnoreIntegrality()) == true - MOI.compute_conflict!(solver) - data = solver.results - @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() - @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) - return -end - function test_pass_attribute_inner() model = Model(HiGHS.Optimizer) set_silent(model) @@ -559,15 +425,20 @@ function test_pass_attribute_inner() @constraint(model, c2, x + y >= 2) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) - MOI.set(solver, MOIIS.InnerOptimizerAttribute(MOI.TimeLimitSec()), 10.0) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set( + solver, + MathOptIIS.InnerOptimizer(), + MOI.OptimizerWithAttributes( + HiGHS.Optimizer, + MOI.TimeLimitSec() => 10.0, + ), + ) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) return end @@ -581,14 +452,13 @@ function test_iis_free_var() @constraint(model, c2, x + y >= 2) @objective(model, Max, -2x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) return end @@ -603,14 +473,13 @@ function test_iis_multiple() @constraint(model, c2, x + y >= 2) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing iis = data[].constraints @test length(iis) == 2 @test Set(iis) ⊆ Set([index(c3), index(c2), index(c1)]) @@ -627,14 +496,13 @@ function test_iis_interval_right() @constraint(model, c2, x + y >= 2) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) return end @@ -648,14 +516,13 @@ function test_iis_interval_left() @constraint(model, c2, 2 <= x + y <= 5) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) return end @@ -672,53 +539,19 @@ function test_iis_spare() @constraint(model, c2, x + y >= 2) @objective(model, Max, x + y) optimize!(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[1].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c2), index(c1)]) - @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(c0)) == - MOI.NOT_IN_CONFLICT - @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(c00)) == - MOI.NOT_IN_CONFLICT - @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(c1)) == - MOI.IN_CONFLICT - @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(c2)) == - MOI.IN_CONFLICT - @test MOI.get( - solver, - MOI.ConstraintConflictStatus(), - index(LowerBoundRef(x)), - ) == MOI.MAYBE_IN_CONFLICT - @test MOI.get( - solver, - MOI.ConstraintConflictStatus(), - index(LowerBoundRef(y)), - ) == MOI.MAYBE_IN_CONFLICT - @test MOI.get( - solver, - MOI.ConstraintConflictStatus(), - index(UpperBoundRef(x)), - ) == MOI.MAYBE_IN_CONFLICT - @test MOI.get( - solver, - MOI.ConstraintConflictStatus(), - index(UpperBoundRef(y)), - ) == MOI.MAYBE_IN_CONFLICT - @test MOI.get( - solver, - MOI.ConstraintConflictStatus(), - index(LowerBoundRef(z)), - ) == MOI.NOT_IN_CONFLICT - @test MOI.get( - solver, - MOI.ConstraintConflictStatus(), - index(UpperBoundRef(z)), - ) == MOI.NOT_IN_CONFLICT + result = Dict(c1 => MOI.IN_CONFLICT, c2 => MOI.IN_CONFLICT) + for ci in all_constraints(model; include_variable_in_set_constraints = true) + @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(ci)) == + get(result, ci, MOI.NOT_IN_CONFLICT) + end return end @@ -728,16 +561,13 @@ function test_iis_binary() @variable(model, x, Bin) @constraint(model, c1, x == 1 / 2) optimize!(model) - @show termination_status(model) - @show primal_status(model) - solver = MOIIS.Optimizer() - MOI.set(solver, MOIIS.InfeasibleModel(), backend(model)) - MOI.set(solver, MOIIS.InnerOptimizer(), HiGHS.Optimizer) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) MOI.compute_conflict!(solver) data = solver.results @test length(data) == 1 - @test data[].irreducible - @test data[].metadata == MOIIS.NoData() + @test data[].metadata === nothing @test _isequal_unordered(data[].constraints, [index(c1)]) @test MOI.get(solver, MOI.ConstraintConflictStatus(), index(c1)) == MOI.IN_CONFLICT @@ -749,10 +579,578 @@ function test_iis_binary() return end -function test_deprecated() - @test (@test_deprecated MOIIS.ConflictCount()) == MOI.ConflictCount() - @test (@test_deprecated MOIIS.ConstraintConflictStatus(2)) == - MOI.ConstraintConflictStatus(2) +function test_verbose() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y) + @constraint(model, 2 * y <= 40) + @constraint(model, c, x + y >= 35) + @objective(model, Max, x + y) + optimize!(model) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MOI.Silent(), false) + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) + dir = mktempdir() + open(joinpath(dir, "log.stdout"), "w") do io + return redirect_stdout(io) do + return MOI.compute_conflict!(solver) + end + end + contents = """ + [MathOptIIS] starting compute_conflict! + [MathOptIIS] model termination status: INFEASIBLE + [MathOptIIS] model primal status: NO_SOLUTION + [MathOptIIS] starting bound analysis + [MathOptIIS] bound analysis found 0 infeasible subsets + [MathOptIIS] starting range analysis + [MathOptIIS] analyzing MOI.ScalarAffineFunction{Float64} -in- MOI.GreaterThan{Float64} + [MathOptIIS] analyzing MOI.ScalarAffineFunction{Float64} -in- MOI.LessThan{Float64} + [MathOptIIS] range analysis found 0 infeasible subsets + [MathOptIIS] starting elastic filter + [MathOptIIS] relaxing integrality if required + [MathOptIIS] constructing the penalty relaxation + [MathOptIIS] using INFEASIBILITY_CERTIFICATE to construct candidate set + [MathOptIIS] size of the candidate set: 2 + [MathOptIIS] starting the deletion filter + [MathOptIIS] size of the candidate set: 2 + [MathOptIIS] elastic filter found 1 infeasible subsets + """ + @test read(joinpath(dir, "log.stdout"), String) == contents + return +end + +function _copy_conflict(model::MOI.ModelLike, solver::MathOptIIS.Optimizer) + filter_fn(::Any) = true + function filter_fn(cref::MOI.ConstraintIndex) + for i in 1:MOI.get(solver, MOI.ConflictCount()) + status = MOI.get(solver, MOI.ConstraintConflictStatus(i), cref) + if status != MOI.NOT_IN_CONFLICT + return true + end + end + return false + end + new_model = MOI.Utilities.Model{Float64}() + filtered_src = MOI.Utilities.ModelFilter(filter_fn, model) + MOI.copy_to(new_model, filtered_src) + MOI.set(new_model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) + return new_model +end + +function _print_model(model) + return replace(sprint(print, model), "-0.0" => "0.0") +end + +function _test_compute_conflict( + input, + output; + silent::Bool = true, + optimizer = HiGHS.Optimizer, + kwargs..., +) + model = MOI.instantiate(optimizer; kwargs...) + MOI.set(model, MOI.Silent(), true) + MOI.Utilities.loadfromstring!(model, input) + MOI.optimize!(model) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), model) + MOI.set(solver, MathOptIIS.InnerOptimizer(), optimizer) + MOI.set(solver, MOI.Silent(), silent) + MOI.compute_conflict!(solver) + @test MOI.get(solver, MOI.ConflictCount()) > 0 + @test MOI.get(solver, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND + iis = _copy_conflict(model, solver) + target = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(target, output) + A, B = _print_model(iis), _print_model(target) + if A != B + @info "IIS" + print(A) + @info "Target" + println(B) + end + @test A == B + return +end + +function test_relax_integrality_integer() + _test_compute_conflict( + """ + variables: x, y + maxobjective: 1.0 * x + y + 2.0 * y <= 40.0 + 1.0 * x + y >= 35.0 + x >= 0.0 + x <= 10.0 + x in Integer() + y >= 0.0 + """, + """ + variables: x, y + 2.0 * y <= 40.0 + 1.0 * x + y >= 35.0 + x <= 10.0 + """, + ) + return +end + +function test_relax_integrality_integer_continuous_infeasible() + _test_compute_conflict( + """ + variables: x + maxobjective: 1.0 * x + x in Integer() + 1.1 * x == 1.0 + """, + """ + variables: x + x in Integer() + 1.1 * x == 1.0 + """, + ) + return +end + +function test_relax_integrality_zero_one() + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + x == 1.0 + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x == 1.0 + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + x in Interval(0.3, 1.2) + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x in ZeroOne() + x in Interval(0.3, 1.2) + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + x in Interval(0.8, 1.2) + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x in Interval(0.8, 1.2) + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + x >= 0.8 + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x >= 0.8 + 1.0 * y <= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + x <= 0.4 + 1.0 * y >= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x <= 0.4 + 1.0 * y >= 0.5 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + 1.0 * y >= 1.5 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x in ZeroOne() + 1.0 * y >= 1.5 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + _test_compute_conflict( + """ + variables: x, y + x in ZeroOne() + 1.0 * y <= -0.1 + 1.0 * x + -1.0 * y == 0.0 + """, + """ + variables: x, y + x in ZeroOne() + 1.0 * y <= -0.1 + 1.0 * x + -1.0 * y == 0.0 + """, + ) + return +end + +function test_model_with_bound_and_range_error() + _test_compute_conflict( + """ + variables: x + x in ZeroOne() + x == 2.0 + 1.0 * x <= -1.0 + 1.0 * x >= 0.0 + """, + """ + variables: x + x in ZeroOne() + x == 2.0 + 1.0 * x <= -1.0 + """, + ) + return +end + +function test_model_with_invalid_bound_and_range_error() + _test_compute_conflict( + """ + variables: x + x in Interval(2.0, 1.0) + 1.0 * x <= -1.0 + 1.0 * x >= 0.0 + """, + """ + variables: x + x in Interval(2.0, 1.0) + 1.0 * x <= -1.0 + """, + ) + return +end + +function test_check_interrupt() + function _test_check_interrupt(err) + return disable_sigint() do + return MathOptIIS._check_interrupt(() -> throw(err)) + end + end + @test _test_check_interrupt(InterruptException()) == true + @test_throws ArgumentError("") _test_check_interrupt(ArgumentError("")) + return +end + +function test_with_two_non_overlapping_iis() + _test_compute_conflict( + """ + variables: x, y + 1.0 * x <= -1.0 + 1.0 * x >= 0.0 + 1.0 * y <= -1.0 + 1.0 * y >= 0.0 + """, + # We return x, but it could equally be `y`. I'm not sure if this is + # flakey or not, because it should depend on the order that we iterate + # through the constraints. + """ + variables: x, y + 1.0 * x <= -1.0 + 1.0 * x >= 0.0 + """, + ) + return +end + +function test_a_large_iis_with_all_constraints() + n = 97 + vars = join(["x_$i" for i in 1:n], ", ") + model = """ + variables: $vars + 1.0 * x_1 + -1.0 * x_$n == 1.0 + """ + for i in 2:n + model *= "1.0 * x_$(i-1) + -1.0 * x_$i == 0.0\n" + end + _test_compute_conflict(model, model) + return +end + +function test_enlight4() + model = read_from_file(joinpath(@__DIR__, "data", "enlight4.mps")) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) + MOI.set(solver, MOI.Silent(), false) + MOI.compute_conflict!(solver) + iis = _copy_conflict(backend(model), solver) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + @test MOI.get(iis, MOI.NumberOfConstraints{F,S}()) == 8 + return +end + +# This problem is surprisingly difficult to solve the relaxed problem of. It +# could be useful for the HiGHS devs. +function test_enlight9() + model = read_from_file(joinpath(@__DIR__, "data", "enlight9.mps")) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) + MOI.set(solver, MOI.Silent(), false) + MOI.set(solver, MOI.TimeLimitSec(), 10.0) + MOI.compute_conflict!(solver) + @test MOI.get(solver, MOI.ConflictStatus()) == MOI.NO_CONFLICT_FOUND + return +end + +function test_scs_basic() + _test_compute_conflict( + """ + variables: x + 1.0 * x <= -1.0 + 1.0 * x >= 0.0 + """, + """ + variables: x + 1.0 * x <= -1.0 + 1.0 * x >= 0.0 + """; + optimizer = SCS.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) + return +end + +function test_scs_no_deletion() + _test_compute_conflict( + """ + variables: x, y + [1.0 * x, 1.0 * y] in Nonnegatives(2) + [-1.0 * x + -1.0 * y + -1.0] in Nonnegatives(1) + """, + """ + variables: x, y + [1.0 * x, 1.0 * y] in Nonnegatives(2) + [-1.0 * x + -1.0 * y + -1.0] in Nonnegatives(1) + """; + optimizer = SCS.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) + return +end + +function test_scs_with_deletion() + _test_compute_conflict( + """ + variables: x, y + [1.0 * x, 1.0 * y] in Nonnegatives(2) + [-1.0 * x + -1.0 * y + -1.0] in Nonnegatives(1) + 1.0 * x + 1.0 * y >= 0.0 + """, + """ + variables: x, y + [1.0 * x, 1.0 * y] in Nonnegatives(2) + [-1.0 * x + -1.0 * y + -1.0] in Nonnegatives(1) + """; + optimizer = SCS.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) + return +end + +function test_ipopt_quadratic() + _test_compute_conflict( + """ + variables: x, y + 1.0 * x * x + 1.0 * y <= 1.0 + x >= 2.0 + 1.0 * y >= 0.0 + 1.0 * x + 1.0 * y >= 0.0 + """, + """ + variables: x, y + 1.0 * x * x + 1.0 * y <= 1.0 + x >= 2.0 + 1.0 * y >= 0.0 + """; + optimizer = Ipopt.Optimizer, + with_cache_type = Float64, + silent = false, + ) + return +end + +function test_ipopt_nonlinear() + _test_compute_conflict( + """ + variables: x, y + ScalarNonlinearFunction(1.0 * x * x + 1.0 * y) <= 1.0 + x >= 2.0 + 1.0 * y >= 0.0 + 1.0 * x + 1.0 * y >= 0.0 + """, + """ + variables: x, y + ScalarNonlinearFunction(1.0 * x * x + 1.0 * y) <= 1.0 + x >= 2.0 + 1.0 * y >= 0.0 + """; + optimizer = Ipopt.Optimizer, + with_cache_type = Float64, + silent = false, + ) + return +end + +function test_get_variables() + op(a, args...) = MOI.ScalarNonlinearFunction(a, Any[args...]) + x, y, z = MOI.VariableIndex.(1:3) + for f in Any[ + # ScalarAffineFunction + 1.0*x+1.0*y, + # ScalarQuadraticFunction + 1.0*x*x+1.0*y*y, + 1.0*x*x+1.0*y, + 1.0*x*y, + # ScalarNonlinearFunction + op(:+, x, y), + op(:+, x, op(:-, y, 1.0)), + op(:+, 1.0 * x + 1.0 * y), + op(:+, x, 1.0 * y * y), + op(:+, op(:log, x), op(:cos, y)), + # AbstractVectorFunction + MOI.Utilities.vectorize([1.0 * x, 1.0 * y]), + MOI.Utilities.vectorize([1.0 * x * x, 1.0 * y * y]), + MOI.Utilities.vectorize([op(:+, op(:log, x), op(:cos, y))]), + ] + vars = Set{MOI.VariableIndex}() + MathOptIIS._get_variables!(vars, f) + @test length(vars) == 2 + @test x in vars + @test y in vars + @test !(z in vars) + end + return +end + +function test_zero_time_limit() + model = HiGHS.Optimizer() + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, 100_000) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.5)) + MOI.add_constraint.(model, x, MOI.ZeroOne()) + f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0) + MOI.add_constraint(model, f, MOI.EqualTo(0.0)) + MOI.optimize!(model) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), model) + MOI.set(solver, MathOptIIS.InnerOptimizer(), HiGHS.Optimizer) + MOI.set(solver, MOI.TimeLimitSec(), 0.0) + MOI.compute_conflict!(solver) + @test MOI.get(solver, MOI.ConflictStatus()) == MOI.NO_CONFLICT_FOUND + return +end + +function _solve_mock(mock) + highs = HiGHS.Optimizer() + MOI.set(highs, MOI.Silent(), true) + index_map = MOI.Utilities.default_copy_to(highs, mock) + MOI.optimize!(highs) + status = MOI.get(highs, MOI.TerminationStatus()) + if status == MOI.OPTIMAL + x = [index_map[xi] for xi in MOI.get(mock, MOI.ListOfVariableIndices())] + sol = MOI.get(highs, MOI.VariablePrimal(), x) + MOI.Utilities.mock_optimize!(mock, status, sol) + obj = MOI.get(highs, MOI.ObjectiveValue()) + MOI.set(mock, MOI.ObjectiveValue(), obj) + else + MOI.Utilities.mock_optimize!(mock, status) + end + return +end + +function _mock_optimizer(solver, N) + # We want the time limit to be hit after N solves + fns = convert(Vector{Any}, fill(_solve_mock, N)) + push!(fns, mock -> (solver.start_time = 0.0; _solve_mock(mock))) + return () -> begin + mock = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) + MOI.Utilities.set_mock_optimize!(mock, fns...) + return mock + end +end + +function test_time_limit_interrupt() + model = read_from_file(joinpath(@__DIR__, "data", "enlight4.mps")) + @testset "N=$N" for N in [5, 12, 13, 14, 15, 16] + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), backend(model)) + MOI.set(solver, MOI.TimeLimitSec(), 100.0) + MOI.set(solver, MathOptIIS.InnerOptimizer(), _mock_optimizer(solver, N)) + MOI.compute_conflict!(solver) + status = MOI.get(solver, MOI.ConflictStatus()) + if status != MOI.NO_CONFLICT_FOUND + @test status == MOI.CONFLICT_FOUND + iis = _copy_conflict(backend(model), solver) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + # We've set N such that it cannot find the full IIS in the time + # allowed. + @test MOI.get(iis, MOI.NumberOfConstraints{F,S}()) > 8 + end + end + return +end + +function test_scs_with_primal_dual_infeasibility() + # The [x, y] in Nonnegatives(2) is redundant, but we can't tell that because + # we can't easily relax it. + _test_compute_conflict( + """ + variables: x, y + minobjective: -1.0 * y + [x, y] in Nonnegatives(2) + [1.0 * x + -1.0 * y, 1.0 * x + -1.0 * y + -1.0] in Zeros(2) + """, + """ + variables: x, y + [x, y] in Nonnegatives(2) + [1.0 * x + -1.0 * y, 1.0 * x + -1.0 * y + -1.0] in Zeros(2) + """; + optimizer = SCS.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) return end From 9fffdc79d121272a7eaf16f7a9cf73b5f98395e8 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 11 Mar 2026 16:15:45 +1300 Subject: [PATCH 2/2] Update --- test/runtests.jl | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 98789dc..61016f5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1132,6 +1132,34 @@ function test_time_limit_interrupt() return end +function test_time_limit_interrupt_with_zero_one() + model = HiGHS.Optimizer() + # We want a model where x in {0,1} is relaxed, and [0, 1] is problematic. + # But also it's not infeasible based on the bounds. + MOI.Utilities.loadfromstring!( + model, + """ + variables: x, y + x in ZeroOne() + 1.0 * y == 0.25 + 1.0 * x + 1.0 * y == 1.5 + """, + ) + solver = MathOptIIS.Optimizer() + MOI.set(solver, MathOptIIS.InfeasibleModel(), model) + MOI.set(solver, MOI.TimeLimitSec(), 100.0) + MOI.set(solver, MathOptIIS.InnerOptimizer(), _mock_optimizer(solver, 3)) + MOI.compute_conflict!(solver) + @test MOI.get(solver, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND + iis = _copy_conflict(model, solver) + x = MOI.get(iis, MOI.VariableIndex, "x") + @test MOI.is_valid( + iis, + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value), + ) + return +end + function test_scs_with_primal_dual_infeasibility() # The [x, y] in Nonnegatives(2) is redundant, but we can't tell that because # we can't easily relax it.