Skip to content

Commit 0f8d35c

Browse files
authored
[Utilities] improve errors and docstrings for querying attributes (#2350)
1 parent fd619d2 commit 0f8d35c

File tree

3 files changed

+98
-100
lines changed

3 files changed

+98
-100
lines changed

src/Utilities/cachingoptimizer.jl

Lines changed: 51 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -864,15 +864,34 @@ function _get_model_attribute(
864864
)
865865
end
866866

867+
function _throw_if_get_attribute_not_allowed(
868+
model::CachingOptimizer,
869+
attr;
870+
needs_optimizer_map::Bool,
871+
)
872+
# If the state(model) == EMPTY_OPTIMIZER, then
873+
# `model.model_to_optimizer_map[index]` might be empty (because copy_to
874+
# has not been called yet), or it might be full, if optimize!(dest, src)
875+
# did not leave a copy in `dest`.
876+
missing_map = needs_optimizer_map && isempty(model.model_to_optimizer_map)
877+
if state(model) == NO_OPTIMIZER || missing_map
878+
msg =
879+
"Cannot query $(attr) from `Utilities.CachingOptimizer` " *
880+
"because no optimizer is attached (the state is `$(state(model))`)."
881+
throw(MOI.GetAttributeNotAllowed(attr, msg))
882+
end
883+
return
884+
end
885+
867886
function MOI.get(model::CachingOptimizer, attr::MOI.AbstractModelAttribute)
868887
if !MOI.is_set_by_optimize(attr)
869888
return MOI.get(model.model_cache, attr)
870-
elseif state(model) == NO_OPTIMIZER
871-
error(
872-
"Cannot query $(attr) from caching optimizer because no " *
873-
"optimizer is attached.",
874-
)
875889
end
890+
_throw_if_get_attribute_not_allowed(
891+
model,
892+
attr;
893+
needs_optimizer_map = false,
894+
)
876895
return _get_model_attribute(model, attr)
877896
end
878897

@@ -901,51 +920,33 @@ function MOI.get(
901920
attr::Union{MOI.AbstractVariableAttribute,MOI.AbstractConstraintAttribute},
902921
index::MOI.Index,
903922
)
904-
if MOI.is_set_by_optimize(attr)
905-
if state(model) == NO_OPTIMIZER
906-
error(
907-
"Cannot query $(attr) from caching optimizer because no " *
908-
"optimizer is attached.",
909-
)
910-
end
911-
return map_indices(
912-
model.optimizer_to_model_map,
913-
attr,
914-
MOI.get(
915-
model.optimizer,
916-
attr,
917-
model.model_to_optimizer_map[index],
918-
)::MOI.attribute_value_type(attr),
919-
)
920-
else
923+
if !MOI.is_set_by_optimize(attr)
921924
return MOI.get(model.model_cache, attr, index)
922925
end
926+
_throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true)
927+
value = MOI.get(
928+
model.optimizer,
929+
attr,
930+
model.model_to_optimizer_map[index],
931+
)::MOI.attribute_value_type(attr)
932+
return map_indices(model.optimizer_to_model_map, attr, value)
923933
end
924934

925935
function MOI.get(
926936
model::CachingOptimizer,
927937
attr::Union{MOI.AbstractVariableAttribute,MOI.AbstractConstraintAttribute},
928938
indices::Vector{<:MOI.Index},
929939
)
930-
if MOI.is_set_by_optimize(attr)
931-
if state(model) == NO_OPTIMIZER
932-
error(
933-
"Cannot query $(attr) from caching optimizer because no " *
934-
"optimizer is attached.",
935-
)
936-
end
937-
return map_indices(
938-
model.optimizer_to_model_map,
939-
attr,
940-
MOI.get(
941-
model.optimizer,
942-
attr,
943-
map(index -> model.model_to_optimizer_map[index], indices),
944-
)::Vector{<:MOI.attribute_value_type(attr)},
945-
)
946-
else
940+
if !MOI.is_set_by_optimize(attr)
947941
return MOI.get(model.model_cache, attr, indices)
948942
end
943+
_throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true)
944+
value = MOI.get(
945+
model.optimizer,
946+
attr,
947+
map(Base.Fix1(getindex, model.model_to_optimizer_map), indices),
948+
)::Vector{<:MOI.attribute_value_type(attr)}
949+
return map_indices(model.optimizer_to_model_map, attr, value)
949950
end
950951

