diff --git a/docs/src/developers/extensions.md b/docs/src/developers/extensions.md index e2fbcd3fc26..9fe5f64be86 100644 --- a/docs/src/developers/extensions.md +++ b/docs/src/developers/extensions.md @@ -253,6 +253,9 @@ Rewriting my_equal_to to == When parsing a constraint you can recurse into sub-constraint (for example, the `{expr}` in `z => {x <= 1}`) by calling [`parse_constraint`](@ref). +To prevent JuMP from promoting the set to the same value type as the model, use +[`SkipModelConvertScalarSetWrapper`](@ref). + ### Build To extend the [`@constraint`](@ref) macro at build time, implement a new diff --git a/src/constraints.jl b/src/constraints.jl index dcb76d0d1c1..64c6e74655d 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -1543,16 +1543,47 @@ function relax_with_penalty!( return relax_with_penalty!(model, Dict(); default = default) end -struct _DoNotConvertSet{S} <: MOI.AbstractScalarSet +""" + SkipModelConvertScalarSetWrapper(set::MOI.AbstractScalarSet) + +JuMP uses [`model_convert`](@ref) to automatically promote [`MOI.AbstractScalarSet`](@ref) +sets to the same [`value_type`](@ref) as the model. + +In cases there this is undesirable, wrap the set in `SkipModelConvertScalarSetWrapper` +to pass the set un-changed to the solver. + +## Examples + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x); + +julia> @constraint(model, x in MOI.EqualTo(1 // 2)) +x = 0.5 + +julia> @constraint(model, x in SkipModelConvertScalarSetWrapper(MOI.EqualTo(1 // 2))) +x = 1//2 +``` +""" +struct SkipModelConvertScalarSetWrapper{S<:MOI.AbstractScalarSet} <: + MOI.AbstractScalarSet set::S end -model_convert(::AbstractModel, set::_DoNotConvertSet) = set +model_convert(::AbstractModel, set::SkipModelConvertScalarSetWrapper) = set -moi_set(c::ScalarConstraint{F,<:_DoNotConvertSet}) where {F} = c.set.set +function moi_set( + c::ScalarConstraint{F,<:SkipModelConvertScalarSetWrapper}, +) where {F} + return c.set.set +end function _build_boolean_equal_to(::Function, lhs::AbstractJuMPScalar, rhs::Bool) - return ScalarConstraint(lhs, _DoNotConvertSet(MOI.EqualTo(rhs))) + return ScalarConstraint( + lhs, + SkipModelConvertScalarSetWrapper(MOI.EqualTo(rhs)), + ) end function _build_boolean_equal_to(error_fn::Function, ::AbstractJuMPScalar, rhs) diff --git a/test/test_constraint.jl b/test/test_constraint.jl index de98fd8b331..59c734d4865 100644 --- a/test/test_constraint.jl +++ b/test/test_constraint.jl @@ -1763,4 +1763,15 @@ function test_def_equal_to_operator_bool() return end +function test_SkipModelConvertScalarSetWrapper() + model = Model() + @variable(model, x) + set = MOI.EqualTo(1 // 2) + c1 = @constraint(model, x in set) + c2 = @constraint(model, x in SkipModelConvertScalarSetWrapper(set)) + @test constraint_object(c1).set === MOI.EqualTo(0.5) + @test constraint_object(c2).set === MOI.EqualTo(1 // 2) + return end + +end # module