Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
91 changes: 89 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,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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3233,6 +3319,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