951952
###
@@ -961,12 +962,7 @@ function MOI.get(
961962
attr::MOI.ConstraintPrimal,
962963
index::MOI.ConstraintIndex,
963964
)
964-
if state(model) == NO_OPTIMIZER
965-
error(
966-
"Cannot query $(attr) from caching optimizer because no " *
967-
"optimizer is attached.",
968-
)
969-
end
965+
_throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true)
970966
try
971967
return MOI.get(
972968
model.optimizer,
@@ -987,12 +983,7 @@ function MOI.get(
987983
attr::MOI.ConstraintPrimal,
988984
indices::Vector{<:MOI.ConstraintIndex},
989985
)
990-
if state(model) == NO_OPTIMIZER
991-
error(
992-
"Cannot query $(attr) from caching optimizer because no " *
993-
"optimizer is attached.",
994-
)
995-
end
986+
_throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true)
996987
try
997988
return MOI.get(
998989
model.optimizer,
@@ -1055,16 +1046,15 @@ function MOI.set(
10551046
end
10561047

10571048
function MOI.get(model::CachingOptimizer, attr::MOI.AbstractOptimizerAttribute)
1058-
if state(model) == NO_OPTIMIZER
1059-
# TODO: Copyable attributes (e.g., `Silent`, `TimeLimitSec`,
1060-
# `NumberOfThreads`) should also be stored in the cache so we could
1061-
# return the value stored in the cache instead. However, for
1062-
# non-copyable attributes( e.g. `SolverName`) the error is appropriate.
1063-
error(
1064-
"Cannot query $(attr) from caching optimizer because no " *
1065-
"optimizer is attached.",
1066-
)
1067-
end
1049+
# TODO: Copyable attributes (e.g., `Silent`, `TimeLimitSec`,
1050+
# `NumberOfThreads`) should also be stored in the cache so we could
1051+
# return the value stored in the cache instead. However, for
1052+
# non-copyable attributes( e.g. `SolverName`) the error is appropriate.
1053+
_throw_if_get_attribute_not_allowed(
1054+
model,
1055+
attr;
1056+
needs_optimizer_map = false,
1057+
)
10681058
return map_indices(
10691059
model.optimizer_to_model_map,
10701060
attr,

src/attributes.jl

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,14 +2242,23 @@ end
22422242
"""
22432243
is_set_by_optimize(::AnyAttribute)
22442244
2245-
Return a `Bool` indicating whether the value of the attribute is modified
2246-
during an [`optimize!`](@ref) call, that is, the attribute is used to query
2247-
the result of the optimization.
2245+
Return a `Bool` indicating whether the value of the attribute is set during an
2246+
[`optimize!`](@ref) call, that is, the attribute is used to query the result of
2247+
the optimization.
22482248
2249-
## Important note when defining new attributes
2249+
If an attibute can be set by the user, define [`is_copyable`](@ref) instead.
2250+
2251+
An attribute cannot be both [`is_copyable`](@ref) and `is_set_by_optimize`.
2252+
2253+
## Default fallback
22502254
22512255
This function returns `false` by default so it should be implemented for
2252-
attributes that are modified by [`optimize!`](@ref).
2256+
attributes that are set by [`optimize!`](@ref).
2257+
2258+
## Undefined behavior
2259+
2260+
Querying the value of the attribute that `is_set_by_optimize` before a call to
2261+
[`optimize!`](@ref) is undefined and depends on solver-specific behavior.
22532262
"""
22542263
is_set_by_optimize(::AnyAttribute) = false
22552264

@@ -2287,27 +2296,20 @@ end
22872296
Return a `Bool` indicating whether the value of the attribute may be copied
22882297
during [`copy_to`](@ref) using [`set`](@ref).
22892298
2290-
## Important note when defining new attributes
2291-
2292-
By default `is_copyable(attr)` returns `!is_set_by_optimize(attr)`. A specific
2293-
method should be defined for attributes which are copied indirectly during
2294-
[`copy_to`](@ref). For instance, both `is_copyable` and
2295-
[`is_set_by_optimize`](@ref) return `false` for the following attributes:
2296-
2297-
* [`ListOfOptimizerAttributesSet`](@ref), [`ListOfModelAttributesSet`](@ref),
2298-
[`ListOfConstraintAttributesSet`](@ref) and
2299-
[`ListOfVariableAttributesSet`](@ref).
2300-
* [`SolverName`](@ref) and [`RawSolver`](@ref): these attributes cannot be set.
2301-
* [`NumberOfVariables`](@ref) and [`ListOfVariableIndices`](@ref): these
2302-
attributes are set indirectly by [`add_variable`](@ref) and
2303-
[`add_variables`](@ref).
2304-
* [`ObjectiveFunctionType`](@ref): this attribute is set indirectly when setting
2305-
the [`ObjectiveFunction`](@ref) attribute.
2306-
* [`NumberOfConstraints`](@ref), [`ListOfConstraintIndices`](@ref),
2307-
[`ListOfConstraintTypesPresent`](@ref), [`CanonicalConstraintFunction`](@ref),
2308-
[`ConstraintFunction`](@ref) and [`ConstraintSet`](@ref):
2309-
these attributes are set indirectly by
2310-
[`add_constraint`](@ref) and [`add_constraints`](@ref).
2299+
If an attribute `is_copyable`, then it cannot be modified by the optimizer, and
2300+
[`get`](@ref) must always return the value that was [`set`](@ref) by the user.
2301+
2302+
If an attibute is the result of an optimization, define
2303+
[`is_set_by_optimize`](@ref) instead.
2304+
2305+
An attribute cannot be both [`is_set_by_optimize`](@ref) and `is_copyable`.
2306+
2307+
## Default fallback
2308+
2309+
By default `is_copyable(attr)` returns `!is_set_by_optimize(attr)`, which is
2310+
most probably `true`.
2311+
2312+
If an attribute should not be copied, define `is_copyable(::MyAttribute) = false`.
23112313
"""
23122314
function is_copyable(attr::AnyAttribute)
23132315
return !is_set_by_optimize(attr)

test/Utilities/cachingoptimizer.jl

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,14 @@ function test_default_attributes()
179179
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
180180
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
181181
x = MOI.add_variables(model, 2)
182-
attr = MOI.VariablePrimal()
183-
exception = ErrorException(
184-
"Cannot query $(attr) from caching optimizer because no optimizer" *
185-
" is attached.",
182+
@test_throws(
183+
MOI.GetAttributeNotAllowed{MOI.VariablePrimal},
184+
MOI.get(model, MOI.VariablePrimal(), x[1]),
185+
)
186+
@test_throws(
187+
MOI.GetAttributeNotAllowed{MOI.VariablePrimal},
188+
MOI.get(model, MOI.VariablePrimal(), x),
186189
)
187-
@test_throws exception MOI.get(model, MOI.VariablePrimal(), x[1])
188-
@test_throws exception MOI.get(model, MOI.VariablePrimal(), x)
189190
for attr in (
190191
MOI.SolverName(),
191192
MOI.Silent(),
@@ -195,10 +196,7 @@ function test_default_attributes()
195196
MOI.ResultCount(),
196197
)
197198
@test_throws(
198-
ErrorException(
199-
"Cannot query $(attr) from caching optimizer because no " *
200-
"optimizer is attached.",
201-
),
199+
MOI.GetAttributeNotAllowed{typeof(attr)},
202200
MOI.get(model, attr),
203201
)
204202
end
@@ -450,7 +448,7 @@ function test_CachingOptimizer_MANUAL_mode()
450448
typeof(v),
451449
)
452450
MOI.set(m, MOIU.AttributeFromOptimizer(MOI.VariablePrimal()), v, 3.0)
453-
451+
MOI.set(s, MOI.TerminationStatus(), MOI.OPTIMAL)
454452
MOI.optimize!(m)
455453

456454
@test MOI.get(m, MOI.VariablePrimal(), v) == 3.0
@@ -828,6 +826,8 @@ MOI.get(::_GetFallbackModel1310, ::MOI.PrimalStatus) = MOI.FEASIBLE_POINT
828826

829827
MOI.get(::_GetFallbackModel1310, ::MOI.DualStatus) = MOI.FEASIBLE_POINT
830828

829+
MOI.get(::_GetFallbackModel1310, ::MOI.TerminationStatus) = MOI.OTHER_ERROR
830+
831831
MOI.get(::_GetFallbackModel1310, ::MOI.ResultCount) = 1
832832

833833
function MOI.optimize!(::_GetFallbackModel1310, model::MOI.ModelLike)
@@ -887,8 +887,14 @@ function test_ConstraintPrimal_fallback_error()
887887
)
888888
x = MOI.add_variable(model)
889889
c = MOI.add_constraint(model, x, MOI.GreaterThan(1.0))
890-
@test_throws(ErrorException, MOI.get(model, MOI.ConstraintPrimal(), c))
891-
@test_throws(ErrorException, MOI.get(model, MOI.ConstraintPrimal(), [c]))
890+
@test_throws(
891+
MOI.GetAttributeNotAllowed{MOI.ConstraintPrimal},
892+
MOI.get(model, MOI.ConstraintPrimal(), c),
893+
)
894+
@test_throws(
895+
MOI.GetAttributeNotAllowed{MOI.ConstraintPrimal},
896+
MOI.get(model, MOI.ConstraintPrimal(), [c]),
897+
)
892898
return
893899
end
894900

0 commit comments

Comments
 (0)