Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/reference/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ COMPUTE_CONFLICT_NOT_CALLED
NO_CONFLICT_EXISTS
NO_CONFLICT_FOUND
CONFLICT_FOUND
ConflictCount
ConstraintConflictStatus
ConflictParticipationStatusCode
NOT_IN_CONFLICT
Expand Down
71 changes: 44 additions & 27 deletions src/Utilities/mockoptimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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}}(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
79 changes: 77 additions & 2 deletions src/attributes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -1844,6 +1886,16 @@ 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).
"""
struct ConflictCount <: AbstractModelAttribute end

attribute_value_type(::ConflictCount) = Int

"""
ListOfVariableAttributesSet()

Expand Down Expand Up @@ -2673,12 +2725,34 @@ 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)::T
```
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
Expand Down Expand Up @@ -3233,6 +3307,7 @@ function is_set_by_optimize(
RawSolver,
ResultCount,
ConflictStatus,
ConflictCount,
ConstraintConflictStatus,
TerminationStatus,
RawStatusString,
Expand Down
17 changes: 17 additions & 0 deletions test/General/attributes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(
Expand Down Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions test/General/errors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion test/Utilities/mockoptimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading