Skip to content

Commit

Permalink
Add support for generic number type
Browse files Browse the repository at this point in the history
Co-authored-by: Benoît Legat <benoit.legat@gmail.com>
  • Loading branch information
blegat authored and odow committed May 24, 2023
1 parent f7300eb commit b473e8a
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 78 deletions.
2 changes: 1 addition & 1 deletion docs/src/manual/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ julia> dual_start_value(con)
julia> set_dual_start_value(con, 2)
julia> dual_start_value(con)
2.0
2
```

Vector-valued constraints require a vector:
Expand Down
24 changes: 24 additions & 0 deletions docs/src/manual/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,30 @@ false
julia> model = Model(solver);
```

## Changing the number types

By default, the coefficients of affine and quadratic expressions are numbers
of type either `Float64` or `Complex{Float64}` (see [Complex number support](@ref)).
The type `Float64` can be changed using the [`GenericModel`](@ref) type parameter as follows:
```jldoctest number_type
julia> model = GenericModel{Rational{BigInt}}();
julia> @variable(model, x)
x
julia> a = x / 3
1//3 x
julia> typeof(a)
GenericAffExpr{Rational{BigInt}, GenericVariableRef{Rational{BigInt}}}
```
Note that this should mostly be used if the underlying solver actually solves
the problem using a number type different from `Float64`.

!!! warning
[Nonlinear Modeling](@ref) is currently only supported with `Float64` number
type.

## Print the model

