diff --git a/src/constraints.jl b/src/constraints.jl index a941ea80e8a..6e13bb3cee5 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -747,6 +747,11 @@ function add_constraint( con::BridgeableConstraint, name::String = "", ) + # Like the generic `add_constraint`, convert to the value type of the model. + # This is needed for constraints that reach this method without going through + # the `model_convert` call of the `@constraint` macro, e.g. constraints built + # in `add_constraint` by an extension that delays their construction. + con = model_convert(model, con) add_bridge(model, con.bridge_type; coefficient_type = con.coefficient_type) return add_constraint(model, con.constraint, name) end diff --git a/src/macros.jl b/src/macros.jl index cb94efa0b0c..724d2f4c568 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -323,10 +323,18 @@ function model_convert(model::AbstractModel, α::Number) end function model_convert(model::AbstractModel, con::BridgeableConstraint) + # The bridge `coefficient_type` is set by `build_constraint` which does not + # have access to the model. We convert it to the value type of the model + # (keeping the same real/complex flavor) just like we convert the + # coefficients of the function. This is needed so that, e.g., the bridges + # registered for a `GenericModel{T}` with `T != Float64` use `T`. return BridgeableConstraint( model_convert(model, con.constraint), con.bridge_type; - con.coefficient_type, + coefficient_type = _complex_convert_type( + value_type(typeof(model)), + con.coefficient_type, + ), ) end diff --git a/test/test_model.jl b/test/test_model.jl index cb11ffa7bb5..12548356881 100644 --- a/test/test_model.jl +++ b/test/test_model.jl @@ -436,7 +436,59 @@ function test_macro_bridgeable() model = Model() @variable(model, x) @constraint(model, x in BridgeMe{Int,Nonnegative}(Nonnegative())) - @test NonnegativeBridge{Int} in model.bridge_types + # `model_convert` normalizes the bridge coefficient type to the value type of + # the model (`Float64`), even though `BridgeMe` requested `Int`. + @test NonnegativeBridge{Float64} in model.bridge_types + @test !(NonnegativeBridge{Int} in model.bridge_types) +end + +function test_model_convert_bridgeable_coefficient_type() + model = GenericModel{BigFloat}() + @variable(model, x) + constraint = ScalarConstraint(x, Nonnegative()) + # A real coefficient type is converted to the value type of the model. + con = BridgeableConstraint( + constraint, + NonnegativeBridge; + coefficient_type = Int, + ) + @test model_convert(model, con).coefficient_type == BigFloat + # A complex coefficient type keeps its complex flavor. + con = BridgeableConstraint( + constraint, + NonnegativeBridge; + coefficient_type = Complex{Int}, + ) + @test model_convert(model, con).coefficient_type == Complex{BigFloat} + return +end + +function test_macro_bridgeable_generic_value_type() + model = GenericModel{BigFloat}() + @variable(model, x) + @constraint(model, x in BridgeMe{Int,Nonnegative}(Nonnegative())) + @test NonnegativeBridge{BigFloat} in model.bridge_types + @test !(NonnegativeBridge{Int} in model.bridge_types) + return +end + +function test_add_constraint_bridgeable_model_convert() + # `add_constraint` applies `model_convert`, so a `BridgeableConstraint` added + # directly (not via the `@constraint` macro, e.g. by an extension that delays + # the construction of the constraint) also gets its coefficient type + # converted to the value type of the model. + model = GenericModel{BigFloat}() + @variable(model, x) + constraint = ScalarConstraint(x, Nonnegative()) + bc = BridgeableConstraint( + constraint, + NonnegativeBridge; + coefficient_type = Int, + ) + add_constraint(model, bc) + @test NonnegativeBridge{BigFloat} in model.bridge_types + @test !(NonnegativeBridge{Int} in model.bridge_types) + return end function test_bridges_add_bridgeable_con_set_optimizer()