Skip to content

Commit d060acd

Browse files
authored
Add ConflictCount and conflict_index to ConstraintConflictStatus (#2775)
1 parent 2b3e0bd commit d060acd

File tree

6 files changed

+162
-30
lines changed

6 files changed

+162
-30
lines changed

docs/src/reference/models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ COMPUTE_CONFLICT_NOT_CALLED
160160
NO_CONFLICT_EXISTS
161161
NO_CONFLICT_FOUND
162162
CONFLICT_FOUND
163+
ConflictCount
163164
ConstraintConflictStatus
164165
ConflictParticipationStatusCode
165166
NOT_IN_CONFLICT

src/Utilities/mockoptimizer.jl

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ mutable struct MockOptimizer{MT<:MOI.ModelLike,T} <: MOI.AbstractOptimizer
6565
# Constraint conflicts
6666
compute_conflict_called::Bool
6767
conflict_status::MOI.ConflictStatusCode
68+
conflict_count::Int
6869
constraint_conflict_status::Dict{
6970
MOI.ConstraintIndex,
70-
MOI.ConflictParticipationStatusCode,
71+
Dict{Int,MOI.ConflictParticipationStatusCode},
7172
}
7273
# Basis status
7374
constraint_basis_status::Dict{
@@ -124,7 +125,11 @@ function MockOptimizer(
124125
#
125126
false,
126127
MOI.COMPUTE_CONFLICT_NOT_CALLED,
127-
Dict{MOI.ConstraintIndex,MOI.ConflictParticipationStatusCode}(),
128+
0,
129+
Dict{
130+
MOI.ConstraintIndex,
131+
Dict{Int,MOI.ConflictParticipationStatusCode},
132+
}(),
128133
# Basis status
129134
Dict{MOI.ConstraintIndex,Dict{Int,MOI.BasisStatusCode}}(),
130135
Dict{MOI.VariableIndex,Dict{Int,MOI.BasisStatusCode}}(),
@@ -302,6 +307,13 @@ function MOI.set(
302307
return
303308
end
304309

310+
MOI.get(mock::MockOptimizer, ::MOI.ConflictCount) = mock.conflict_count
311+
312+
function MOI.set(mock::MockOptimizer, ::MOI.ConflictCount, x)
313+
mock.conflict_count = x
314+
return
315+
end
316+
305317
function MOI.set(
306318
mock::MockOptimizer,
307319
::MOI.ConflictStatus,
@@ -449,11 +461,11 @@ end
449461

450462
function MOI.set(
451463
mock::MockOptimizer,
452-
::MOI.ConstraintConflictStatus,
464+
attr::MOI.ConstraintConflictStatus,
453465
idx::MOI.ConstraintIndex,
454466
value,
455467
)
456-
mock.constraint_conflict_status[xor_index(idx)] = value
468+
_safe_set_result(mock.constraint_conflict_status, attr, idx, value)
457469
return
458470
end
459471

@@ -723,11 +735,17 @@ end
723735

724736
function MOI.get(
725737
mock::MockOptimizer,
726-
::MOI.ConstraintConflictStatus,
738+
attr::MOI.ConstraintConflictStatus,
727739
idx::MOI.ConstraintIndex,
728740
)
741+
MOI.check_conflict_index_bounds(mock, attr)
729742
MOI.throw_if_not_valid(mock, idx)
730-
return mock.constraint_conflict_status[xor_index(idx)]
743+
return _safe_get_result(
744+
mock.constraint_conflict_status,
745+
attr,
746+
idx,
747+
"conflict status",
748+
)
731749
end
732750

733751
function _safe_set_result(
@@ -740,6 +758,9 @@ function _safe_set_result(
740758
if !haskey(dict, xored)
741759
dict[xored] = V()
742760
end
761+
if hasproperty(attr, :conflict_index)
762+
return dict[xored][attr.conflict_index] = value
763+
end
743764
return dict[xored][attr.result_index] = value
744765
end
745766

@@ -754,11 +775,16 @@ function _safe_get_result(
754775
if result_to_value === nothing
755776
error("No mock $name is set for ", index_name, " `", index, "`.")
756777
end
757-
value = get(result_to_value, attr.result_index, nothing)
778+
data_index = if hasproperty(attr, :conflict_index)
779+
attr.conflict_index
780+
else
781+
attr.result_index
782+
end
783+
value = get(result_to_value, data_index, nothing)
758784
if value === nothing
759785
error(
760786
"No mock $name is set for $(index_name) `$(index)` at result " *
761-
"index `$(attr.result_index)`.",
787+
"index `$(data_index)`.",
762788
)
763789
end
764790
return value
@@ -996,10 +1022,10 @@ end
9961022
<:Vector,
9971023
}
9981024
dual_status::MOI.ResultStatusCode,
999-
constraint_duals::Pair{Tuple{DataTypeDataType},<:Vector}...;
1000-
constraint_basis_status = Pair{Tuple{DataTypeDataType},<:Vector}[],
1025+
constraint_duals::Pair{Tuple{DataType,DataType},<:Vector}...;
1026+
constraint_basis_status = Pair{Tuple{DataType,DataType},<:Vector}[],
10011027
variable_basis_status = MOI.BasisStatusCode[],
1002-
constraint_conflict_status = Pair{Tuple{Type,Type},<:Vector}[],
1028+
constraint_conflict_status = Pair{Tuple{DataType,DataType},<:Vector}[],
10031029
)
10041030
10051031
Fake the result of a call to `optimize!` in the mock optimizer by storing the
@@ -1053,28 +1079,19 @@ function mock_optimize!(
10531079
MOI.set(mock, MOI.ResultCount(), 1)
10541080
_set_mock_primal(mock, primal)
10551081
_set_mock_dual(mock, dual_status_constraint_duals...)
1056-
for con_basis_pair in constraint_basis_status
1057-
F, S = con_basis_pair.first
1082+
for ((F, S), result) in constraint_basis_status
10581083
indices = MOI.get(mock, MOI.ListOfConstraintIndices{F,S}())
10591084
for (i, ci) in enumerate(indices)
1060-
MOI.set(
1061-
mock,
1062-
MOI.ConstraintBasisStatus(),
1063-
ci,
1064-
con_basis_pair.second[i],
1065-
)
1085+
MOI.set(mock, MOI.ConstraintBasisStatus(), ci, result[i])
10661086
end
10671087
end
1068-
for con_conflict_pair in constraint_conflict_status
1069-
F, S = con_conflict_pair.first
1088+
if length(constraint_conflict_status) > 0
1089+
MOI.set(mock, MOI.ConflictCount(), 1)
1090+
end
1091+
for ((F, S), result) in constraint_conflict_status
10701092
indices = MOI.get(mock, MOI.ListOfConstraintIndices{F,S}())
10711093
for (i, ci) in enumerate(indices)
1072-
MOI.set(
1073-
mock,
1074-
MOI.ConstraintConflictStatus(),
1075-
ci,
1076-
con_conflict_pair.second[i],
1077-
)
1094+
MOI.set(mock, MOI.ConstraintConflictStatus(), ci, result[i])
10781095
end
10791096
end
10801097
if length(variable_basis_status) > 0

src/attributes.jl

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,48 @@ function Base.showerror(io::IO, err::ResultIndexBoundsError)
217217
)
218218
end
219219

220+
"""
221+
struct ConflictIndexBoundsError{AttrType} <: Exception
222+
attr::AttrType
223+
conflict_count::Int
224+
end
225+
226+
An error indicating that the requested attribute `attr` could not be retrieved,
227+
because the solver returned too few conflicts compared to what was requested.
228+
For instance, the user tries to retrieve `ConstraintConflictStatus(2)` when
229+
only one conflict is available, or when the model is feasible.
230+
231+
See also: [`check_conflict_index_bounds`](@ref).
232+
"""
233+
struct ConflictIndexBoundsError{AttrType} <: Exception
234+
attr::AttrType
235+
conflict_count::Int
236+
end
237+
238+
"""
239+
check_conflict_index_bounds(model::ModelLike, attr)
240+
241+
This function checks whether enough conflicts are available in the `model` for
242+
the requested `attr`, using its `conflict_index` field. If the model
243+
does not have sufficient conflicts to answer the query, it throws a
244+
[`ConflictIndexBoundsError`](@ref).
245+
"""
246+
function check_conflict_index_bounds(model::ModelLike, attr)
247+
conflict_count = get(model, ConflictCount())
248+
if !(1 <= attr.conflict_index <= conflict_count)
249+
throw(ConflictIndexBoundsError(attr, conflict_count))
250+
end
251+
return
252+
end
253+
254+
function Base.showerror(io::IO, err::ConflictIndexBoundsError)
255+
return print(
256+
io,
257+
"Conflict index of attribute $(err.attr) out of bounds. There are " *
258+
"currently $(err.conflict_count) conflict(s) in the model.",
259+
)
260+
end
261+
220262
"""
221263
supports(model::ModelLike, sub::AbstractSubmittable)::Bool
222264
@@ -1844,6 +1886,24 @@ struct ConflictStatus <: AbstractModelAttribute end
18441886

18451887
attribute_value_type(::ConflictStatus) = ConflictStatusCode
18461888

1889+
"""
1890+
ConflictCount()
1891+
1892+
An [`AbstractModelAttribute`](@ref) for the number of conflicts found by the
1893+
solver in the most recent call to [`compute_conflict!`](@ref).
1894+
1895+
## Implementation
1896+
1897+
Optimizers should implement the following methods:
1898+
```julia
1899+
MOI.get(::Optimizer, ::MOI.ConflictCount)::Int
1900+
```
1901+
They should not implement [`set`](@ref) or [`supports`](@ref).
1902+
"""
1903+
struct ConflictCount <: AbstractModelAttribute end
1904+
1905+
attribute_value_type(::ConflictCount) = Int
1906+
18471907
"""
18481908
ListOfVariableAttributesSet()
18491909
@@ -2673,12 +2733,38 @@ end
26732733
)
26742734

26752735
"""
2676-
ConstraintConflictStatus()
2736+
ConstraintConflictStatus(conflict_index = 1)
26772737
26782738
A constraint attribute to query the [`ConflictParticipationStatusCode`](@ref)
26792739
indicating whether the constraint participates in the conflict.
2740+
2741+
## `conflict_index`
2742+
2743+
The optimizer may return multiple conflicts. See [`ConflictCount`](@ref)
2744+
for querying the number of conflicts found.
2745+
2746+
If the solver does not have a conflict because the
2747+
`conflict_index` is beyond the available solutions (whose number is indicated by
2748+
the [`ConflictCount`](@ref) attribute), then
2749+
`MOI.check_result_index_bounds(model, ConstraintConflictStatus(conflict_index))`
2750+
will throw a [`ResultIndexBoundsError`](@ref).
2751+
2752+
## Implementation
2753+
2754+
Optimizers should implement the following methods:
2755+
```julia
2756+
MOI.get(
2757+
::Optimizer,
2758+
::MOI.ConstraintConflictStatus,
2759+
::MOI.ConstraintIndex,
2760+
)::ConflictParticipationStatusCode
2761+
```
2762+
They should not implement [`set`](@ref) or [`supports`](@ref).
26802763
"""
2681-
struct ConstraintConflictStatus <: AbstractConstraintAttribute end
2764+
struct ConstraintConflictStatus <: AbstractConstraintAttribute
2765+
conflict_index::Int
2766+
ConstraintConflictStatus(conflict_index = 1) = new(conflict_index)
2767+
end
26822768

26832769
function attribute_value_type(::ConstraintConflictStatus)
26842770
return ConflictParticipationStatusCode
@@ -3233,6 +3319,7 @@ function is_set_by_optimize(
32333319
RawSolver,
32343320
ResultCount,
32353321
ConflictStatus,
3322+
ConflictCount,
32363323
ConstraintConflictStatus,
32373324
TerminationStatus,
32383325
RawStatusString,

test/General/attributes.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function test_attributes_integration_compute_conflict_1()
7171
@test MOI.get(optimizer, MOI.ConflictStatus()) ==
7272
MOI.COMPUTE_CONFLICT_NOT_CALLED
7373
MOI.set(optimizer, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
74+
MOI.set(optimizer, MOI.ConflictCount(), 1)
7475
MOI.set(
7576
optimizer,
7677
MOI.ConstraintConflictStatus(),
@@ -97,9 +98,23 @@ function test_attributes_integration_compute_conflict_1()
9798
)
9899
MOI.compute_conflict!(model)
99100
@test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
101+
@test MOI.get(model, MOI.ConflictCount()) == 1
100102
@test MOI.get(model, MOI.ConstraintConflictStatus(), c1) ==
101103
MOI.NOT_IN_CONFLICT
102104
@test MOI.get(model, MOI.ConstraintConflictStatus(), c2) == MOI.IN_CONFLICT
105+
@test MOI.get(model, MOI.ConstraintConflictStatus(1), c1) ==
106+
MOI.NOT_IN_CONFLICT
107+
@test MOI.get(model, MOI.ConstraintConflictStatus(1), c2) == MOI.IN_CONFLICT
108+
@test_throws MOI.ConflictIndexBoundsError MOI.get(
109+
model,
110+
MOI.ConstraintConflictStatus(2),
111+
c1,
112+
)
113+
@test_throws MOI.ConflictIndexBoundsError MOI.get(
114+
model,
115+
MOI.ConstraintConflictStatus(2),
116+
c2,
117+
)
103118
end
104119

105120
MOI.Utilities.@model(
@@ -127,8 +142,10 @@ function test_attributes_integration_compute_conflict_2()
127142
@test MOI.get(optimizer, MOI.ConflictStatus()) ==
128143
MOI.COMPUTE_CONFLICT_NOT_CALLED
129144
MOI.set(optimizer, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
145+
MOI.set(optimizer, MOI.ConflictCount(), 1)
130146
MOI.compute_conflict!(model)
131147
@test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
148+
@test MOI.get(model, MOI.ConflictCount()) == 1
132149
@test_throws ArgumentError MOI.get(model, MOI.ConstraintConflictStatus(), c)
133150
end
134151

test/General/errors.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ function test_errors_ResultIndexBoundsError()
300300
" bounds. There are currently 0 solution(s) in the model."
301301
end
302302

303+
function test_errors_ConflictIndexBoundsError()
304+
@test sprint(
305+
showerror,
306+
MOI.ConflictIndexBoundsError(MOI.ConstraintConflictStatus(1), 0),
307+
) ==
308+
"Conflict index of attribute " *
309+
"MathOptInterface.ConstraintConflictStatus(1) out of bounds. " *
310+
"There are currently 0 conflict(s) in the model."
311+
end
312+
303313
function test_errors_InvalidCalbackUsage()
304314
@test sprint(
305315
showerror,

test/Utilities/mockoptimizer.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ function test_conflict_access()
158158
c = MOI.add_constraint(mock, 1fx + fy, MOI.LessThan(1))
159159
MOI.set(mock, MOI.ConstraintConflictStatus(), cx, MOI.NOT_IN_CONFLICT)
160160
MOI.set(mock, MOI.ConstraintConflictStatus(), c, MOI.IN_CONFLICT)
161+
MOI.set(mock, MOI.ConflictCount(), 1)
161162
MOI.compute_conflict!(mock)
162-
163163
@test MOI.get(mock, MOI.ConstraintConflictStatus(), cx) ==
164164
MOI.NOT_IN_CONFLICT
165165
@test MOI.get(mock, MOI.ConstraintConflictStatus(), c) == MOI.IN_CONFLICT

0 commit comments

Comments
 (0)