By default, `show(model)` will print a summary of the problem:
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ build_variable
```@docs
build_constraint
add_constraint
model_convert
AbstractShape
shape
reshape_vector
Expand Down
13 changes: 8 additions & 5 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -560,8 +560,10 @@ representing the function and the `set` field contains the MOI set.
See also the [documentation](@ref Constraints) on JuMP's representation of
constraints for more background.
"""
struct ScalarConstraint{F<:AbstractJuMPScalar,S<:MOI.AbstractScalarSet} <:
AbstractConstraint
struct ScalarConstraint{
F<:Union{Number,AbstractJuMPScalar},
S<:MOI.AbstractScalarSet,
} <: AbstractConstraint
func::F
set::S
end
Expand Down Expand Up @@ -595,7 +597,7 @@ See also the [documentation](@ref Constraints) on JuMP's representation of
constraints.
"""
struct VectorConstraint{
F<:AbstractJuMPScalar,
F<:Union{Number,AbstractJuMPScalar},
S<:MOI.AbstractVectorSet,
Shape<:AbstractShape,
} <: AbstractConstraint
Expand All @@ -604,7 +606,7 @@ struct VectorConstraint{
shape::Shape
end
function VectorConstraint(
func::Vector{<:AbstractJuMPScalar},
func::Vector{<:Union{Number,AbstractJuMPScalar}},
set::MOI.AbstractVectorSet,
)
if length(func) != MOI.dimension(set)
Expand All @@ -619,7 +621,7 @@ function VectorConstraint(
end

function VectorConstraint(
func::AbstractVector{<:AbstractJuMPScalar},
func::AbstractVector{<:Union{Number,AbstractJuMPScalar}},
set::MOI.AbstractVectorSet,
)
# collect() is not used here so that DenseAxisArray will work
Expand Down Expand Up @@ -674,6 +676,7 @@ function add_constraint(
con::AbstractConstraint,
name::String = "",
)
con = model_convert(model, con)
# The type of backend(model) is unknown so we directly redirect to another
# function.
check_belongs_to_model(con, model)
Expand Down
2 changes: 1 addition & 1 deletion src/feasibility_checker.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ end
"""
primal_feasibility_report(
point::Function,
model::GenericModel{t};
model::GenericModel{T};
atol::T = zero(T),
skip_missing::Bool = false,
) where {T}
Expand Down
161 changes: 123 additions & 38 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ function _functionize(
end

_functionize(x) = x
_functionize(::_MA.Zero) = 0.0
_functionize(::_MA.Zero) = false

"""
parse_constraint(_error::Function, expr::Expr)
Expand Down Expand Up @@ -641,7 +641,13 @@ function build_constraint(
args...;
kwargs...,
)
return build_constraint(_error, f, MOI.GreaterThan(0.0), args...; kwargs...)
return build_constraint(
_error,
f,
MOI.GreaterThan(false),
args...;
kwargs...,
)
end

function build_constraint(
Expand All @@ -651,11 +657,11 @@ function build_constraint(
args...;
kwargs...,
)
return build_constraint(_error, f, MOI.LessThan(0.0), args...; kwargs...)
return build_constraint(_error, f, MOI.LessThan(false), args...; kwargs...)
end

function build_constraint(_error::Function, f, set::Zeros, args...; kwargs...)
return build_constraint(_error, f, MOI.EqualTo(0.0), args...; kwargs...)
return build_constraint(_error, f, MOI.EqualTo(false), args...; kwargs...)
end

function build_constraint(
Expand Down Expand Up @@ -706,55 +712,52 @@ function build_constraint(
end

function build_constraint(
_error::Function,
::Function,
v::AbstractJuMPScalar,
set::MOI.AbstractScalarSet,
)
return ScalarConstraint(v, set)
end

function _clear_constant!(expr::Union{GenericAffExpr,GenericQuadExpr})
offset = constant(expr)
add_to_expression!(expr, -offset)
return expr, offset
end

function _clear_constant!::Number)
return zero(α), α
end

function build_constraint(
_error::Function,
expr::Union{GenericAffExpr,GenericQuadExpr},
::Function,
expr::Union{Number,GenericAffExpr,GenericQuadExpr},
set::MOI.AbstractScalarSet,
)
if MOI.Utilities.supports_shift_constant(typeof(set))
offset = constant(expr)
add_to_expression!(expr, -offset)
expr, offset = _clear_constant!(expr)
new_set = MOI.Utilities.shift_constant(set, -offset)
return ScalarConstraint(expr, new_set)
else
return ScalarConstraint(expr, set)
end
end
function build_constraint(
_error::Function,
α::Number,
set::MOI.AbstractScalarSet,
)
return build_constraint(_error, convert(AffExpr, α), set)
end

function build_constraint(
_error::Function,
::_MA.Zero,
set::MOI.AbstractScalarSet,
)
return build_constraint(_error, zero(AffExpr), set)
return build_constraint(_error, false, set)
end

function build_constraint(
::Function,
x::AbstractVector{<:AbstractJuMPScalar},
x::AbstractVector{<:Union{Number,AbstractJuMPScalar}},
set::MOI.AbstractVectorSet,
)
return VectorConstraint(x, set)
end
function build_constraint(
_error::Function,
a::Vector{<:Number},
set::MOI.AbstractVectorSet,
)
return build_constraint(_error, convert(Vector{AffExpr}, a), set)
end

function build_constraint(
_error::Function,
Expand Down Expand Up @@ -846,23 +849,100 @@ function build_constraint(
lb::Real,
ub::Real,
)
return build_constraint(_error, 1.0func, lb, ub)
return build_constraint(
_error,
one(value_type(typeof(func))) * func,
lb,
ub,
)
end

function build_constraint(
::Function,
x::AbstractVector{<:AbstractJuMPScalar},
x::AbstractVector{T},
set::MOI.SOS1,
)
return VectorConstraint(x, MOI.SOS1{Float64}(set.weights))
) where {T<:AbstractJuMPScalar}
return VectorConstraint(
x,
MOI.SOS1{value_type(variable_ref_type(T))}(set.weights),
)
end

function build_constraint(
::Function,
x::AbstractVector{<:AbstractJuMPScalar},
x::AbstractVector{T},
set::MOI.SOS2,
)
return VectorConstraint(x, MOI.SOS2{Float64}(set.weights))
) where {T<:AbstractJuMPScalar}
return VectorConstraint(
x,
MOI.SOS2{value_type(variable_ref_type(T))}(set.weights),
)
end

"""
model_convert(
model::AbstractModel,
rhs::Union{
AbstractConstraint,
Number,
AbstractJuMPScalar,
MOI.AbstractSet,
},
)
Convert the coefficients and constants of functions and sets in the `rhs` to the
coefficient type `value_type(typeof(model))`.
## Purpose
Creating and adding a constraint is a two-step process. The first step calls
[`build_constraint`](@ref), and the result of that is passed to
[`add_constraint`](@ref).
However, because [`build_constraint`](@ref) does not take the `model` as an
argument, the coefficients and constants of the function or set might be
different than `value_type(typeof(model))`.
Therefore, the result of [`build_constraint`](@ref) is converted in a call to
`model_convert` before the result is passed to [`add_constraint`](@ref).
"""
model_convert(::AbstractModel, rhs::Any) = rhs

function model_convert(model::AbstractModel, set::MOI.AbstractScalarSet)
if MOI.Utilities.supports_shift_constant(typeof(set))
T = value_type(typeof(model))
return MOI.Utilities.shift_constant(set, zero(T))
end
return set
end

function model_convert(model::AbstractModel, α::Number)
T = value_type(typeof(model))
V = variable_ref_type(model)
C = _complex_convert_type(T, typeof(α))
return convert(GenericAffExpr{C,V}, α)
end

function model_convert(model::AbstractModel, con::BridgeableConstraint)
return BridgeableConstraint(
model_convert(model, con.constraint),
con.bridge_type,
)
end

function model_convert(model::AbstractModel, con::ScalarConstraint)
return ScalarConstraint(
model_convert(model, con.func),
model_convert(model, con.set),
)
end

function model_convert(model::AbstractModel, con::VectorConstraint)
return VectorConstraint(
model_convert.(model, con.func),
model_convert(model, con.set),
con.shape,
)
end

# TODO: update 3-argument @constraint macro to pass through names like @variable
Expand Down Expand Up @@ -972,6 +1052,11 @@ function _constraint_macro(
vectorized, parsecode, buildcall = parsefun(_error, x)
_add_positional_args(buildcall, extra)
_add_kw_args(buildcall, extra_kw_args)
if vectorized
buildcall = :(model_convert.($model, $buildcall))
else
buildcall = :(model_convert($model, $buildcall))
end
name_expr = _name_call(base_name, idxvars)
new_name_expr = if isempty(set_string_name_kw_args)
Expr(:if, :(set_string_names_on_creation($model)), name_expr, "")
Expand Down Expand Up @@ -1437,12 +1522,12 @@ function _throw_error_for_invalid_sense(
end

"""
_replace_zero(x)
_replace_zero(model::M, x) where {M<:AbstractModel}
Replaces `_MA.Zero` with a floating point `0.0`.
Replaces `_MA.Zero` with a floating point `zero(value_type(M))`.
"""
_replace_zero(::_MA.Zero) = 0.0
_replace_zero(x) = x
_replace_zero(::M, ::_MA.Zero) where {M<:AbstractModel} = zero(value_type(M))
_replace_zero(::AbstractModel, x::Any) = x

"""
@objective(model::GenericModel, sense, func)
Expand Down Expand Up @@ -1504,7 +1589,7 @@ macro objective(model, args...)
$parsecode
# Don't leak a `_MA.Zero` if the objective expression is an empty
# summation, or other structure that returns `_MA.Zero()`.
$newaff = _replace_zero($newaff)
$newaff = _replace_zero($esc_model, $newaff)
set_objective($esc_model, $sense_expr, $newaff)
$newaff
end
Expand Down Expand Up @@ -1592,7 +1677,7 @@ macro expression(args...)
code = quote
# Don't leak a `_MA.Zero` if the expression is an empty summation, or
# other structure that returns `_MA.Zero()`.
_replace_zero($code)
_replace_zero($m, $code)
end
code =
Containers.container_code(idxvars, indices, code, requested_container)
Expand Down
4 changes: 3 additions & 1 deletion src/print.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ end
# Helper function that rounds carefully for the purposes of printing Reals
# e.g. 5.3 => 5.3
# 1.0 => 1
_string_round(x::Float64) = isinteger(x) ? string(round(Int, x)) : string(x)
function _string_round(x::Union{Float32,Float64})
return isinteger(x) ? string(round(Int, x)) : string(x)
end

_string_round(::typeof(abs), x::Real) = _string_round(abs(x))

Expand Down
Loading

0 comments on commit b473e8a

Please sign in to comment.