diff --git a/docs/src/reference/models.md b/docs/src/reference/models.md index f4994bfea8..da397aae74 100644 --- a/docs/src/reference/models.md +++ b/docs/src/reference/models.md @@ -160,6 +160,7 @@ COMPUTE_CONFLICT_NOT_CALLED NO_CONFLICT_EXISTS NO_CONFLICT_FOUND CONFLICT_FOUND +ConflictCount ConstraintConflictStatus ConflictParticipationStatusCode NOT_IN_CONFLICT diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index 2fc46c8653..a6102c5d06 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -65,9 +65,10 @@ mutable struct MockOptimizer{MT<:MOI.ModelLike,T} <: MOI.AbstractOptimizer # Constraint conflicts compute_conflict_called::Bool conflict_status::MOI.ConflictStatusCode + conflict_count::Int constraint_conflict_status::Dict{ MOI.ConstraintIndex, - MOI.ConflictParticipationStatusCode, + Dict{Int,MOI.ConflictParticipationStatusCode}, } # Basis status constraint_basis_status::Dict{ @@ -124,7 +125,11 @@ function MockOptimizer( # false, MOI.COMPUTE_CONFLICT_NOT_CALLED, - Dict{MOI.ConstraintIndex,MOI.ConflictParticipationStatusCode}(), + 0, + Dict{ + MOI.ConstraintIndex, + Dict{Int,MOI.ConflictParticipationStatusCode}, + }(), # Basis status Dict{MOI.ConstraintIndex,Dict{Int,MOI.BasisStatusCode}}(), Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}(), @@ -302,6 +307,13 @@ function MOI.set( return end +MOI.get(mock::MockOptimizer, ::MOI.ConflictCount) = mock.conflict_count + +function MOI.set(mock::MockOptimizer, ::MOI.ConflictCount, x) + mock.conflict_count = x + return +end + function MOI.set( mock::MockOptimizer, ::MOI.ConflictStatus, @@ -449,11 +461,11 @@ end function MOI.set( mock::MockOptimizer, - ::MOI.ConstraintConflictStatus, + attr::MOI.ConstraintConflictStatus, idx::MOI.ConstraintIndex, value, ) - mock.constraint_conflict_status[xor_index(idx)] = value + _safe_set_result(mock.constraint_conflict_status, attr, idx, value) return end @@ -723,11 +735,17 @@ end function MOI.get( mock::MockOptimizer, - ::MOI.ConstraintConflictStatus, + attr::MOI.ConstraintConflictStatus, idx::MOI.ConstraintIndex, ) + MOI.check_conflict_index_bounds(mock, attr) MOI.throw_if_not_valid(mock, idx) - return mock.constraint_conflict_status[xor_index(idx)] + return _safe_get_result( + mock.constraint_conflict_status, + attr, + idx, + "conflict status", + ) end function _safe_set_result( @@ -740,6 +758,9 @@ function _safe_set_result( if !haskey(dict, xored) dict[xored] = V() end + if hasproperty(attr, :conflict_index) + return dict[xored][attr.conflict_index] = value + end return dict[xored][attr.result_index] = value end @@ -754,11 +775,16 @@ function _safe_get_result( if result_to_value === nothing error("No mock $name is set for ", index_name, " `", index, "`.") end - value = get(result_to_value, attr.result_index, nothing) + data_index = if hasproperty(attr, :conflict_index) + attr.conflict_index + else + attr.result_index + end + value = get(result_to_value, data_index, nothing) if value === nothing error( "No mock $name is set for $(index_name) `$(index)` at result " * - "index `$(attr.result_index)`.", + "index `$(data_index)`.", ) end return value @@ -996,10 +1022,10 @@ end <:Vector, } dual_status::MOI.ResultStatusCode, - constraint_duals::Pair{Tuple{DataTypeDataType},<:Vector}...; - constraint_basis_status = Pair{Tuple{DataTypeDataType},<:Vector}[], + constraint_duals::Pair{Tuple{DataType,DataType},<:Vector}...; + constraint_basis_status = Pair{Tuple{DataType,DataType},<:Vector}[], variable_basis_status = MOI.BasisStatusCode[], - constraint_conflict_status = Pair{Tuple{Type,Type},<:Vector}[], + constraint_conflict_status = Pair{Tuple{DataType,DataType},<:Vector}[], ) Fake the result of a call to `optimize!` in the mock optimizer by storing the @@ -1053,28 +1079,19 @@ function mock_optimize!( MOI.set(mock, MOI.ResultCount(), 1) _set_mock_primal(mock, primal) _set_mock_dual(mock, dual_status_constraint_duals...) - for con_basis_pair in constraint_basis_status - F, S = con_basis_pair.first + for ((F, S), result) in constraint_basis_status indices = MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()) for (i, ci) in enumerate(indices) - MOI.set( - mock, - MOI.ConstraintBasisStatus(), - ci, - con_basis_pair.second[i], - ) + MOI.set(mock, MOI.ConstraintBasisStatus(), ci, result[i]) end end - for con_conflict_pair in constraint_conflict_status - F, S = con_conflict_pair.first + if length(constraint_conflict_status) > 0 + MOI.set(mock, MOI.ConflictCount(), 1) + end + for ((F, S), result) in constraint_conflict_status indices = MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()) for (i, ci) in enumerate(indices) - MOI.set( - mock, - MOI.ConstraintConflictStatus(), - ci, - con_conflict_pair.second[i], - ) + MOI.set(mock, MOI.ConstraintConflictStatus(), ci, result[i]) end end if length(variable_basis_status) > 0 diff --git a/src/attributes.jl b/src/attributes.jl index 6bc6ec2845..e3754c493c 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -217,6 +217,48 @@ function Base.showerror(io::IO, err::ResultIndexBoundsError) ) end +""" + struct ConflictIndexBoundsError{AttrType} <: Exception + attr::AttrType + conflict_count::Int + end + +An error indicating that the requested attribute `attr` could not be retrieved, +because the solver returned too few conflicts compared to what was requested. +For instance, the user tries to retrieve `ConstraintConflictStatus(2)` when +only one conflict is available, or when the model is feasible. + +See also: [`check_conflict_index_bounds`](@ref). +""" +struct ConflictIndexBoundsError{AttrType} <: Exception + attr::AttrType + conflict_count::Int +end + +""" + check_conflict_index_bounds(model::ModelLike, attr) + +This function checks whether enough conflicts are available in the `model` for +the requested `attr`, using its `conflict_index` field. If the model +does not have sufficient conflicts to answer the query, it throws a +[`ConflictIndexBoundsError`](@ref). +""" +function check_conflict_index_bounds(model::ModelLike, attr) + conflict_count = get(model, ConflictCount()) + if !(1 <= attr.conflict_index <= conflict_count) + throw(ConflictIndexBoundsError(attr, conflict_count)) + end + return +end + +function Base.showerror(io::IO, err::ConflictIndexBoundsError) + return print( + io, + "Conflict index of attribute $(err.attr) out of bounds. There are " * + "currently $(err.conflict_count) conflict(s) in the model.", + ) +end + """ supports(model::ModelLike, sub::AbstractSubmittable)::Bool @@ -1844,6 +1886,24 @@ struct ConflictStatus <: AbstractModelAttribute end attribute_value_type(::ConflictStatus) = ConflictStatusCode +""" + ConflictCount() + +An [`AbstractModelAttribute`](@ref) for the number of conflicts found by the +solver in the most recent call to [`compute_conflict!`](@ref). + +## Implementation + +Optimizers should implement the following methods: +```julia +MOI.get(::Optimizer, ::MOI.ConflictCount)::Int +``` +They should not implement [`set`](@ref) or [`supports`](@ref). +""" +struct ConflictCount <: AbstractModelAttribute end + +attribute_value_type(::ConflictCount) = Int + """ ListOfVariableAttributesSet() @@ -2673,12 +2733,38 @@ end ) """ - ConstraintConflictStatus() + ConstraintConflictStatus(conflict_index = 1) A constraint attribute to query the [`ConflictParticipationStatusCode`](@ref) indicating whether the constraint participates in the conflict. + +## `conflict_index` + +The optimizer may return multiple conflicts. See [`ConflictCount`](@ref) +for querying the number of conflicts found. + +If the solver does not have a conflict because the +`conflict_index` is beyond the available solutions (whose number is indicated by +the [`ConflictCount`](@ref) attribute), then +`MOI.check_result_index_bounds(model, ConstraintConflictStatus(conflict_index))` +will throw a [`ResultIndexBoundsError`](@ref). + +## Implementation + +Optimizers should implement the following methods: +```julia +MOI.get( + ::Optimizer, + ::MOI.ConstraintConflictStatus, + ::MOI.ConstraintIndex, +)::ConflictParticipationStatusCode +``` +They should not implement [`set`](@ref) or [`supports`](@ref). """ -struct ConstraintConflictStatus <: AbstractConstraintAttribute end +struct ConstraintConflictStatus <: AbstractConstraintAttribute + conflict_index::Int + ConstraintConflictStatus(conflict_index = 1) = new(conflict_index) +end function attribute_value_type(::ConstraintConflictStatus) return ConflictParticipationStatusCode @@ -3233,6 +3319,7 @@ function is_set_by_optimize( RawSolver, ResultCount, ConflictStatus, + ConflictCount, ConstraintConflictStatus, TerminationStatus, RawStatusString, diff --git a/test/General/attributes.jl b/test/General/attributes.jl index bd0a53be7b..134476263d 100644 --- a/test/General/attributes.jl +++ b/test/General/attributes.jl @@ -71,6 +71,7 @@ function test_attributes_integration_compute_conflict_1() @test MOI.get(optimizer, MOI.ConflictStatus()) == MOI.COMPUTE_CONFLICT_NOT_CALLED MOI.set(optimizer, MOI.ConflictStatus(), MOI.CONFLICT_FOUND) + MOI.set(optimizer, MOI.ConflictCount(), 1) MOI.set( optimizer, MOI.ConstraintConflictStatus(), @@ -97,9 +98,23 @@ function test_attributes_integration_compute_conflict_1() ) MOI.compute_conflict!(model) @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND + @test MOI.get(model, MOI.ConflictCount()) == 1 @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.NOT_IN_CONFLICT @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) == MOI.IN_CONFLICT + @test MOI.get(model, MOI.ConstraintConflictStatus(1), c1) == + MOI.NOT_IN_CONFLICT + @test MOI.get(model, MOI.ConstraintConflictStatus(1), c2) == MOI.IN_CONFLICT + @test_throws MOI.ConflictIndexBoundsError MOI.get( + model, + MOI.ConstraintConflictStatus(2), + c1, + ) + @test_throws MOI.ConflictIndexBoundsError MOI.get( + model, + MOI.ConstraintConflictStatus(2), + c2, + ) end MOI.Utilities.@model( @@ -127,8 +142,10 @@ function test_attributes_integration_compute_conflict_2() @test MOI.get(optimizer, MOI.ConflictStatus()) == MOI.COMPUTE_CONFLICT_NOT_CALLED MOI.set(optimizer, MOI.ConflictStatus(), MOI.CONFLICT_FOUND) + MOI.set(optimizer, MOI.ConflictCount(), 1) MOI.compute_conflict!(model) @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND + @test MOI.get(model, MOI.ConflictCount()) == 1 @test_throws ArgumentError MOI.get(model, MOI.ConstraintConflictStatus(), c) end diff --git a/test/General/errors.jl b/test/General/errors.jl index 91c109530e..33eb14d2c4 100644 --- a/test/General/errors.jl +++ b/test/General/errors.jl @@ -300,6 +300,16 @@ function test_errors_ResultIndexBoundsError() " bounds. There are currently 0 solution(s) in the model." end +function test_errors_ConflictIndexBoundsError() + @test sprint( + showerror, + MOI.ConflictIndexBoundsError(MOI.ConstraintConflictStatus(1), 0), + ) == + "Conflict index of attribute " * + "MathOptInterface.ConstraintConflictStatus(1) out of bounds. " * + "There are currently 0 conflict(s) in the model." +end + function test_errors_InvalidCalbackUsage() @test sprint( showerror, diff --git a/test/Utilities/mockoptimizer.jl b/test/Utilities/mockoptimizer.jl index 7ec29a0821..8cb2b56786 100644 --- a/test/Utilities/mockoptimizer.jl +++ b/test/Utilities/mockoptimizer.jl @@ -158,8 +158,8 @@ function test_conflict_access() c = MOI.add_constraint(mock, 1fx + fy, MOI.LessThan(1)) MOI.set(mock, MOI.ConstraintConflictStatus(), cx, MOI.NOT_IN_CONFLICT) MOI.set(mock, MOI.ConstraintConflictStatus(), c, MOI.IN_CONFLICT) + MOI.set(mock, MOI.ConflictCount(), 1) MOI.compute_conflict!(mock) - @test MOI.get(mock, MOI.ConstraintConflictStatus(), cx) == MOI.NOT_IN_CONFLICT @test MOI.get(mock, MOI.ConstraintConflictStatus(), c) == MOI.IN_CONFLICT