From 0ef5819408dd8d6cb8b6ce818d31e5c210879c7c Mon Sep 17 00:00:00 2001 From: Jean-Francois Baffier Date: Sun, 23 Jun 2024 10:41:28 +0900 Subject: [PATCH 1/6] Add usual constraints from XCSP3 (#29) * Fix AllDifferent with param too * AllDiff and AllEq * Cardinality * Channel * Circuit * Count * Cumulative * Element * Extension * Instantiation * Intention (DistDifferent) * Maximum * Minimum * MDD * NValues * No Overlap * Ordered * Regular * Sum * Test items and fixes * Fix test items --- .JuliaFormatter.toml | 1 + Project.toml | 6 +- docs/make.jl | 28 +-- src/CBLS.jl | 54 +++- src/MOI_wrapper.jl | 16 +- src/attributes.jl | 8 +- src/constraints.jl | 416 ++----------------------------- src/constraints/all_different.jl | 65 +++++ src/constraints/all_equal.jl | 86 +++++++ src/constraints/cardinality.jl | 98 ++++++++ src/constraints/channel.jl | 80 ++++++ src/constraints/circuit.jl | 80 ++++++ src/constraints/count.jl | 173 +++++++++++++ src/constraints/cumulative.jl | 94 +++++++ src/constraints/element.jl | 82 ++++++ src/constraints/extension.jl | 201 +++++++++++++++ src/constraints/instantiation.jl | 69 +++++ src/constraints/intention.jl | 59 +++++ src/constraints/maximum.jl | 74 ++++++ src/constraints/mdd.jl | 94 +++++++ src/constraints/minimum.jl | 74 ++++++ src/constraints/n_values.jl | 88 +++++++ src/constraints/no_overlap.jl | 90 +++++++ src/constraints/ordered.jl | 83 ++++++ src/constraints/regular.jl | 83 ++++++ src/constraints/sum.jl | 86 +++++++ src/objectives.jl | 19 +- src/results.jl | 3 +- src/variables.jl | 103 ++++---- test/JuMP.jl | 13 +- test/MOI_wrapper.jl | 6 +- test/TestItemRunner.jl | 5 + test/runtests.jl | 1 + 33 files changed, 1935 insertions(+), 503 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 src/constraints/all_different.jl create mode 100644 src/constraints/all_equal.jl create mode 100644 src/constraints/cardinality.jl create mode 100644 src/constraints/channel.jl create mode 100644 src/constraints/circuit.jl create mode 100644 src/constraints/count.jl create mode 100644 src/constraints/cumulative.jl create mode 100644 src/constraints/element.jl create mode 100644 src/constraints/extension.jl create mode 100644 src/constraints/instantiation.jl create mode 100644 src/constraints/intention.jl create mode 100644 src/constraints/maximum.jl create mode 100644 src/constraints/mdd.jl create mode 100644 src/constraints/minimum.jl create mode 100644 src/constraints/n_values.jl create mode 100644 src/constraints/no_overlap.jl create mode 100644 src/constraints/ordered.jl create mode 100644 src/constraints/regular.jl create mode 100644 src/constraints/sum.jl create mode 100644 test/TestItemRunner.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..453925c --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1 @@ +style = "sciml" \ No newline at end of file diff --git a/Project.toml b/Project.toml index a8e340b..fcffc56 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Jean-Francois Baffier"] version = "0.1.13" [deps] +ConstraintCommons = "e37357d9-0691-492f-a822-e5ea6a920954" ConstraintDomains = "5800fd60-8556-4464-8d61-84ebf7a0bedb" Constraints = "30f324ab-b02d-43f0-b619-e131c61659f7" Intervals = "d8418881-c3e1-53bb-8760-2df7ec849ed5" @@ -11,6 +12,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Lazy = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0" LocalSearchSolvers = "2b10edaa-728d-4283-ac71-07e312d6ccf3" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] ConstraintDomains = "0.3" @@ -23,7 +25,9 @@ MathOptInterface = "1" julia = "1.6" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Test"] +test = ["Aqua", "Test", "TestItemRunner"] \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index dc38de0..75d15bb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,24 +1,24 @@ using CBLS using Documenter -DocMeta.setdocmeta!(CBLS, :DocTestSetup, :(using CBLS); recursive=true) +DocMeta.setdocmeta!(CBLS, :DocTestSetup, :(using CBLS); recursive = true) makedocs(; - modules=[CBLS], - authors="Jean-Francois Baffier", - repo="https://github.com/JuliaConstraints/CBLS.jl/blob/{commit}{path}#{line}", - sitename="CBLS.jl", - format=Documenter.HTML(; - prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://JuliaConstraints.github.io/CBLS.jl", - assets=String[], + modules = [CBLS], + authors = "Jean-Francois Baffier", + repo = "https://github.com/JuliaConstraints/CBLS.jl/blob/{commit}{path}#{line}", + sitename = "CBLS.jl", + format = Documenter.HTML(; + prettyurls = get(ENV, "CI", "false") == "true", + canonical = "https://JuliaConstraints.github.io/CBLS.jl", + assets = String[] ), - pages=[ - "Home" => "index.md", - ], + pages = [ + "Home" => "index.md" + ] ) deploydocs(; - repo="github.com/JuliaConstraints/CBLS.jl", - devbranch="main", + repo = "github.com/JuliaConstraints/CBLS.jl", + devbranch = "main" ) diff --git a/src/CBLS.jl b/src/CBLS.jl index 4c0f00d..e636f28 100644 --- a/src/CBLS.jl +++ b/src/CBLS.jl @@ -1,12 +1,14 @@ module CBLS -using Constraints +using ConstraintCommons using ConstraintDomains +using Constraints using Intervals using JuMP using Lazy using LocalSearchSolvers using MathOptInterface +using TestItems # Const const LS = LocalSearchSolvers @@ -28,19 +30,27 @@ const VAR_TYPES = Union{MOI.ZeroOne, MOI.Integer} export DiscreteSet # Export: Constraints +export Error, Predicate + export AllDifferent export AllEqual -export AllEqualParam -export AlwaysTrue -export DistDifferent -export Eq -export Error -export LessThanParam -export MinusEqualParam +export Cardinality, CardinalityClosed, CardinalityOpen +export Channel +export Circuit +export Count, AtLeast, AtMost, Exactly +export Cumulative +export Element +export Extension, Supports, Conflicts +export Instantiation +export DistDifferent # Implementation of an intensional constraint +export Maximum +export MDDConstraint +export Minimum +export NValues +export NoOverlap, NoOverlapNoZero, NoOverlapWithZero export Ordered -export Predicate -export SequentialTasks -export SumEqualParam +export Regular +export Sum #Export: Scalar objective function export ScalarFunction @@ -49,7 +59,29 @@ export ScalarFunction include("MOI_wrapper.jl") include("attributes.jl") include("variables.jl") + +## Constraints include("constraints.jl") +include("constraints/all_different.jl") +include("constraints/all_equal.jl") +include("constraints/cardinality.jl") +include("constraints/channel.jl") +include("constraints/circuit.jl") +include("constraints/count.jl") +include("constraints/cumulative.jl") +include("constraints/element.jl") +include("constraints/extension.jl") +include("constraints/instantiation.jl") +include("constraints/intention.jl") +include("constraints/maximum.jl") +include("constraints/mdd.jl") +include("constraints/minimum.jl") +include("constraints/n_values.jl") +include("constraints/no_overlap.jl") +include("constraints/ordered.jl") +include("constraints/regular.jl") +include("constraints/sum.jl") + include("objectives.jl") include("results.jl") diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 3b00126..fca601b 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -9,10 +9,10 @@ DOCSTRING - `set`: DESCRIPTION """ function JuMP.build_variable( - ::Function, - info::JuMP.VariableInfo, - set::T, -) where {T<:MOI.AbstractScalarSet} + ::Function, + info::JuMP.VariableInfo, + set::T +) where {T <: MOI.AbstractScalarSet} return JuMP.VariableConstrainedOnCreation(JuMP.ScalarVariable(info), set) end @@ -42,7 +42,7 @@ function Optimizer(model = model(); options = Options()) solver(model, options = options), Set{Int}(), Set{Int}() - ) + ) end # forward functions from Solver @@ -102,7 +102,7 @@ struct DiscreteSet{T <: Number} <: MOI.AbstractScalarSet values::Vector{T} end DiscreteSet(values) = DiscreteSet(collect(values)) -DiscreteSet(values::T...) where {T<:Number} = DiscreteSet(collect(values)) +DiscreteSet(values::T...) where {T <: Number} = DiscreteSet(collect(values)) """ Base.copy(set::DiscreteSet) = begin @@ -118,7 +118,9 @@ DOCSTRING """ MOI.empty!(opt) = empty!(opt) - function MOI.is_valid(optimizer::Optimizer, index::CI{VI, MOI.Integer}) return index.value ∈ optimizer.int_vars end + +Base.copy(op::F) where {F <: Function} = op +Base.copy(::Nothing) = nothing diff --git a/src/attributes.jl b/src/attributes.jl index 04396fd..12490d3 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -8,7 +8,7 @@ MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = true MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value::Union{Nothing,Float64}) Set the time limit """ -function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value::Union{Nothing,Float64}) +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value::Union{Nothing, Float64}) set_option!(model, "time_limit", isnothing(value) ? Inf : value) end function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) @@ -20,14 +20,14 @@ end MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value) Set a RawOptimizerAttribute to `value` """ -MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value) = set_option!(model, p.name, value) +MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value) = set_option!( + model, p.name, value) MOI.get(model::Optimizer, p::MOI.RawOptimizerAttribute) = get_option(model, p.name) - function MOI.set(model::Optimizer, ::MOI.NumberOfThreads, value) set_option!(model, "threads", isnothing(value) ? typemax(0) : value) end function MOI.get(model::Optimizer, ::MOI.NumberOfThreads) nt = get_option(model, "threads") return nt == typemax(0) ? nothing : nt -end \ No newline at end of file +end diff --git a/src/constraints.jl b/src/constraints.jl index 45716af..0e93d56 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -43,7 +43,8 @@ DOCSTRING - `vars`: DESCRIPTION - `set`: DESCRIPTION """ -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIError{F}) where {F <: Function} +function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, + set::MOIError{F}) where {F <: Function} cidx = constraint!(optimizer, set.f, map(x -> x.value, vars.variables)) return CI{VOV, MOIError{F}}(cidx) end @@ -94,7 +95,8 @@ function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIPredicate{F ) where {F <: Function} return true end -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIPredicate{F}) where {F <: Function} +function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, + set::MOIPredicate{F}) where {F <: Function} err = x -> convert(Float64, !set.f(x)) cidx = constraint!(optimizer, err, map(x -> x.value, vars.variables)) return CI{VOV, MOIPredicate{F}}(cidx) @@ -116,405 +118,21 @@ struct Predicate{F <: Function} <: JuMP.AbstractVectorSet end JuMP.moi_set(set::Predicate, dim::Int) = MOIPredicate(set.f, dim) -""" - MOIAllDifferent <: MOI.AbstractVectorSet +## SECTION - Test Items +@testitem "Error and Predicate" begin + using CBLS + using JuMP -DOCSTRING -""" -struct MOIAllDifferent <: MOI.AbstractVectorSet - dimension::Int - - MOIAllDifferent(dim = 0) = new(dim) -end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIAllDifferent}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIAllDifferent) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:all_different])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIAllDifferent}(cidx) -end -Base.copy(set::MOIAllDifferent) = MOIAllDifferent(copy(set.dimension)) - -""" -Global constraint ensuring that all the values of a given configuration are unique. - -```julia -@constraint(model, X in AllDifferent()) -``` -""" -struct AllDifferent <: JuMP.AbstractVectorSet end -JuMP.moi_set(::AllDifferent, dim::Int) = MOIAllDifferent(dim) + model = Model(CBLS.Optimizer) -""" - MOIAllEqual <: MOI.AbstractVectorSet + @variable(model, 1≤X[1:4]≤4, Int) + @variable(model, 1≤Y[1:4]≤4, Int) -DOCSTRING -""" -struct MOIAllEqual <: MOI.AbstractVectorSet - dimension::Int + @constraint(model, X in Error(x -> x[1] + x[2] + x[3] + x[4] == 10)) + @constraint(model, Y in Predicate(x -> x[1] + x[2] + x[3] + x[4] == 10)) - MOIAllEqual(dim = 0) = new(dim) + optimize!(model) + @info "Error and Predicate" value.(X) value.(Y) + termination_status(model) + @info solution_summary(model) end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIAllEqual}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIAllEqual) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:all_equal])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIAllEqual}(cidx) -end - -Base.copy(set::MOIAllEqual) = MOIAllEqual(copy(set.dimension)) - -""" -Global constraint ensuring that all the values of `X` are all equal. - -```julia -@constraint(model, X in AllEqual()) -``` -""" -struct AllEqual <: JuMP.AbstractVectorSet end -JuMP.moi_set(::AllEqual, dim::Int) = MOIAllEqual(dim) - -""" - MOIEq <: MOI.AbstractVectorSet - -DOCSTRING -""" -struct MOIEq <: MOI.AbstractVectorSet - dimension::Int - - MOIEq(dim = 0) = new(dim) -end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIEq}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIEq) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:eq])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIEq}(cidx) -end - -Base.copy(set::MOIEq) = MOIEq(copy(set.dimension)) - -""" -Equality between two variables. - -```julia -@constraint(model, X in Eq()) -``` -""" -struct Eq <: JuMP.AbstractVectorSet end -JuMP.moi_set(::Eq, dim::Int) = MOIEq(dim) - -""" - MOIAlwaysTrue <: MOI.AbstractVectorSet - -DOCSTRING -""" -struct MOIAlwaysTrue <: MOI.AbstractVectorSet - dimension::Int - - MOIAlwaysTrue(dim = 0) = new(dim) -end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIAlwaysTrue}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIAlwaysTrue) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f(USUAL_CONSTRAINTS[:always_true])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIAlwaysTrue}(cidx) -end - -Base.copy(set::MOIAlwaysTrue) = MOIAlwaysTrue(copy(set.dimension)) - -""" -Always return `true`. Mainly used for testing purpose. - -```julia -@constraint(model, X in AlwaysTrue()) -``` -""" -struct AlwaysTrue <: JuMP.AbstractVectorSet end -JuMP.moi_set(::AlwaysTrue, dim::Int) = MOIAlwaysTrue(dim) - -""" - MOIOrdered <: MOI.AbstractVectorSet - -DOCSTRING -""" -struct MOIOrdered <: MOI.AbstractVectorSet - dimension::Int - - MOIOrdered(dim = 0) = new(dim) -end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIOrdered}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIOrdered) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:ordered])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIOrdered}(cidx) -end - -Base.copy(set::MOIOrdered) = MOIOrdered(copy(set.dimension)) - -""" -Global constraint ensuring that all the values of `x` are ordered. - -```julia -@constraint(model, X in Ordered()) -``` -""" -struct Ordered <: JuMP.AbstractVectorSet end -JuMP.moi_set(::Ordered, dim::Int) = MOIOrdered(dim) - - -""" - MOIDistDifferent <: MOI.AbstractVectorSet - -DOCSTRING -""" -struct MOIDistDifferent <: MOI.AbstractVectorSet - dimension::Int - - MOIDistDifferent(dim = 4) = new(dim) -end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIDistDifferent}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIDistDifferent) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:dist_different])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIDistDifferent}(cidx) -end -Base.copy(set::MOIDistDifferent) = MOIDistDifferent(copy(set.dimension)) - -""" -Local constraint ensuring that, given a vector `X` of size 4, `|X[1] - X[2]| ≠ |X[3] - X[4]|)`. - -```julia -@constraint(model, X in DistDifferent()) -``` -""" -struct DistDifferent <: JuMP.AbstractVectorSet end -JuMP.moi_set(::DistDifferent, dim::Int) = MOIDistDifferent(dim) - -""" - MOIAllEqualParam{T <: Number} <: MOI.AbstractVectorSet - -DOCSTRING - -# Arguments: -- `param::T`: DESCRIPTION -- `dimension::Int`: DESCRIPTION -- `MOIAllEqualParam(param, dim = 0) = begin - #= none:5 =# - new{typeof(param)}(param, dim) - end`: DESCRIPTION -""" -struct MOIAllEqualParam{T <: Number} <: MOI.AbstractVectorSet - param::T - dimension::Int - - MOIAllEqualParam(param, dim = 0) = new{typeof(param)}(param, dim) -end -function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIAllEqualParam{T}} -) where {T <: Number} - return true -end -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIAllEqualParam{T} -) where T <: Number - # max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:all_equal_param])(x; param=param, dom_size=dom_size - ) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIAllEqualParam{T}}(cidx) -end - -Base.copy(set::MOIAllEqualParam) = MOIAllEqualParam(copy(set.param), copy(set.dimension)) - -""" -Global constraint ensuring that all the values of `X` are all equal to a given parameter `param`. - -```julia -@constraint(model, X in AllEqualParam(param)) -``` -""" -struct AllEqualParam{T <: Number} <: JuMP.AbstractVectorSet - param::T -end -JuMP.moi_set(set::AllEqualParam, dim::Int) = MOIAllEqualParam(set.param, dim) - -""" - MOISumEqualParam{T <: Number} <: MOI.AbstractVectorSet - -DOCSTRING - -# Arguments: -- `param::T`: DESCRIPTION -- `dimension::Int`: DESCRIPTION -- `MOISumEqualParam(param, dim = 0) = begin - #= none:5 =# - new{typeof(param)}(param, dim) - end`: DESCRIPTION -""" -struct MOISumEqualParam{T <: Number} <: MOI.AbstractVectorSet - param::T - dimension::Int - - MOISumEqualParam(param, dim = 0) = new{typeof(param)}(param, dim) -end -function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOISumEqualParam{T}} -) where {T <: Number} - return true -end -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOISumEqualParam{T} -) where {T <: Number} - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:sum_equal_param])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOISumEqualParam{T}}(cidx) -end - -Base.copy(set::MOISumEqualParam) = MOISumEqualParam(copy(set.param), -copy(set.dimension)) - -""" -Global constraint ensuring that the sum of the values of `X` is equal to a given parameter `param`. - -```julia -@constraint(model, X in SumEqualParam(param)) -``` -""" -struct SumEqualParam{T <: Number} <: JuMP.AbstractVectorSet - param::T -end -JuMP.moi_set(set::SumEqualParam, dim::Int) = MOISumEqualParam(set.param, dim) - -""" - MOILessThanParam{T <: Number} <: MOI.AbstractVectorSet - -DOCSTRING - -# Arguments: -- `param::T`: DESCRIPTION -- `dimension::Int`: DESCRIPTION -- `MOILessThanParam(param, dim = 0) = begin - #= none:5 =# - new{typeof(param)}(param, dim) - end`: DESCRIPTION -""" -struct MOILessThanParam{T <: Number} <: MOI.AbstractVectorSet - param::T - dimension::Int - - MOILessThanParam(param, dim = 0) = new{typeof(param)}(param, dim) -end -function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOILessThanParam{T}} -) where {T <: Number} - return true -end -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOILessThanParam{T} -) where {T <: Number} - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:less_than_param])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOILessThanParam{T}}(cidx) -end - -Base.copy(set::MOILessThanParam) = MOILessThanParam(copy(set.param), -copy(set.dimension)) - -""" -Constraint ensuring that the value of `x` is less than a given parameter `param`. - -```julia -@constraint(model, x in LessThanParam(param)) -``` -""" -struct LessThanParam{T <: Number} <: JuMP.AbstractVectorSet - param::T -end -JuMP.moi_set(set::LessThanParam, dim::Int) = MOILessThanParam(set.param, dim) - -""" - MOIMinusEqualParam{T <: Number} <: MOI.AbstractVectorSet - -DOCSTRING - -# Arguments: -- `param::T`: DESCRIPTION -- `dimension::Int`: DESCRIPTION -- `MOIMinusEqualParam(param, dim = 0) = begin - #= none:5 =# - new{typeof(param)}(param, dim) - end`: DESCRIPTION -""" -struct MOIMinusEqualParam{T <: Number} <: MOI.AbstractVectorSet - param::T - dimension::Int - - MOIMinusEqualParam(param, dim = 0) = new{typeof(param)}(param, dim) -end -function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIMinusEqualParam{T}} -) where {T <: Number} - return true -end -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIMinusEqualParam{T} -) where {T <: Number} - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:minus_equal_param])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOIMinusEqualParam{T}}(cidx) -end - -Base.copy(set::MOIMinusEqualParam) = MOIMinusEqualParam(copy(set.param), -copy(set.dimension)) - -""" -Constraint ensuring that the value of `x` is less than a given parameter `param`. - -```julia -@constraint(model, x in MinusEqualParam(param)) -``` -""" -struct MinusEqualParam{T <: Number} <: JuMP.AbstractVectorSet - param::T -end -JuMP.moi_set(set::MinusEqualParam, dim::Int) = MOIMinusEqualParam(set.param, dim) - - -""" - MOISequentialTasks <: MOI.AbstractVectorSet - -DOCSTRING -""" -struct MOISequentialTasks <: MOI.AbstractVectorSet - dimension::Int - - MOISequentialTasks(dim = 4) = new(dim) -end -MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOISequentialTasks}) = true -function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOISequentialTasks) - #max_dom_size = max_domains_size(optimizer, map(x -> x.value, vars.variables)) - e = (x; kargs...) -> error_f( - USUAL_CONSTRAINTS[:sequential_tasks])(x; kargs...) - cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) - return CI{VOV, MOISequentialTasks}(cidx) -end -Base.copy(set::MOISequentialTasks) = MOISequentialTasks(copy(set.dimension)) - -""" -Local constraint ensuring that, given a vector `X` of size 4, `|X[1] - X[2]| ≠ |X[3] - X[4]|)`. - -```julia -@constraint(model, X in SequentialTasks()) -``` -""" -struct SequentialTasks <: JuMP.AbstractVectorSet end -JuMP.moi_set(::SequentialTasks, dim::Int) = MOISequentialTasks(dim) diff --git a/src/constraints/all_different.jl b/src/constraints/all_different.jl new file mode 100644 index 0000000..576a074 --- /dev/null +++ b/src/constraints/all_different.jl @@ -0,0 +1,65 @@ +""" + MOIAllDifferent <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIAllDifferent{T <: Number} <: MOI.AbstractVectorSet + vals::Vector{T} + dimension::Int + + MOIAllDifferent(vals, dim = 0) = new{eltype(vals)}(vals, dim) +end + +function MOI.supports_constraint( + ::Optimizer, ::Type{VOV}, ::Type{MOIAllDifferent{T}}) where {T <: Number} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIAllDifferent) + vals = isempty(set.vals) ? nothing : set.vals + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:vals => vals)) + return error_f(USUAL_CONSTRAINTS[:all_different])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIAllDifferent{eltype(set.vals)}}(cidx) +end + +Base.copy(set::MOIAllDifferent) = MOIAllDifferent(copy(set.vals), copy(set.dimension)) + +""" +Global constraint ensuring that all the values of a given configuration are unique. + +```julia +@constraint(model, X in AllDifferent()) +``` +""" +struct AllDifferent{T <: Number} <: JuMP.AbstractVectorSet + vals::Vector{T} + + AllDifferent(vals) = new{eltype(vals)}(vals) +end + +AllDifferent(; vals::Vector{T} = Vector{Number}()) where {T <: Number} = AllDifferent(vals) + +JuMP.moi_set(set::AllDifferent, dim::Int) = MOIAllDifferent(set.vals, dim) + +## SECTION - Test Items +@testitem "All Different" tags=[:usual, :constraints, :all_different] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤4, Int) + @variable(model, 0≤Y[1:4]≤2, Int) + + @constraint(model, X in AllDifferent()) + @constraint(model, Y in AllDifferent(; vals = [0])) + + optimize!(model) + @info "All Different" value.(X) value.(Y) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/all_equal.jl b/src/constraints/all_equal.jl new file mode 100644 index 0000000..d5417d1 --- /dev/null +++ b/src/constraints/all_equal.jl @@ -0,0 +1,86 @@ +""" + MOIAllEqual <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIAllEqual{F <: Function, T1 <: Number, T2 <: Union{Nothing, Number}} <: + MOI.AbstractVectorSet + op::F + pair_vars::Vector{T1} + val::T2 + dimension::Int + + function MOIAllEqual(op, pair_vars, val, dim = 0) + return new{typeof(op), eltype(pair_vars), typeof(val)}(op, pair_vars, val, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIAllEqual{F, T1, T2}}) where { + F <: Function, T1 <: Number, T2 <: Union{Nothing, Number}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIAllEqual) + function e(x; kwargs...) + new_kwargs = merge( + kwargs, Dict(:op => set.op, :pair_vars => set.pair_vars, :val => set.val)) + return error_f(USUAL_CONSTRAINTS[:all_equal])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIAllEqual{typeof(set.op), eltype(set.pair_vars), typeof(set.val)}}(cidx) +end + +function Base.copy(set::MOIAllEqual) + return MOIAllEqual( + copy(set.op), copy(set.pair_vars), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that all the values of `X` are all equal. + +```julia +@constraint(model, X in AllEqual()) +``` +""" +struct AllEqual{F <: Function, T1 <: Number, T2 <: Union{Nothing, Number}} <: + JuMP.AbstractVectorSet + op::F + pair_vars::Vector{T1} + val::T2 + + function AllEqual(op, pair_vars, val) + return new{typeof(op), eltype(pair_vars), typeof(val)}(op, pair_vars, val) + end +end + +function AllEqual(; op::F = +, pair_vars::Vector{T1} = Vector{Number}(), + val::T2 = nothing) where {F <: Function, T1 <: Number, T2 <: Union{Nothing, Number}} + return AllEqual(op, pair_vars, val) +end + +JuMP.moi_set(set::AllEqual, dim::Int) = MOIAllEqual(set.op, set.pair_vars, set.val, dim) + +@testitem "All Equal" tags=[:usual, :constraints, :all_equal] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 0≤X1[1:4]≤4, Int) + @variable(model, 0≤X2[1:4]≤4, Int) + @variable(model, 0≤X3[1:4]≤4, Int) + @variable(model, 0≤X4[1:4]≤4, Int) + + @constraint(model, X1 in AllEqual()) + @constraint(model, X2 in AllEqual(; pair_vars = [0, 1, 2, 3])) + @constraint(model, X3 in AllEqual(; op = /, val = 1, pair_vars = [1, 2, 3, 4])) + @constraint(model, X4 in AllEqual(; op = *, val = 1, pair_vars = [1, 2, 3, 4])) + + optimize!(model) + @info "All Equal" value.(X1) value.(X2) value.(X3) value.(X4) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/cardinality.jl b/src/constraints/cardinality.jl new file mode 100644 index 0000000..c2abb4e --- /dev/null +++ b/src/constraints/cardinality.jl @@ -0,0 +1,98 @@ +""" + MOICardinality <: MOI.AbstractVectorSet + +DOCSTRING +""" + +struct MOICardinality{T <: Number, V <: VecOrMat{T}} <: MOI.AbstractVectorSet + bool::Bool + vals::V + dimension::Int + + MOICardinality(bool, vals, dim = 0) = new{eltype(vals), typeof(vals)}(bool, vals, dim) +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOICardinality{T, V}}) where {T <: Number, V <: VecOrMat{T}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOICardinality) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:bool => set.bool, :vals => set.vals)) + return error_f(USUAL_CONSTRAINTS[:cardinality])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOICardinality{eltype(set.vals), typeof(set.vals)}}(cidx) +end + +function Base.copy(set::MOICardinality) + return MOICardinality(copy(set.bool), copy(set.vals), copy(set.dimension)) +end + +""" +Global constraint ensuring that the number of occurrences of each value in `X` is equal to the corresponding value in `vals`. + +```julia +@constraint(model, X in Cardinality(vals)) +``` +""" + +struct Cardinality{T <: Number, V <: VecOrMat{T}} <: JuMP.AbstractVectorSet + bool::Bool + vals::V + + Cardinality(bool, vals) = new{eltype(vals), typeof(vals)}(bool, vals) +end + +function Cardinality(; vals::VecOrMat{T}, bool::Bool = false) where {T <: Number} + return Cardinality(bool, vals) +end + +JuMP.moi_set(set::Cardinality, dim::Int) = MOICardinality(set.bool, set.vals, dim) + +struct CardinalityOpen{T <: Number, V <: VecOrMat{T}} <: JuMP.AbstractVectorSet + vals::V + + CardinalityOpen(vals) = new{eltype(vals), typeof(vals)}(vals) +end + +function CardinalityOpen(; vals::VecOrMat{T}) where {T <: Number} + return CardinalityOpen(vals) +end + +JuMP.moi_set(set::CardinalityOpen, dim::Int) = MOICardinality(false, set.vals, dim) + +struct CardinalityClosed{T <: Number, V <: VecOrMat{T}} <: JuMP.AbstractVectorSet + vals::V + + CardinalityClosed(vals) = new{eltype(vals), typeof(vals)}(vals) +end + +function CardinalityClosed(; vals::VecOrMat{T}) where {T <: Number} + return CardinalityClosed(vals) +end + +JuMP.moi_set(set::CardinalityClosed, dim::Int) = MOICardinality(true, set.vals, dim) + +@testitem "Cardinality" tags=[:usual, :constraints, :cardinality] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤10, Int) + @variable(model, 1≤Y[1:4]≤10, Int) + @variable(model, 1≤Z[1:4]≤10, Int) + + @constraint(model, X in Cardinality(; vals = [2 0 1; 5 1 3; 10 2 3])) + @constraint(model, Y in CardinalityOpen(; vals = [2 0 1; 5 1 3; 10 2 3])) + @constraint(model, Z in CardinalityClosed(; vals = [2 0 1; 5 1 3; 10 2 3])) + + optimize!(model) + @info "Cardinality" value.(X) value.(Y) value.(Z) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/channel.jl b/src/constraints/channel.jl new file mode 100644 index 0000000..012c747 --- /dev/null +++ b/src/constraints/channel.jl @@ -0,0 +1,80 @@ +""" + MOIChannel <: MOI.AbstractVectorSet + +DOCSTRING +""" + +struct MOIChannel{D <: Integer, I <: Integer} <: MOI.AbstractVectorSet + dim::D + id::I + dimension::Int + + function MOIChannel(dim, id, dim_moi = 0) + return new{typeof(dim), typeof(id)}(dim, id, dim_moi) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIChannel{D, I}}) where {D <: Integer, I <: Integer} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIChannel) + id = iszero(set.id) ? nothing : set.id + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:dim => set.dim, :id => id)) + return error_f(USUAL_CONSTRAINTS[:channel])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIChannel{typeof(set.dim), typeof(set.id)}}(cidx) +end + +function Base.copy(set::MOIChannel) + return MOIChannel(copy(set.dim), copy(set.id), copy(set.dimension)) +end + +""" +Global constraint ensuring that the values of `X` are a channel. + +```julia +@constraint(model, X in Channel()) +``` +""" + +struct Channel{D <: Integer, I <: Integer} <: JuMP.AbstractVectorSet + dim::D + id::I + + function Channel(dim, id) + return new{typeof(dim), typeof(id)}(dim, id) + end +end + +function Channel(; dim::D = 1, id::I = 0) where {D <: Integer, I <: Integer} + return Channel(dim, id) +end + +JuMP.moi_set(set::Channel, dim_moi::Int) = MOIChannel(set.dim, set.id, dim_moi) + +@testitem "Channel" tags=[:usual, :constraints, :channel] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤4, Int) + @variable(model, 1≤Y[1:10]≤5, Int) + @variable(model, 0≤Z[1:4]≤1, Int) + + @constraint(model, X in CBLS.Channel()) + @constraint(model, Y in CBLS.Channel(; dim = 2)) + @constraint(model, Z in CBLS.Channel(; id = 3)) + + optimize!(model) + + @info "Channel" value.(X) value.(Y) value.(Z) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/circuit.jl b/src/constraints/circuit.jl new file mode 100644 index 0000000..760aba3 --- /dev/null +++ b/src/constraints/circuit.jl @@ -0,0 +1,80 @@ +""" + MOICircuit <: MOI.AbstractVectorSet + +DOCSTRING +""" + +struct MOICircuit{F <: Function, T <: Number} <: MOI.AbstractVectorSet + op::F + val::T + dimension::Int + + function MOICircuit(op, val, dim_moi = 0) + return new{typeof(op), typeof(val)}(op, val, dim_moi) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOICircuit{F, T}}) where {F <: Function, T <: Number} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOICircuit) + val = iszero(set.val) ? length(vars.variables) : set.val + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:op => set.op, :val => val)) + return error_f(USUAL_CONSTRAINTS[:circuit])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOICircuit{typeof(set.op), typeof(set.val)}}(cidx) +end + +function Base.copy(set::MOICircuit) + return MOICircuit(copy(set.op), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that the values of `X` are a circuit. + +```julia +@constraint(model, X in Circuit()) +``` +""" + +struct Circuit{F <: Function, T <: Number} <: JuMP.AbstractVectorSet + op::F + val::T + + function Circuit(op, val) + return new{typeof(op), typeof(val)}(op, val) + end +end + +function Circuit(; op::F = ≥, val::T = 0) where {F <: Function, T <: Number} + return Circuit(op, val) +end + +JuMP.moi_set(set::Circuit, dim_moi::Int) = MOICircuit(set.op, set.val, dim_moi) + +@testitem "Circuit" tags=[:usual, :constraints, :circuit] begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤4, Int) + @variable(model, 1≤Y[1:4]≤4, Int) + @variable(model, 1≤Z[1:4]≤4, Int) + + @constraint(model, X in Circuit()) + @constraint(model, Y in Circuit(op = ==, val = 3)) + @constraint(model, Z in Circuit(op = >, val = 0)) + + optimize!(model) + + @info "Circuit" value.(X) value.(Y) value.(Z) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/count.jl b/src/constraints/count.jl new file mode 100644 index 0000000..b0c80d8 --- /dev/null +++ b/src/constraints/count.jl @@ -0,0 +1,173 @@ +""" + MOICount <: MOI.AbstractVectorSet + +DOCSTRING +""" + +struct MOICount{F <: Function, T1 <: Number, T2 <: Number} <: MOI.AbstractVectorSet + op::F + val::T1 + vals::Vector{T2} + dimension::Int + + function MOICount(op, val, vals, dim = 0) + new{typeof(op), typeof(val), eltype(vals)}(op, val, vals, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOICount{F, T1, T2}}) where {F <: Function, T1 <: Number, T2 <: Number} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOICount) + s = if set.op == == + :exactly + elseif set.op == ≥ + :at_least + elseif set.op == ≤ + :at_most + else + :count + end + function e(x; kwargs...) + d = Dict(:vals => set.vals, :val => set.val) + s == :count && push!(d, :op => set.op) + new_kwargs = merge(kwargs, d) + return error_f(USUAL_CONSTRAINTS[s])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOICount{typeof(set.op), typeof(set.val), eltype(set.vals)}}(cidx) +end + +function Base.copy(set::MOICount) + return MOICount( + copy(set.op), copy(set.val), copy(set.vals), copy(set.dimension)) +end + +""" +Global constraint ensuring that the number of occurrences of `val` in `X` is equal to `count`. + +```julia +@constraint(model, X in Count(count, val, vals)) +``` +""" +struct Count{F <: Function, T1 <: Number, T2 <: Number} <: JuMP.AbstractVectorSet + op::F + val::T1 + vals::Vector{T2} + + function Count(op, val, vals) + return new{typeof(op), typeof(val), eltype(vals)}(op, val, vals) + end +end + +function Count(; + op::F, val::T1, vals::Vector{T2}) where { + F <: Function, T1 <: Number, T2 <: Number} + return Count(op, val, vals) +end + +function JuMP.moi_set(set::Count, dim::Int) + return MOICount(set.op, set.val, set.vals, dim) +end + +""" +Constraint ensuring that the number of occurrences of the values in `vals` in `x` is at least `val`. + +```julia +@constraint(model, X in AtLeast(val, vals)) +``` +""" +struct AtLeast{T1 <: Number, T2 <: Number} <: JuMP.AbstractVectorSet + val::T1 + vals::Vector{T2} + + function AtLeast(val, vals) + return new{typeof(val), eltype(vals)}(val, vals) + end +end + +function AtLeast(; + val::T1, vals::Vector{T2}) where {T1 <: Number, T2 <: Number} + return AtLeast(val, vals) +end + +function JuMP.moi_set(set::AtLeast, dim::Int) + return MOICount(≥, set.val, set.vals, dim) +end + +""" +Constraint ensuring that the number of occurrences of the values in `vals` in `x` is at most `val`. + +```julia +@constraint(model, X in AtMost(val, vals)) +``` +""" +struct AtMost{T1 <: Number, T2 <: Number} <: JuMP.AbstractVectorSet + val::T1 + vals::Vector{T2} + + function AtMost(val, vals) + return new{typeof(val), eltype(vals)}(val, vals) + end +end + +function AtMost(; + val::T1, vals::Vector{T2}) where {T1 <: Number, T2 <: Number} + return AtMost(val, vals) +end + +function JuMP.moi_set(set::AtMost, dim::Int) + return MOICount(≤, set.val, set.vals, dim) +end + +""" +Constraint ensuring that the number of occurrences of the values in `vals` in `x` is exactly `val`. + +```julia +@constraint(model, X in Exactly(val, vals)) +``` +""" +struct Exactly{T1 <: Number, T2 <: Number} <: JuMP.AbstractVectorSet + val::T1 + vals::Vector{T2} + + function Exactly(val, vals) + return new{typeof(val), eltype(vals)}(val, vals) + end +end + +function Exactly(; + val::T1, vals::Vector{T2}) where {T1 <: Number, T2 <: Number} + return Exactly(val, vals) +end + +function JuMP.moi_set(set::Exactly, dim::Int) + return MOICount(==, set.val, set.vals, dim) +end + +## SECTION - Test Items +@testitem "Count" tags=[:usual, :constraints, :count] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤4, Int) + @variable(model, 1≤X_at_least[1:4]≤4, Int) + @variable(model, 1≤X_at_most[1:4]≤4, Int) + @variable(model, 1≤X_exactly[1:4]≤4, Int) + + @constraint(model, X in Count(vals = [1, 2, 3, 4], op = ≥, val = 2)) + @constraint(model, X_at_least in AtLeast(vals = [1, 2, 3, 4], val = 2)) + @constraint(model, X_at_most in AtMost(vals = [1, 2], val = 1)) + @constraint(model, X_exactly in Exactly(vals = [1, 2], val = 2)) + + optimize!(model) + @info "Count" value.(X) value.(X_at_least) value.(X_at_most) value.(X_exactly) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/cumulative.jl b/src/constraints/cumulative.jl new file mode 100644 index 0000000..62425c7 --- /dev/null +++ b/src/constraints/cumulative.jl @@ -0,0 +1,94 @@ +""" + MOICumulative{F <: Function, T1 <: Number, T2 <: Number} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOICumulative{F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} <: + MOI.AbstractVectorSet + op::F + pair_vars::V + val::T2 + dimension::Int + + function MOICumulative(op, pair_vars, val, dim = 0) + return new{typeof(op), eltype(pair_vars), typeof(val), typeof(pair_vars)}( + op, pair_vars, val, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOICumulative{F, T1, T2, V}}) where { + F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOICumulative) + function e(x; kwargs...) + d = Dict(:op => set.op, :val => set.val) + !isempty(set.pair_vars) && push!(d, :pair_vars => set.pair_vars) + new_kwargs = merge(kwargs, d) + return error_f(USUAL_CONSTRAINTS[:cumulative])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, + MOICumulative{ + typeof(set.op), eltype(set.pair_vars), typeof(set.val), typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOICumulative) + return MOICumulative( + copy(set.op), copy(set.pair_vars), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that the cumulative sum of the heights of the tasks is less than or equal to `val`. + +```julia +@constraint(model, X in Cumulative(; pair_vars, op, val)) +``` +""" +struct Cumulative{F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} <: + JuMP.AbstractVectorSet + op::F + pair_vars::V + val::T2 + + function Cumulative(op, pair_vars, val) + return new{typeof(op), eltype(pair_vars), typeof(val), typeof(pair_vars)}( + op, pair_vars, val) + end +end + +function Cumulative(; op::F = ≤, pair_vars::V = Vector{Number}(), + val::T2) where {F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} + return Cumulative(op, pair_vars, val) +end + +function JuMP.moi_set(set::Cumulative, dim::Int) + return MOICumulative(set.op, set.pair_vars, set.val, dim) +end + +## SECTION - Test Items +@testitem "Cumulative" tags=[:usual, :constraints, :cumulative] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤5, Int) + @variable(model, 1≤Z[1:5]≤5, Int) + + @constraint(model, X in Cumulative(; val = 1)) + @constraint(model, + Y in Cumulative(; pair_vars = [3 2 5 4 2; 1 2 1 1 3], op = ≤, val = 5)) + @constraint(model, + Z in Cumulative(; pair_vars = [3 2 5 4 2; 1 2 1 1 3], op = <, val = 5)) + + optimize!(model) + @info "Cumulative" value.(X) value.(Y) value.(Z) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/element.jl b/src/constraints/element.jl new file mode 100644 index 0000000..ce2e177 --- /dev/null +++ b/src/constraints/element.jl @@ -0,0 +1,82 @@ +""" + MOIElement{I <: Integer, F <: Function, T <: Union{Nothing, Number}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIElement{I <: Integer, F <: Function, T <: Union{Nothing, Number}} <: + MOI.AbstractVectorSet + id::I + op::F + val::T + dimension::Int + + function MOIElement(id, op, val, dim = 0) + return new{typeof(id), typeof(op), typeof(val)}(id, op, val, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIElement{I, F, T}}) where { + I <: Integer, F <: Function, T <: Union{Nothing, Number}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIElement) + id = iszero(set.id) ? nothing : set.id + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:id => id, :op => set.op, :val => set.val)) + return error_f(USUAL_CONSTRAINTS[:element])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIElement{typeof(set.id), typeof(set.op), typeof(set.val)}}(cidx) +end + +function Base.copy(set::MOIElement) + return MOIElement(copy(set.id), copy(set.op), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that the value of `X` at index `id` is equal to `val`. + +```julia +@constraint(model, X in Element(; id = nothing, op = ==, val = 0)) +``` +""" +struct Element{I <: Integer, F <: Function, T <: Union{Nothing, Number}} <: + JuMP.AbstractVectorSet + id::I + op::F + val::T + + function Element(; id::I = 0, op::F = ==, + val::T = 0) where {I <: Integer, F <: Function, T <: Union{Nothing, Number}} + return new{typeof(id), typeof(op), typeof(val)}(id, op, val) + end +end + +function JuMP.moi_set(set::Element, dim_moi::Int) + return MOIElement(set.id, set.op, set.val, dim_moi) +end + +## SECTION - Test Items +@testitem "Element" tags=[:usual, :constraints, :element] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤5, Int) + @variable(model, 0≤Z[1:5]≤5, Int) + + @constraint(model, X in Element()) + @constraint(model, Y in Element(; id = 1, val = 1)) + @constraint(model, Z in Element(; id = 2, val = 2)) + + optimize!(model) + @info "Element" value.(X) value.(Y) value.(Z) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/extension.jl b/src/constraints/extension.jl new file mode 100644 index 0000000..60e03bd --- /dev/null +++ b/src/constraints/extension.jl @@ -0,0 +1,201 @@ +""" + MOIExtension{T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} <: MOI.AbstractVectorSet + + DOCSTRING +""" +struct MOIExtension{ + T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} <: + MOI.AbstractVectorSet + pair_vars::V + dimension::Int + + function MOIExtension(pair_vars, dim = 0) + ET = eltype(first(typeof(pair_vars) <: Tuple ? first(pair_vars) : pair_vars)) + return new{ET, typeof(pair_vars)}(pair_vars, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIExtension{T, V}}) where { + T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIExtension) + pair_vars = set.pair_vars + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:pair_vars => set.pair_vars)) + return error_f(USUAL_CONSTRAINTS[:extension])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + ET = eltype(first(typeof(pair_vars) <: Tuple ? first(pair_vars) : pair_vars)) + return CI{VOV, MOIExtension{ET, typeof(pair_vars)}}(cidx) +end + +function Base.copy(set::MOIExtension) + return MOIExtension(copy(set.pair_vars), copy(set.dimension)) +end + +""" +Global constraint enforcing that the tuple `x` matches a configuration within the supports set `pair_vars[1]` or does not match any configuration within the conflicts set `pair_vars[2]`. It embodies the logic: `x ∈ pair_vars[1] || x ∉ pair_vars[2]`, providing a comprehensive way to define valid (supported) and invalid (conflicted) tuples for constraint satisfaction problems. This constraint is versatile, allowing for the explicit delineation of both acceptable and unacceptable configurations. +""" +struct Extension{T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} <: + JuMP.AbstractVectorSet + pair_vars::V + + function Extension(pair_vars) + ET = eltype(first(typeof(pair_vars) <: Tuple ? first(pair_vars) : pair_vars)) + return new{ET, typeof(pair_vars)}(pair_vars) + end +end + +function Extension(; + pair_vars::V) where { + T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} + return Extension(pair_vars) +end + +function JuMP.moi_set(set::Extension, dim::Int) + return MOIExtension(set.pair_vars, dim) +end + +""" + MOISupports{T <: Number, V <: Vector{Vector{T}}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOISupports{T <: Number, V <: Vector{Vector{T}}} <: MOI.AbstractVectorSet + pair_vars::V + dimension::Int + + function MOISupports(pair_vars, dim = 0) + ET = eltype(first(pair_vars)) + return new{ET, typeof(pair_vars)}(pair_vars, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOISupports{T, V}}) where { + T <: Number, V <: Vector{Vector{T}}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOISupports) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:pair_vars => set.pair_vars)) + return error_f(USUAL_CONSTRAINTS[:supports])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + ET = eltype(first(set.pair_vars)) + return CI{VOV, MOISupports{ET, typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOISupports) + return MOISupports(copy(set.pair_vars), copy(set.dimension)) +end + +""" +Global constraint ensuring that the tuple `x` matches a configuration listed within the support set `pair_vars`. This constraint is derived from the extension model, specifying that `x` must be one of the explicitly defined supported configurations: `x ∈ pair_vars`. It is utilized to directly declare the tuples that are valid and should be included in the solution space. + +```julia +@constraint(model, X in Supports(; pair_vars)) +``` +""" +struct Supports{T <: Number, V <: Vector{Vector{T}}} <: JuMP.AbstractVectorSet + pair_vars::V + + function Supports(; pair_vars) + ET = eltype(first(pair_vars)) + return new{ET, typeof(pair_vars)}(pair_vars) + end +end + +function JuMP.moi_set(set::Supports, dim::Int) + return MOISupports(set.pair_vars, dim) +end + +""" + MOIConflicts{T <: Number, V <: Vector{Vector{T}}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIConflicts{T <: Number, V <: Vector{Vector{T}}} <: + MOI.AbstractVectorSet + pair_vars::V + dimension::Int + + function MOIConflicts(pair_vars, dim = 0) + ET = eltype(first(pair_vars)) + return new{ET, typeof(pair_vars)}(pair_vars, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIConflicts{T, V}}) where { + T <: Number, V <: Vector{Vector{T}}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIConflicts) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:pair_vars => set.pair_vars)) + return error_f(USUAL_CONSTRAINTS[:conflicts])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + ET = eltype(first(set.pair_vars)) + return CI{VOV, MOIConflicts{ET, typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOIConflicts) + return MOIConflicts(copy(set.pair_vars), copy(set.dimension)) +end + +""" +Global constraint ensuring that the tuple `x` does not match any configuration listed within the conflict set `pair_vars`. This constraint, originating from the extension model, stipulates that `x` must avoid all configurations defined as conflicts: `x ∉ pair_vars`. It is useful for specifying tuples that are explicitly forbidden and should be excluded from the solution space. + +```julia +@constraint(model, X in Conflicts(; pair_vars)) +``` +""" +struct Conflicts{T <: Number, V <: Vector{Vector{T}}} <: JuMP.AbstractVectorSet + pair_vars::V + + function Conflicts(; pair_vars) + ET = eltype(first(pair_vars)) + return new{ET, typeof(pair_vars)}(pair_vars) + end +end + +function JuMP.moi_set(set::Conflicts, dim::Int) + return MOIConflicts(set.pair_vars, dim) +end + +## SECTION - Test Items +@testitem "Extension" tags=[:usual, :constraints, :extension] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤5, Int) + @variable(model, 1≤X_Supports[1:5]≤5, Int) + @variable(model, 1≤X_Conflicts[1:5]≤5, Int) + + @constraint(model, X in Extension(; pair_vars = [[1, 2, 3, 4, 5]])) + @constraint(model, Y in Extension(; pair_vars = [[1, 2, 1, 4, 5], [1, 2, 3, 5, 5]])) + @constraint(model, X_Supports in Supports(; pair_vars = [[1, 2, 3, 4, 5]])) + @constraint(model, + X_Conflicts in Conflicts(; pair_vars = [[1, 2, 1, 4, 5], [1, 2, 3, 5, 5]])) + + optimize!(model) + @info "Extension" value.(X) value.(Y) value.(X_Supports) value.(X_Conflicts) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/instantiation.jl b/src/constraints/instantiation.jl new file mode 100644 index 0000000..ec319f3 --- /dev/null +++ b/src/constraints/instantiation.jl @@ -0,0 +1,69 @@ +""" + MOIInstantiation{T <: Number, V <: Vector{T}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIInstantiation{T <: Number, V <: Vector{T}} <: + MOI.AbstractVectorSet + pair_vars::V + dimension::Int + + function MOIInstantiation(pair_vars, dim = 0) + return new{eltype(pair_vars), typeof(pair_vars)}(pair_vars, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIInstantiation{T, V}}) where { + T <: Number, V <: Vector{T}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIInstantiation) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:pair_vars => set.pair_vars)) + return error_f(USUAL_CONSTRAINTS[:instantiation])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIInstantiation{eltype(set.pair_vars), typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOIInstantiation) + return MOIInstantiation(copy(set.pair_vars), copy(set.dimension)) +end + +""" +The instantiation constraint is a global constraint used in constraint programming that ensures that a list of variables takes on a specific set of values in a specific order. +""" +struct Instantiation{T <: Number, V <: Vector{T}} <: JuMP.AbstractVectorSet + pair_vars::V + + function Instantiation(; pair_vars) + return new{eltype(pair_vars), typeof(pair_vars)}(pair_vars) + end +end + +function JuMP.moi_set(set::Instantiation, dim::Int) + return MOIInstantiation(set.pair_vars, dim) +end + +## SECTION - Test Items +@testitem "Instantiation" tags=[:usual, :constraints, :instantiation] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤6, Int) + @variable(model, 1≤Y[1:5]≤6, Int) + + @constraint(model, X in Instantiation(; pair_vars = [1, 2, 3, 4, 5])) + @constraint(model, Y in Instantiation(; pair_vars = [1, 2, 3, 4, 6])) + + optimize!(model) + @info "Instantiation" value.(X) value.(Y) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/intention.jl b/src/constraints/intention.jl new file mode 100644 index 0000000..65a5c44 --- /dev/null +++ b/src/constraints/intention.jl @@ -0,0 +1,59 @@ +# Intention constraints emcompass any generic constraint. DistDifferent is implemented as an example of an intensional constraint. + +""" + MOIDistDifferent <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIDistDifferent <: MOI.AbstractVectorSet + dimension::Int + + function MOIDistDifferent(dim = 4) + return new(dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIDistDifferent}) + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, ::MOIDistDifferent) + function e(x; kwargs...) + return error_f(USUAL_CONSTRAINTS[:dist_different])(x) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIDistDifferent}(cidx) +end + +function Base.copy(set::MOIDistDifferent) + return MOIDistDifferent(copy(set.dimension)) +end + +""" +A constraint ensuring that the distances between marks on the ruler are unique. Specifically, it checks that the distance between `x[1]` and `x[2]`, and the distance between `x[3]` and `x[4]`, are different. This constraint is fundamental in ensuring the validity of a Golomb ruler, where no two pairs of marks should have the same distance between them. +""" +struct DistDifferent <: JuMP.AbstractVectorSet end + +function JuMP.moi_set(::DistDifferent, dim::Int) + return MOIDistDifferent(dim) +end + +## SECTION - Test Items +@testitem "Dist different (intension)" tags=[:usual, :constraints, :intension] begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤6, Int) + + @constraint(model, X in DistDifferent()) + + optimize!(model) + @info "Dist different (intension)" value.(X) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/maximum.jl b/src/constraints/maximum.jl new file mode 100644 index 0000000..1219057 --- /dev/null +++ b/src/constraints/maximum.jl @@ -0,0 +1,74 @@ +""" + MOIMaximum {F <: Function, T <: Number} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIMaximum{F <: Function, T <: Number} <: MOI.AbstractVectorSet + op::F + val::T + dimension::Int + + function MOIMaximum(op, val, dim = 0) + return new{typeof(op), typeof(val)}(op, val, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIMaximum{F, T}}) where { + F <: Function, T <: Number} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIMaximum) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:op => set.op, :val => set.val)) + return error_f(USUAL_CONSTRAINTS[:maximum])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIMaximum{typeof(set.op), typeof(set.val)}}(cidx) +end + +function Base.copy(set::MOIMaximum) + return MOIMaximum(copy(set.op), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that the maximum value in the tuple `x` satisfies the condition `op(x) val`. This constraint is useful for specifying that the maximum value in the tuple must satisfy a certain condition. + +```julia +@constraint(model, X in Maximum(; op = ==, val)) +``` +""" +struct Maximum{F <: Function, T <: Number} <: JuMP.AbstractVectorSet + op::F + val::T + + function Maximum(op, val) + return new{typeof(op), typeof(val)}(op, val) + end +end + +Maximum(; op::F = ==, val::T) where {F <: Function, T <: Number} = Maximum(op, val) + +function JuMP.moi_set(set::Maximum, dim::Int) + return MOIMaximum(set.op, set.val, dim) +end + +## SECTION - Test Items +@testitem "Maximum" tags=[:usual, :constraints, :maximum] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + + @constraint(model, X in Maximum(; op = ==, val = 5)) + + optimize!(model) + @info "Maximum" value.(X) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/mdd.jl b/src/constraints/mdd.jl new file mode 100644 index 0000000..dce5826 --- /dev/null +++ b/src/constraints/mdd.jl @@ -0,0 +1,94 @@ +""" + MOIMultivaluedDecisionDiagram{L <: ConstraintCommons.AbstractMultivaluedDecisionDiagram} <: AbstractVectorSet + +DOCSTRING +""" +struct MOIMultivaluedDecisionDiagram{L <: + ConstraintCommons.AbstractMultivaluedDecisionDiagram} <: + MOI.AbstractVectorSet + language::L + dimension::Int + + function MOIMultivaluedDecisionDiagram(language, dim = 0) + return new{typeof(language)}(language, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIMultivaluedDecisionDiagram{L}}) where {L <: + ConstraintCommons.AbstractMultivaluedDecisionDiagram} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIMultivaluedDecisionDiagram) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:language => set.language)) + return error_f(USUAL_CONSTRAINTS[:mdd])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIMultivaluedDecisionDiagram{typeof(set.language)}}(cidx) +end + +function Base.copy(set::MOIMultivaluedDecisionDiagram) + return MOIMultivaluedDecisionDiagram(deepcopy(set.language), copy(set.dimension)) +end + +""" +Multi-valued Decision Diagram (MDD) constraint. + +The MDD constraint is a constraint that can be used to model a wide range of problems. It is a directed graph where each node is labeled with a value and each edge is labeled with a value. The constraint is satisfied if there is a path from the first node to the last node such that the sequence of edge labels is a valid sequence of the value labels. + +```julia +@constraint(model, X in MDDConstraint(; language)) +``` +""" +struct MDDConstraint{L <: ConstraintCommons.AbstractMultivaluedDecisionDiagram} <: + JuMP.AbstractVectorSet + language::L + + function MDDConstraint(; language) + return new{typeof(language)}(language) + end +end + +function JuMP.moi_set(set::MDDConstraint, dim::Int) + return MOIMultivaluedDecisionDiagram(set.language, dim) +end + +## SECTION - Test Items for MDD +@testitem "MDD" tags=[:usual, :constraints, :mdd] default_imports=false begin + using CBLS + using JuMP + + import ConstraintCommons: MDD + + model = Model(CBLS.Optimizer) + + states = [ + Dict( # level x1 + (:r, 0) => :n1, + (:r, 1) => :n2, + (:r, 2) => :n3 + ), + Dict( # level x2 + (:n1, 2) => :n4, + (:n2, 2) => :n4, + (:n3, 0) => :n5 + ), + Dict( # level x3 + (:n4, 0) => :t, + (:n5, 0) => :t + ) + ] + + @variable(model, 0≤X[1:3]≤2, Int) + + @constraint(model, X in MDDConstraint(; language = MDD(states))) + + optimize!(model) + @info "MDD" value.(X) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/minimum.jl b/src/constraints/minimum.jl new file mode 100644 index 0000000..7c0ddcf --- /dev/null +++ b/src/constraints/minimum.jl @@ -0,0 +1,74 @@ +""" + MOIMinimum {F <: Function, T <: Number} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIMinimum{F <: Function, T <: Number} <: MOI.AbstractVectorSet + op::F + val::T + dimension::Int + + function MOIMinimum(op, val, dim = 0) + return new{typeof(op), typeof(val)}(op, val, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIMinimum{F, T}}) where { + F <: Function, T <: Number} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIMinimum) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:op => set.op, :val => set.val)) + return error_f(USUAL_CONSTRAINTS[:minimum])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIMinimum{typeof(set.op), typeof(set.val)}}(cidx) +end + +function Base.copy(set::MOIMinimum) + return MOIMinimum(copy(set.op), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that the minimum value in the tuple `x` satisfies the condition `op(x) val`. This constraint is useful for specifying that the minimum value in the tuple must satisfy a certain condition. + +```julia +@constraint(model, X in Minimum(; op = ==, val)) +``` +""" +struct Minimum{F <: Function, T <: Number} <: JuMP.AbstractVectorSet + op::F + val::T + + function Minimum(op, val) + return new{typeof(op), typeof(val)}(op, val) + end +end + +Minimum(; op::F = ==, val::T) where {F <: Function, T <: Number} = Minimum(op, val) + +function JuMP.moi_set(set::Minimum, dim::Int) + return MOIMinimum(set.op, set.val, dim) +end + +## SECTION - Test Items +@testitem "Minimum" tags=[:usual, :constraints, :minimum] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + + @constraint(model, X in Minimum(; op = ==, val = 3)) + + optimize!(model) + @info "Minimum" value.(X) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/n_values.jl b/src/constraints/n_values.jl new file mode 100644 index 0000000..f4f542a --- /dev/null +++ b/src/constraints/n_values.jl @@ -0,0 +1,88 @@ +""" + MOINValues{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOINValues{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} <: + MOI.AbstractVectorSet + op::F + val::T1 + vals::V + dimension::Int + + function MOINValues(op, val, vals, dim = 0) + return new{typeof(op), typeof(val), eltype(vals), typeof(vals)}(op, val, vals, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOINValues{F, T1, T2, V}}) where { + F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOINValues) + vals = isempty(set.vals) ? nothing : set.vals + function e(x; kwargs...) + d = Dict(:op => set.op, :val => set.val) + isnothing(vals) && (d[:vals] = vals) + new_kwargs = merge(kwargs, d) + return error_f(USUAL_CONSTRAINTS[:nvalues])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, + MOINValues{typeof(set.op), typeof(set.val), eltype(set.vals), typeof(set.vals)}}(cidx) +end + +function Base.copy(set::MOINValues) + return MOINValues(copy(set.op), copy(set.val), copy(set.vals), copy(set.dimension)) +end + +""" +Global constraint ensuring that the number of distinct values in `X` satisfies the given condition. +""" +struct NValues{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} <: + JuMP.AbstractVectorSet + op::F + val::T1 + vals::V + + function NValues(op, val, vals) + return new{typeof(op), typeof(val), eltype(vals), typeof(vals)}(op, val, vals) + end +end + +function NValues(; op::F = ==, + val::T1, + vals::V = Vector{Number}()) where { + F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} + return NValues(op, val, vals) +end + +function JuMP.moi_set(set::NValues, dim::Int) + vals = isnothing(set.vals) ? Vector{Number}() : set.vals + return MOINValues(set.op, set.val, vals, dim) +end + +## SECTION - Test Items +@testitem "NValues" tags=[:usual, :constraints, :nvalues] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤5, Int) + @variable(model, 1≤Z[1:5]≤5, Int) + + @constraint(model, X in NValues(; op = ==, val = 5)) + @constraint(model, Y in NValues(; op = ==, val = 2)) + @constraint(model, Z in NValues(; op = <=, val = 5, vals = [1, 2])) + + optimize!(model) + @info "NValues" value.(X) value.(Y) value.(Z) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/no_overlap.jl b/src/constraints/no_overlap.jl new file mode 100644 index 0000000..9d1850c --- /dev/null +++ b/src/constraints/no_overlap.jl @@ -0,0 +1,90 @@ +""" + MOINoOverlap{I <: Integer, T <: Number, V <: Vector{T}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOINoOverlap{I <: Integer, T <: Number, V <: Vector{T}} <: + MOI.AbstractVectorSet + bool::Bool + dim::I + pair_vars::V + dimension::Int + + function MOINoOverlap(bool, dim, pair_vars, moi_dim = 0) + return new{typeof(dim), eltype(pair_vars), typeof(pair_vars)}( + bool, dim, pair_vars, moi_dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOINoOverlap{I, T, V}}) where { + I <: Integer, T <: Number, V <: Vector{T}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOINoOverlap) + function e(x; kwargs...) + d = if isempty(set.pair_vars) + Dict(:dim => set.dim, :bool => set.bool) + else + Dict{Symbol, Any}( + :dim => set.dim, :bool => set.bool, :pair_vars => set.pair_vars) + end + new_kwargs = merge(kwargs, d) + return error_f(USUAL_CONSTRAINTS[:no_overlap])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{ + VOV, MOINoOverlap{typeof(set.dim), eltype(set.pair_vars), typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOINoOverlap) + return MOINoOverlap( + copy(set.bool), copy(set.dim), copy(set.pair_vars), copy(set.dimension)) +end + +""" +Global constraint ensuring that the tuple `x` does not overlap with any configuration listed within the pair set `pair_vars`. This constraint, originating from the extension model, stipulates that `x` must avoid all configurations defined as pairs: `x ∩ pair_vars = ∅`. It is useful for specifying tuples that are explicitly forbidden and should be excluded from the solution space. + +```julia +@constraint(model, X in NoOverlap(; bool = true, dim = 1, pair_vars = nothing)) +``` +""" +struct NoOverlap{I <: Integer, T <: Number, V <: Vector{T}} <: + JuMP.AbstractVectorSet + bool::Bool + dim::I + pair_vars::V + + function NoOverlap(; bool = true, dim = 1, pair_vars = Vector{Number}()) + return new{typeof(dim), eltype(pair_vars), typeof(pair_vars)}(bool, dim, pair_vars) + end +end + +function JuMP.moi_set(set::NoOverlap, dim::Int) + return MOINoOverlap(set.bool, set.dim, set.pair_vars, dim) +end + +## SECTION - Test Items +@testitem "noOverlap" tags=[:usual, :constraints, :no_overlap] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤6, Int) + @variable(model, 1≤Z[1:12]≤12, Int) + + @constraint(model, X in NoOverlap()) + @constraint(model, Y in NoOverlap(; pair_vars = [1, 1, 1, 1, 1])) + @constraint(model, + Z in NoOverlap(; pair_vars = [2, 4, 1, 4, 2, 3, 5, 1, 2, 3, 3, 2], dim = 3)) + + optimize!(model) + @info "NoOverlap" value.(X) value.(Y) value.(Z) + termination_status(model) + @info termination_status(model) +end diff --git a/src/constraints/ordered.jl b/src/constraints/ordered.jl new file mode 100644 index 0000000..43dddea --- /dev/null +++ b/src/constraints/ordered.jl @@ -0,0 +1,83 @@ +""" + MOIOrdered{F <: Function, T <: Number, V <: Vector{T}} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOIOrdered{F <: Function, T <: Number, V <: Vector{T}} <: + MOI.AbstractVectorSet + op::F + pair_vars::V + dimension::Int + + function MOIOrdered(op, pair_vars, moi_dim = 0) + return new{typeof(op), eltype(pair_vars), typeof(pair_vars)}( + op, pair_vars, moi_dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIOrdered{F, T, V}}) where { + F <: Function, T <: Number, V <: Vector{T}} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIOrdered) + function e(x; kwargs...) + d = if isempty(set.pair_vars) + Dict(:op => set.op) + else + Dict(:op => set.op, :pair_vars => set.pair_vars) + end + new_kwargs = merge(kwargs, d) + return error_f(USUAL_CONSTRAINTS[:ordered])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{ + VOV, MOIOrdered{typeof(set.op), eltype(set.pair_vars), typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOIOrdered) + return MOIOrdered(copy(set.op), copy(set.pair_vars), copy(set.dimension)) +end + +""" +Global constraint ensuring that the variables are ordered according to `op`. +""" +struct Ordered{F <: Function, T <: Number, V <: Vector{T}} <: + JuMP.AbstractVectorSet + op::F + pair_vars::V + + function Ordered(op, pair_vars) + return new{typeof(op), eltype(pair_vars), typeof(pair_vars)}(op, pair_vars) + end +end + +function Ordered(; op = ≤, pair_vars = Vector{Number}()) + return Ordered(op, pair_vars) +end + +function JuMP.moi_set(set::Ordered, dim::Int) + return MOIOrdered(set.op, set.pair_vars, dim) +end + +## SECTION - Test Items +@testitem "Ordered" tags=[:usual, :constraints, :ordered] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤5, Int) + + @constraint(model, X in Ordered()) + @constraint(model, Y in Ordered(; op = <)) + + optimize!(model) + @info "Ordered" value.(X) value.(Y) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/regular.jl b/src/constraints/regular.jl new file mode 100644 index 0000000..8ee907f --- /dev/null +++ b/src/constraints/regular.jl @@ -0,0 +1,83 @@ +""" + MOIRegular{L <: ConstraintCommons.AbstractAutomaton} <: AbstractVectorSet + +DOCSTRING +""" +struct MOIRegular{L <: ConstraintCommons.AbstractAutomaton} <: MOI.AbstractVectorSet + language::L + dimension::Int + + function MOIRegular(language, dim = 0) + return new{typeof(language)}(language, dim) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOIRegular{L}}) where {L <: ConstraintCommons.AbstractAutomaton} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIRegular) + function e(x; kwargs...) + new_kwargs = merge(kwargs, Dict(:language => set.language)) + return error_f(USUAL_CONSTRAINTS[:regular])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, MOIRegular{typeof(set.language)}}(cidx) +end + +function Base.copy(set::MOIRegular) + return MOIRegular(deepcopy(set.language), copy(set.dimension)) +end + +""" +Ensures that a sequence `x` (interpreted as a word) is accepted by the regular language represented by a given automaton. This constraint verifies the compliance of `x` with the language rules encoded within the `automaton` parameter, which must be an instance of `<:AbstractAutomaton`. + +```julia +@constraint(model, X in RegularConstraint(; language)) +``` +""" +struct Regular{L <: ConstraintCommons.AbstractAutomaton} <: JuMP.AbstractVectorSet + language::L + + function Regular(; language) + return new{typeof(language)}(language) + end +end + +function JuMP.moi_set(set::Regular, dim::Int) + return MOIRegular(set.language, dim) +end + +## SECTION - Test Items for Regular +@testitem "Regular" tags=[:usual, :constraints, :regular] default_imports=false begin + using CBLS + using JuMP + + import ConstraintCommons: Automaton + + states = Dict( + (:a, 0) => :a, + (:a, 1) => :b, + (:b, 1) => :c, + (:c, 0) => :d, + (:d, 0) => :d, + (:d, 1) => :e, + (:e, 0) => :e + ) + start = :a + finish = :e + a = Automaton(states, start, finish) + + model = Model(CBLS.Optimizer) + + @variable(model, 0≤X[1:9]≤1, Int) + @constraint(model, X in Regular(; language = a)) + + optimize!(model) + @info "Regular" value.(X) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/constraints/sum.jl b/src/constraints/sum.jl new file mode 100644 index 0000000..7bbcbed --- /dev/null +++ b/src/constraints/sum.jl @@ -0,0 +1,86 @@ +""" + MOISum{F <: Function, T1 <: Number, T2 <: Number, V <: Number} <: MOI.AbstractVectorSet + +DOCSTRING +""" +struct MOISum{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T1}} <: + MOI.AbstractVectorSet + op::F + pair_vars::V + val::T2 + dimension::Int + + function MOISum(op, pair_vars, val, dimension) + return new{typeof(op), eltype(pair_vars), typeof(val), typeof(pair_vars)}( + op, pair_vars, val, dimension) + end +end + +function MOI.supports_constraint(::Optimizer, + ::Type{VOV}, + ::Type{MOISum{F, T1, T2, V}}) where {F, T1, T2, V} + return true +end + +function MOI.add_constraint( + optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOISum) + function e(x; kwargs...) + d = if isempty(set.pair_vars) + Dict(:op => set.op, :val => set.val) + else + Dict(:op => set.op, :pair_vars => set.pair_vars, :val => set.val) + end + new_kwargs = merge(kwargs, d) + return error_f(USUAL_CONSTRAINTS[:sum])(x; new_kwargs...) + end + cidx = constraint!(optimizer, e, map(x -> x.value, vars.variables)) + return CI{VOV, + MOISum{ + typeof(set.op), eltype(set.pair_vars), typeof(set.val), typeof(set.pair_vars)}}(cidx) +end + +function Base.copy(set::MOISum) + return MOISum(copy(set.op), copy(set.pair_vars), copy(set.val), copy(set.dimension)) +end + +""" +Global constraint ensuring that the sum of the variables in `x` satisfies a given condition. +""" +struct Sum{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T1}} <: + JuMP.AbstractVectorSet + op::F + pair_vars::V + val::T2 + + function Sum(op, pair_vars, val) + return new{typeof(op), eltype(pair_vars), typeof(val), typeof(pair_vars)}( + op, pair_vars, val) + end +end + +function Sum(; op = ==, pair_vars = Vector{Number}(), val) + return Sum(op, pair_vars, val) +end + +function JuMP.moi_set(set::Sum, dim::Int) + return MOISum(set.op, set.pair_vars, set.val, dim) +end + +## SECTION - Test Items for Sum +@testitem "Sum" tags=[:usual, :constraints, :sum] default_imports=false begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:5]≤5, Int) + @variable(model, 1≤Y[1:5]≤5, Int) + + @constraint(model, X in Sum(; op = ==, val = 15)) + @constraint(model, Y in Sum(; op = <=, val = 10)) + + optimize!(model) + @info "Sum" value.(X) value.(Y) + termination_status(model) + @info solution_summary(model) +end diff --git a/src/objectives.jl b/src/objectives.jl index 5bb4cc3..024aa9b 100644 --- a/src/objectives.jl +++ b/src/objectives.jl @@ -45,7 +45,10 @@ ScalarFunction(f, x::VI) = ScalarFunction(f, VOV([x])) Base.copy(func::ScalarFunction) = ScalarFunction(func.f, func.X) # supports -MOI.supports(::Optimizer, ::OF{ScalarFunction{F, V}}) where {F <: Function, V <: Union{Nothing, VI,VOV}} = true +function MOI.supports(::Optimizer, + ::OF{ScalarFunction{F, V}}) where {F <: Function, V <: Union{Nothing, VI, VOV}} + true +end # set function MOI.set(optimizer::Optimizer, ::OF, func::ScalarFunction{F, Nothing} @@ -55,17 +58,17 @@ end function MOI.set(optimizer::Optimizer, ::OF, func::ScalarFunction{F, VOV} ) where {F <: Function} # VOV, mainly for JuMP - objective_func = _ -> func.f(map(y -> get_value(optimizer,y.value), func.X.variables)) - return objective!(optimizer, objective_func) - end + objective_func = _ -> func.f(map(y -> get_value(optimizer, y.value), func.X.variables)) + return objective!(optimizer, objective_func) +end # @autodoc - function MOIU.map_indices(index_map::Function, sf::ScalarFunction{F,VOV} +function MOIU.map_indices(index_map::Function, sf::ScalarFunction{F, VOV} ) where {F <: Function} return ScalarFunction(sf.f, MOIU.map_indices(index_map, sf.X)) - end +end # @autodoc - function MOIU.map_indices(::Function, sf::ScalarFunction{F,Nothing}) where {F <: Function} - return ScalarFunction(sf.f, nothing) +function MOIU.map_indices(::Function, sf::ScalarFunction{F, Nothing}) where {F <: Function} + return ScalarFunction(sf.f, nothing) end diff --git a/src/results.jl b/src/results.jl index 71571c8..a9a24c5 100644 --- a/src/results.jl +++ b/src/results.jl @@ -32,9 +32,8 @@ function MOI.get(optimizer::Optimizer, ::MOI.VariablePrimal, vi::MOI.VariableInd end end - MOI.get(optimizer::Optimizer, ::MOI.SolveTimeSec) = time_info(optimizer)[:total_run] function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString) return has_solution(optimizer) ? "Satisfying solution" : "No solutions" -end \ No newline at end of file +end diff --git a/src/variables.jl b/src/variables.jl index 6fa6298..5261d0b 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -29,7 +29,10 @@ function MOI.supports_constraint(::Optimizer, ::Type{VI}, ::Type{MOI.GreaterThan return true end -MOI.supports_constraint(::Optimizer, ::Type{VI}, ::Type{DiscreteSet{T}}) where {T <: Number} = true +function MOI.supports_constraint( + ::Optimizer, ::Type{VI}, ::Type{DiscreteSet{T}}) where {T <: Number} + true +end """ MOI.add_constraint(optimizer::Optimizer, v::VI, set::DiscreteSet{T}) where T <: Number @@ -45,55 +48,36 @@ function MOI.add_constraint(optimizer::Optimizer, v::VI, set::DiscreteSet{T} ) where {T <: Number} vidx = v.value _set_domain!(optimizer, vidx, set.values) - return CI{VI,DiscreteSet{T}}(vidx) + return CI{VI, DiscreteSet{T}}(vidx) end -# function MOI.add_constraint(optimizer::Optimizer, v::VI, lt::MOI.LessThan{T} -# ) where {T <: AbstractFloat} -# vidx = v.value -# d = make_domain(typemin(Int), lt.upper, Val(:range)) -# update_domain!(optimizer, vidx, d) -# return CI{VI,MOI.LessThan{T}}(vidx) -# end - -# function MOI.add_constraint(optimizer::Optimizer, v::VI, gt::MOI.GreaterThan{T} -# ) where {T <: AbstractFloat} -# vidx = v.value -# d = make_domain(gt.lower, typemax(Int), Val(:range)) -# update_domain!(optimizer, vidx, d) -# return CI{VI,MOI.GreaterThan{T}}(vidx) -# end - -# make_domain(a, b, ::Val{:range}) = domain(Int(a):Int(b)) -# make_domain(a, b, ::Val{:inter}) = domain((a, true), (b, true)) - function MOI.add_constraint(optimizer::Optimizer, v::VI, lt::MOI.LessThan{T} - ) where {T <: AbstractFloat} - vidx = v.value - push!(optimizer.compare_vars, vidx) - if vidx ∈ optimizer.int_vars - d = domain(Int(typemin(Int)), Int(lt.upper)) - else - a = Float64(-floatmax(Float32)) - d = domain(Interval{Open, Closed}(a, lt.upper)) - end - update_domain!(optimizer, vidx, d) - return CI{VI,MOI.LessThan{T}}(vidx) +) where {T <: AbstractFloat} + vidx = v.value + push!(optimizer.compare_vars, vidx) + if vidx ∈ optimizer.int_vars + d = domain(Int(typemin(Int)), Int(lt.upper)) + else + a = Float64(-floatmax(Float32)) + d = domain(Interval{Open, Closed}(a, lt.upper)) end + update_domain!(optimizer, vidx, d) + return CI{VI, MOI.LessThan{T}}(vidx) +end - function MOI.add_constraint(optimizer::Optimizer, v::VI, gt::MOI.GreaterThan{T} - ) where {T <: AbstractFloat} - vidx = v.value - push!(optimizer.compare_vars, vidx) - if vidx ∈ optimizer.int_vars - d = domain(Int(gt.lower):Int(typemax(Int))) - else - b = Float64(floatmax(Float32)) - d = domain(Interval{Closed, Open}(gt.lower, b)) - end - update_domain!(optimizer, vidx, d) - return CI{VI,MOI.GreaterThan{T}}(vidx) +function MOI.add_constraint(optimizer::Optimizer, v::VI, gt::MOI.GreaterThan{T} +) where {T <: AbstractFloat} + vidx = v.value + push!(optimizer.compare_vars, vidx) + if vidx ∈ optimizer.int_vars + d = domain(Int(gt.lower):Int(typemax(Int))) + else + b = Float64(floatmax(Float32)) + d = domain(Interval{Closed, Open}(gt.lower, b)) end + update_domain!(optimizer, vidx, d) + return CI{VI, MOI.GreaterThan{T}}(vidx) +end function MOI.add_constraint(optimizer::Optimizer, v::VI, i::MOI.Interval{T} ) where {T <: Real} @@ -101,14 +85,14 @@ function MOI.add_constraint(optimizer::Optimizer, v::VI, i::MOI.Interval{T} is_int = MOI.is_valid(optimizer, CI{VI, MOI.Integer}(vidx)) d = make_domain(i.lower, i.upper, Val(is_int ? :range : :inter)) _set_domain!(optimizer, vidx, d) - return CI{VI,MOI.Interval{T}}(vidx) + return CI{VI, MOI.Interval{T}}(vidx) end function MOI.add_constraint(optimizer::Optimizer, v::VI, et::MOI.EqualTo{T} ) where {T <: Number} vidx = v.value _set_domain!(optimizer, vidx, et.value) - return CI{VI,MOI.EqualTo{T}}(vidx) + return CI{VI, MOI.EqualTo{T}}(vidx) end """ @@ -124,5 +108,30 @@ function MOI.add_constraint(optimizer::Optimizer, v::VI, ::MOI.Integer) x = get_variable(optimizer, vidx) _set_domain!(optimizer, vidx, convert(RangeDomain, x.domain)) end - return MOI.ConstraintIndex{VI,MOI.Integer}(vidx) + return MOI.ConstraintIndex{VI, MOI.Integer}(vidx) +end + +# MOI.supports_constraint(::Optimizer, ::Type{VI}, ::Type{<:MOI.ZeroOne}) = true + +# function MOI.add_constraint(optimizer::Optimizer, v::VI, ::MOI.ZeroOne) +# vidx = v.value +# push!(optimizer.int_vars, vidx) +# if vidx ∈ optimizer.compare_vars +# d = domain(0:1) +# _set_domain!(optimizer, vidx, d) +# end +# return MOI.ConstraintIndex{VI, MOI.ZeroOne}(vidx) +# end + +## SECTION - Test Items +@testitem "Variable Index" begin + using CBLS + using JuMP + + model = Model(CBLS.Optimizer) + + @variable(model, 1≤X[1:4]≤4, Int) + # @variable(model, Y[1:4], Bin) + + optimize!(model) end diff --git a/test/JuMP.jl b/test/JuMP.jl index 8a16965..e549e08 100644 --- a/test/JuMP.jl +++ b/test/JuMP.jl @@ -6,7 +6,7 @@ using JuMP err = _ -> 1.0 concept = _ -> true - @variable(m, 1 ≤ X[1:10] ≤ 4, Int) + @variable(m, 1≤X[1:10]≤4, Int) @constraint(m, X in Error(err)) @constraint(m, X in Predicate(concept)) @@ -30,18 +30,17 @@ end set_time_limit_sec(model, 5.0) @test time_limit_sec(model) == 5.0 - @variable(model, 0 ≤ x ≤ 20, Int) + @variable(model, 0≤x≤20, Int) @variable(model, y in DiscreteSet(0:20)) - @constraint(model, [x,y] in Predicate(v -> 6v[1] + 8v[2] >= 100 )) - @constraint(model, [x,y] in Predicate(v -> 7v[1] + 12v[2] >= 120 )) + @constraint(model, [x, y] in Predicate(v -> 6v[1] + 8v[2] >= 100)) + @constraint(model, [x, y] in Predicate(v -> 7v[1] + 12v[2] >= 120)) objFunc = v -> 12v[1] + 20v[2] @objective(model, Min, ScalarFunction(objFunc)) optimize!(model) - @info "JuMP: basic opt" value(x) value(y) (12*value(x)+20*value(y)) solve_time(model) termination_status(model) + @info "JuMP: basic opt" value(x) value(y) (12 * value(x)+20 * value(y)) solve_time(model) termination_status(model) @info solution_summary(model) - -end \ No newline at end of file +end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 0106487..758b163 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -25,7 +25,7 @@ end const BRIDGED = MOI.instantiate( OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64 ) -const CONFIG = MOIT.Config(atol=1e-6, rtol=1e-6) +const CONFIG = MOIT.Config(atol = 1e-6, rtol = 1e-6) # @testset "Unit" begin # # Test all the functions included in dictionary `MOI.Test.unittests`, @@ -68,10 +68,10 @@ const CONFIG = MOIT.Config(atol=1e-6, rtol=1e-6) m1 = CBLS.Optimizer() MOI.add_variable(m1) - MOI.add_constraint(m1, VI(1), CBLS.DiscreteSet([1,2,3])) + MOI.add_constraint(m1, VI(1), CBLS.DiscreteSet([1, 2, 3])) m2 = CBLS.Optimizer() - MOI.add_constrained_variable(m2, CBLS.DiscreteSet([1,2,3])) + MOI.add_constrained_variable(m2, CBLS.DiscreteSet([1, 2, 3])) # opt = CBLS.sudoku(3, modeler = :MOI) # MOI.optimize!(opt) diff --git a/test/TestItemRunner.jl b/test/TestItemRunner.jl new file mode 100644 index 0000000..d843b4d --- /dev/null +++ b/test/TestItemRunner.jl @@ -0,0 +1,5 @@ +using TestItemRunner + +@testset "TestItemRunner" begin + @run_package_tests +end diff --git a/test/runtests.jl b/test/runtests.jl index bf16a49..8fb5049 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,4 +4,5 @@ using Test @testset "CBLS.jl" begin include("MOI_wrapper.jl") include("JuMP.jl") + include("TestItemRunner.jl") end From 89941ea17a43117630334eddcf0fc4f721479e8e Mon Sep 17 00:00:00 2001 From: Jean-Francois Baffier Date: Sun, 23 Jun 2024 10:43:22 +0900 Subject: [PATCH 2/6] Update Project.toml --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index fcffc56..399be22 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CBLS" uuid = "a3809bfe-37bb-4d48-a667-bac4c6be8d90" authors = ["Jean-Francois Baffier"] -version = "0.1.13" +version = "0.1.14" [deps] ConstraintCommons = "e37357d9-0691-492f-a822-e5ea6a920954" @@ -30,4 +30,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "Test", "TestItemRunner"] \ No newline at end of file +test = ["Aqua", "Test", "TestItemRunner"] From bf33070b92f21cbb02f04265e28db602cc3b36e8 Mon Sep 17 00:00:00 2001 From: Jean-Francois Baffier Date: Sun, 23 Jun 2024 10:51:21 +0900 Subject: [PATCH 3/6] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 399be22..0335df4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CBLS" uuid = "a3809bfe-37bb-4d48-a667-bac4c6be8d90" authors = ["Jean-Francois Baffier"] -version = "0.1.14" +version = "0.2.0" [deps] ConstraintCommons = "e37357d9-0691-492f-a822-e5ea6a920954" From 27b8c368a77cdf5a99fb0b5023b02bc5d86e423a Mon Sep 17 00:00:00 2001 From: Azzaare Date: Sun, 23 Jun 2024 02:14:28 +0000 Subject: [PATCH 4/6] Update compat --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 0335df4..52052ca 100644 --- a/Project.toml +++ b/Project.toml @@ -15,6 +15,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] +ConstraintCommons = "0.2" ConstraintDomains = "0.3" Constraints = "0.5" Intervals = "1" @@ -22,6 +23,7 @@ JuMP = "1" Lazy = "0.15" LocalSearchSolvers = "0.4" MathOptInterface = "1" +TestItems = "0.1" julia = "1.6" [extras] From 5d12b8c29f2101e866966050131d4a98f9600394 Mon Sep 17 00:00:00 2001 From: Jean-Francois Baffier Date: Wed, 26 Jun 2024 13:30:46 +0200 Subject: [PATCH 5/6] Fix test and printing bug for objectives (#32) * Basic tests for MOI/JuMP * Solve the printing issue for objectives --- Project.toml | 1 + src/CBLS.jl | 4 +- src/MOI_wrapper.jl | 177 ++++++++++++++++++++++++++++------ src/constraints.jl | 121 ++++++++++++++++++----- src/constraints/cumulative.jl | 5 +- src/constraints/extension.jl | 6 +- src/constraints/n_values.jl | 7 +- src/objectives.jl | 8 ++ test/JuMP.jl | 16 +-- test/MOI_wrapper.jl | 136 +++++++++++++------------- 10 files changed, 337 insertions(+), 144 deletions(-) diff --git a/Project.toml b/Project.toml index 52052ca..a6cd041 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Lazy = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0" LocalSearchSolvers = "2b10edaa-728d-4283-ac71-07e312d6ccf3" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] diff --git a/src/CBLS.jl b/src/CBLS.jl index e636f28..2895f26 100644 --- a/src/CBLS.jl +++ b/src/CBLS.jl @@ -8,6 +8,7 @@ using JuMP using Lazy using LocalSearchSolvers using MathOptInterface +using Pkg using TestItems # Const @@ -30,7 +31,8 @@ const VAR_TYPES = Union{MOI.ZeroOne, MOI.Integer} export DiscreteSet # Export: Constraints -export Error, Predicate +export Error +export Intention, Predicate export AllDifferent export AllEqual diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index fca601b..e1070c6 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1,12 +1,14 @@ """ JuMP.build_variable(::Function, info::JuMP.VariableInfo, set::T) where T <: MOI.AbstractScalarSet -DOCSTRING +Create a variable constrained by a scalar set. -# Arguments: -- ``: DESCRIPTION -- `info`: DESCRIPTION -- `set`: DESCRIPTION +# Arguments +- `info::JuMP.VariableInfo`: Information about the variable to be created. +- `set::T where T <: MOI.AbstractScalarSet`: The set defining the constraints on the variable. + +# Returns +- `JuMP.VariableConstrainedOnCreation`: A variable constrained by the specified set. """ function JuMP.build_variable( ::Function, @@ -19,12 +21,12 @@ end """ Optimizer <: MOI.AbstractOptimizer -DOCSTRING +Defines an optimizer for CBLS. -# Arguments: -- `solver::Solver`: DESCRIPTION -- `status::MOI.TerminationStatusCode`: DESCRIPTION -- `options::Options`: DESCRIPTION +# Fields +- `solver::LS.MainSolver`: The main solver used for local search. +- `int_vars::Set{Int}`: Set of integer variables. +- `compare_vars::Set{Int}`: Set of variables to compare. """ mutable struct Optimizer <: MOI.AbstractOptimizer solver::LS.MainSolver @@ -35,7 +37,14 @@ end """ Optimizer(model = Model(); options = Options()) -DOCSTRING +Create an instance of the Optimizer. + +# Arguments +- `model`: The model to be optimized. +- `options::Options`: Options for configuring the solver. + +# Returns +- `Optimizer`: An instance of the optimizer. """ function Optimizer(model = model(); options = Options()) return Optimizer( @@ -51,52 +60,104 @@ end @forward Optimizer.solver LS._best_bound, LS.best_value, LS.is_sat, LS.get_value @forward Optimizer.solver LS.domain_size, LS.best_values, LS._max_cons, LS.update_domain! @forward Optimizer.solver LS.get_variable, LS.has_solution, LS.sense, LS.sense! -@forward Optimizer.solver LS.time_info, LS.status +@forward Optimizer.solver LS.time_info, LS.status, LS.length_vars # forward functions from Solver (from Options) @forward Optimizer.solver LS._verbose, LS.set_option!, LS.get_option """ - MOI.get(::Optimizer, ::MOI.SolverName) = begin + MOI.get(::Optimizer, ::MOI.SolverName) + +Get the name of the solver. -DOCSTRING +# Arguments +- `::Optimizer`: The optimizer instance. + +# Returns +- `String`: The name of the solver. """ -MOI.get(::Optimizer, ::MOI.SolverName) = "LocalSearchSolvers" +MOI.get(::Optimizer, ::MOI.SolverName) = "CBLS" """ - MOI.set(::Optimizer, ::MOI.Silent, bool = true) = begin + MOI.set(::Optimizer, ::MOI.Silent, bool = true) + +Set the verbosity of the solver. -DOCSTRING +# Arguments +- `::Optimizer`: The optimizer instance. +- `::MOI.Silent`: The silent option for the solver. +- `bool::Bool`: Whether to set the solver to silent mode. -# Arguments: -- ``: DESCRIPTION -- ``: DESCRIPTION -- `bool`: DESCRIPTION +# Returns +- `Nothing` """ MOI.set(::Optimizer, ::MOI.Silent, bool = true) = @debug "TODO: Silent" """ - MOI.is_empty(model::Optimizer) = begin + MOI.is_empty(model::Optimizer) + +Check if the model is empty. -DOCSTRING +# Arguments +- `model::Optimizer`: The optimizer instance. + +# Returns +- `Bool`: True if the model is empty, false otherwise. """ MOI.is_empty(model::Optimizer) = LS._is_empty(model.solver) """ -Copy constructor for the optimizer + MOI.supports_incremental_interface(::Optimizer) + +Check if the optimizer supports incremental interface. + +# Arguments +- `::Optimizer`: The optimizer instance. + +# Returns +- `Bool`: True if the optimizer supports incremental interface, false otherwise. """ MOI.supports_incremental_interface(::Optimizer) = true + +""" + MOI.copy_to(model::Optimizer, src::MOI.ModelLike) + +Copy the source model to the optimizer. + +# Arguments +- `model::Optimizer`: The optimizer instance. +- `src::MOI.ModelLike`: The source model to be copied. + +# Returns +- `Nothing` +""" function MOI.copy_to(model::Optimizer, src::MOI.ModelLike) return MOIU.default_copy_to(model, src) end """ MOI.optimize!(model::Optimizer) + +Optimize the model using the optimizer. + +# Arguments +- `model::Optimizer`: The optimizer instance. + +# Returns +- `Nothing` """ MOI.optimize!(optimizer::Optimizer) = solve!(optimizer.solver) """ DiscreteSet(values) + +Create a discrete set of values. + +# Arguments +- `values::Vector{T}`: A vector of values to include in the set. + +# Returns +- `DiscreteSet{T}`: A discrete set containing the specified values. """ struct DiscreteSet{T <: Number} <: MOI.AbstractScalarSet values::Vector{T} @@ -105,22 +166,82 @@ DiscreteSet(values) = DiscreteSet(collect(values)) DiscreteSet(values::T...) where {T <: Number} = DiscreteSet(collect(values)) """ - Base.copy(set::DiscreteSet) = begin + Base.copy(set::DiscreteSet) + +Copy a discrete set. + +# Arguments +- `set::DiscreteSet`: The discrete set to be copied. -DOCSTRING +# Returns +- `DiscreteSet`: A copy of the discrete set. """ Base.copy(set::DiscreteSet) = DiscreteSet(copy(set.values)) """ - MOI.empty!(opt) = begin + MOI.empty!(opt) -DOCSTRING +Empty the optimizer. + +# Arguments +- `opt::Optimizer`: The optimizer instance. + +# Returns +- `Nothing` """ MOI.empty!(opt) = empty!(opt) +""" + MOI.is_valid(optimizer::Optimizer, index::CI{VI, MOI.Integer}) + +Check if an index is valid for the optimizer. + +# Arguments +- `optimizer::Optimizer`: The optimizer instance. +- `index::CI{VI, MOI.Integer}`: The index to be checked. + +# Returns +- `Bool`: True if the index is valid, false otherwise. +""" function MOI.is_valid(optimizer::Optimizer, index::CI{VI, MOI.Integer}) return index.value ∈ optimizer.int_vars end +""" + Base.copy(op::F) where {F <: Function} + +Copy a function. + +# Arguments +- `op::F`: The function to be copied. + +# Returns +- `F`: The copied function. +""" Base.copy(op::F) where {F <: Function} = op + +""" + Base.copy(::Nothing) + +Copy a `Nothing` value. + +# Arguments +- `::Nothing`: The `Nothing` value to be copied. + +# Returns +- `Nothing`: The copied `Nothing` value. +""" Base.copy(::Nothing) = nothing + +""" + Moi.get(::Optimizer, ::MOI.SolverVersion) + +Get the version of the solver, here `LocalSearchSolvers.jl`. +""" +function MOI.get(::Optimizer, ::MOI.SolverVersion) + deps = Pkg.dependencies() + local_search_solver_uuid = Base.UUID("2b10edaa-728d-4283-ac71-07e312d6ccf3") + return "v" * string(deps[local_search_solver_uuid].version) +end + +MOI.get(opt::Optimizer, ::MOI.NumberOfVariables) = LS.length_vars(opt) diff --git a/src/constraints.jl b/src/constraints.jl index 0e93d56..d49b244 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -73,50 +73,125 @@ end JuMP.moi_set(set::Error{F}, dim::Int) where {F <: Function} = MOIError(set.f, dim) """ - MOIPredicate{F <: Function} <: MOI.AbstractVectorSet + MOIIntention{F <: Function} <: MOI.AbstractVectorSet -DOCSTRING +Represents an intention set in the model. -# Arguments: -- `f::F`: DESCRIPTION -- `dimension::Int`: DESCRIPTION -- `MOIPredicate(f, dim = 0) = begin - #= none:5 =# - new{typeof(f)}(f, dim) - end`: DESCRIPTION +# Arguments +- `f::F`: A function representing the intention. +- `dimension::Int`: The dimension of the vector set. """ -struct MOIPredicate{F <: Function} <: MOI.AbstractVectorSet +struct MOIIntention{F <: Function} <: MOI.AbstractVectorSet f::F dimension::Int - MOIPredicate(f, dim = 0) = new{typeof(f)}(f, dim) + MOIIntention(f, dim = 0) = new{typeof(f)}(f, dim) end -function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIPredicate{F}} -) where {F <: Function} + +""" + MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIIntention{F}}) where {F <: Function} + +Check if the optimizer supports a given intention constraint. + +# Arguments +- `::Optimizer`: The optimizer instance. +- `::Type{VOV}`: The type of the variable. +- `::Type{MOIIntention{F}}`: The type of the intention. + +# Returns +- `Bool`: True if the optimizer supports the constraint, false otherwise. +""" +function MOI.supports_constraint( + ::Optimizer, ::Type{VOV}, ::Type{MOIIntention{F}}) where {F <: Function} return true end + +""" + MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIIntention{F}) where {F <: Function} + +Add an intention constraint to the optimizer. + +# Arguments +- `optimizer::Optimizer`: The optimizer instance. +- `vars::MOI.VectorOfVariables`: The variables for the constraint. +- `set::MOIIntention{F}`: The intention set defining the constraint. + +# Returns +- `CI{VOV, MOIIntention{F}}`: The constraint index. +""" function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, - set::MOIPredicate{F}) where {F <: Function} + set::MOIIntention{F}) where {F <: Function} err = x -> convert(Float64, !set.f(x)) cidx = constraint!(optimizer, err, map(x -> x.value, vars.variables)) - return CI{VOV, MOIPredicate{F}}(cidx) + return CI{VOV, MOIIntention{F}}(cidx) end -Base.copy(set::MOIPredicate) = MOIPredicate(deepcopy(set.f), copy(set.dimension)) +""" + Base.copy(set::MOIIntention) + +Copy an intention set. + +# Arguments +- `set::MOIIntention`: The intention set to be copied. + +# Returns +- `MOIIntention`: A copy of the intention set. +""" +Base.copy(set::MOIIntention) = MOIIntention(deepcopy(set.f), copy(set.dimension)) """ Predicate{F <: Function} <: JuMP.AbstractVectorSet -Assuming `X` is a (collection of) variables, `concept` a boolean function over `X`, and that a `model` is defined. In `JuMP` syntax we can create a constraint based on `concept` as follows. +Deprecated: Use `Intention` instead. -```julia -@constraint(model, X in Predicate(concept)) -``` +Represents a predicate set in the model. + +# Arguments +- `f::F`: A function representing the predicate. """ struct Predicate{F <: Function} <: JuMP.AbstractVectorSet f::F end -JuMP.moi_set(set::Predicate, dim::Int) = MOIPredicate(set.f, dim) + +""" + Intention{F <: Function} <: JuMP.AbstractVectorSet + +Represents an intention set in the model. + +# Arguments +- `f::F`: A function representing the intention. +""" +struct Intention{F <: Function} <: JuMP.AbstractVectorSet + f::F +end + +""" + JuMP.moi_set(set::Predicate, dim::Int) -> MOIIntention + +Convert a `Predicate` set to a `MOIIntention` set. + +# Arguments +- `set::Predicate`: The predicate set to be converted. +- `dim::Int`: The dimension of the vector set. + +# Returns +- `MOIIntention`: The converted MOIIntention set. +""" +JuMP.moi_set(set::Predicate, dim::Int) = MOIIntention(set.f, dim) + +""" + JuMP.moi_set(set::Intention, dim::Int) -> MOIIntention + +Convert an `Intention` set to a `MOIIntention` set. + +# Arguments +- `set::Intention`: The intention set to be converted. +- `dim::Int`: The dimension of the vector set. + +# Returns +- `MOIIntention`: The converted MOIIntention set. +""" +JuMP.moi_set(set::Intention, dim::Int) = MOIIntention(set.f, dim) ## SECTION - Test Items @testitem "Error and Predicate" begin @@ -129,10 +204,10 @@ JuMP.moi_set(set::Predicate, dim::Int) = MOIPredicate(set.f, dim) @variable(model, 1≤Y[1:4]≤4, Int) @constraint(model, X in Error(x -> x[1] + x[2] + x[3] + x[4] == 10)) - @constraint(model, Y in Predicate(x -> x[1] + x[2] + x[3] + x[4] == 10)) + @constraint(model, Y in Intention(x -> x[1] + x[2] + x[3] + x[4] == 10)) optimize!(model) - @info "Error and Predicate" value.(X) value.(Y) + @info "Error and Intention" value.(X) value.(Y) termination_status(model) @info solution_summary(model) end diff --git a/src/constraints/cumulative.jl b/src/constraints/cumulative.jl index 62425c7..700efa2 100644 --- a/src/constraints/cumulative.jl +++ b/src/constraints/cumulative.jl @@ -61,10 +61,7 @@ struct Cumulative{F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} end end -function Cumulative(; op::F = ≤, pair_vars::V = Vector{Number}(), - val::T2) where {F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} - return Cumulative(op, pair_vars, val) -end +Cumulative(; op = ≤, pair_vars = Vector{Number}(), val) = Cumulative(op, pair_vars, val) function JuMP.moi_set(set::Cumulative, dim::Int) return MOICumulative(set.op, set.pair_vars, set.val, dim) diff --git a/src/constraints/extension.jl b/src/constraints/extension.jl index 60e03bd..3b597c0 100644 --- a/src/constraints/extension.jl +++ b/src/constraints/extension.jl @@ -51,11 +51,7 @@ struct Extension{T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vec end end -function Extension(; - pair_vars::V) where { - T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} - return Extension(pair_vars) -end +Extension(; pair_vars) = Extension(pair_vars) function JuMP.moi_set(set::Extension, dim::Int) return MOIExtension(set.pair_vars, dim) diff --git a/src/constraints/n_values.jl b/src/constraints/n_values.jl index f4f542a..fd9b5de 100644 --- a/src/constraints/n_values.jl +++ b/src/constraints/n_values.jl @@ -54,12 +54,7 @@ struct NValues{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} <: end end -function NValues(; op::F = ==, - val::T1, - vals::V = Vector{Number}()) where { - F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} - return NValues(op, val, vals) -end +NValues(; op = ==, val, vals = Vector{Number}()) = NValues(op, val, vals) function JuMP.moi_set(set::NValues, dim::Int) vals = isnothing(set.vals) ? Vector{Number}() : set.vals diff --git a/src/objectives.jl b/src/objectives.jl index 024aa9b..9d31c2b 100644 --- a/src/objectives.jl +++ b/src/objectives.jl @@ -72,3 +72,11 @@ end function MOIU.map_indices(::Function, sf::ScalarFunction{F, Nothing}) where {F <: Function} return ScalarFunction(sf.f, nothing) end + +function MOIU._to_string(::MOIU._PrintOptions, ::MOI.ModelLike, f::ScalarFunction) + return "Scalar Objective function: $(typeof(f))" +end + +function JuMP.jump_function_type(::GenericModel{T}, F::Type{<:ScalarFunction}) where {T} + return F +end diff --git a/test/JuMP.jl b/test/JuMP.jl index e549e08..8707bcd 100644 --- a/test/JuMP.jl +++ b/test/JuMP.jl @@ -9,15 +9,7 @@ using JuMP @variable(m, 1≤X[1:10]≤4, Int) @constraint(m, X in Error(err)) - @constraint(m, X in Predicate(concept)) - - @constraint(m, X in AllDifferent()) - @constraint(m, X in AllEqual()) - #@constraint(m, X in AllEqualParam(2)) - @constraint(m, X[1:4] in DistDifferent()) - #@constraint(m, X[1:4] in SequentialTasks()) - @constraint(m, X in Ordered()) - #@constraint(m, X in SumEqualParam(22)) + @constraint(m, X in Intention(concept)) optimize!(m) end @@ -33,8 +25,8 @@ end @variable(model, 0≤x≤20, Int) @variable(model, y in DiscreteSet(0:20)) - @constraint(model, [x, y] in Predicate(v -> 6v[1] + 8v[2] >= 100)) - @constraint(model, [x, y] in Predicate(v -> 7v[1] + 12v[2] >= 120)) + @constraint(model, [x, y] in Intention(v -> 6v[1] + 8v[2] >= 100)) + @constraint(model, [x, y] in Intention(v -> 7v[1] + 12v[2] >= 120)) objFunc = v -> 12v[1] + 20v[2] @objective(model, Min, ScalarFunction(objFunc)) @@ -43,4 +35,6 @@ end @info "JuMP: basic opt" value(x) value(y) (12 * value(x)+20 * value(y)) solve_time(model) termination_status(model) @info solution_summary(model) + + @info "testing objective printing" model end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 758b163..f0004d2 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -1,79 +1,83 @@ -using MathOptInterface -const MOI = MathOptInterface -const MOIT = MOI.Test -const MOIU = MOI.Utilities -const MOIB = MOI.Bridges +# ============================ /test/MOI_wrapper.jl ============================ +module TestCBLS -const VOV = MOI.VectorOfVariables -const VI = MOI.VariableIndex +import CBLS +using Test -const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes( - CBLS.Optimizer, MOI.Silent() => true -) -const OPTIMIZER = MOI.instantiate(OPTIMIZER_CONSTRUCTOR) - -@testset "LocalSearchSolvers" begin - @test MOI.get(OPTIMIZER, MOI.SolverName()) == "LocalSearchSolvers" -end +import MathOptInterface as MOI -# @testset "supports_default_copy_to" begin -# @test MOIU.supports_default_copy_to(OPTIMIZER, false) -# # Use `@test !...` if names are not supported -# @test !MOIU.supports_default_copy_to(OPTIMIZER, true) -# end +const OPTIMIZER = MOI.instantiate( + MOI.OptimizerWithAttributes(CBLS.Optimizer, MOI.Silent() => true), +) const BRIDGED = MOI.instantiate( - OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64 + MOI.OptimizerWithAttributes(CBLS.Optimizer, MOI.Silent() => true), + with_bridge_type = Float64, with_cache_type = Float64 ) -const CONFIG = MOIT.Config(atol = 1e-6, rtol = 1e-6) - -# @testset "Unit" begin -# # Test all the functions included in dictionary `MOI.Test.unittests`, -# # except functions "number_threads" and "solve_qcp_edge_cases." -# MOIT.unittest( -# BRIDGED, -# CONFIG, -# ["number_threads", "solve_qcp_edge_cases"] -# ) -# end - -# @testset "Modification" begin -# MOIT.modificationtest(BRIDGED, CONFIG) -# end - -# @testset "Continuous Linear" begin -# MOIT.contlineartest(BRIDGED, CONFIG) -# end -# @testset "Continuous Conic" begin -# MOIT.contlineartest(BRIDGED, CONFIG) -# end - -# @testset "Integer Conic" begin -# MOIT.intconictest(BRIDGED, CONFIG) -# end -@testset "MOI: examples" begin - # m = LocalSearchSolvers.Optimizer() - # MOI.add_variables(m, 3) - # MOI.add_constraint(m, VI(1), LS.DiscreteSet([1,2,3])) - # MOI.add_constraint(m, VI(2), LS.DiscreteSet([1,2,3])) - # MOI.add_constraint(m, VI(3), LS.DiscreteSet([1,2,3])) +# See the docstring of MOI.Test.Config for other arguments. +const CONFIG = MOI.Test.Config( + # Modify tolerances as necessary. + atol = 1e-6, + rtol = 1e-6, + # Use MOI.LOCALLY_SOLVED for local solvers. + optimal_status = MOI.LOCALLY_SOLVED # Pass attributes or MOI functions to `exclude` to skip tests that # rely on this functionality. # exclude = Any[MOI.VariableName, MOI.delete] +) - # MOI.add_constraint(m, VOV([VI(1),VI(2)]), LS.MOIPredicate(allunique)) - # MOI.add_constraint(m, VOV([VI(2),VI(3)]), LS.MOIAllDifferent(2)) +""" + runtests() + +This function runs all functions in the this Module starting with `test_`. +""" +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end +end - # MOI.set(m, MOI.ObjectiveFunction{LS.ScalarFunction}(), LS.ScalarFunction(sum, VI(1))) +""" + test_runtests() + +This function runs all the tests in MathOptInterface.Test. + +Pass arguments to `exclude` to skip tests for functionality that is not +implemented or that your solver doesn't support. +""" +function test_runtests() + MOI.Test.runtests( + BRIDGED, + CONFIG, + exclude = [ + "test_attribute_SolveTimeSec", # Hang indefinitely + "test_model_copy_to_UnsupportedAttribute", # Not supported. What it is suppsoed to be? + "supports_constraint_VariableIndex_EqualTo" # Not supported. What it is suppsoed to be? + ], + # This argument is useful to prevent tests from failing on future + # releases of MOI that add new tests. Don't let this number get too far + # behind the current MOI release though. You should periodically check + # for new tests to fix bugs and implement new features. + exclude_tests_after = v"1.30.0", + verbose = true + ) + return +end - # MOI.optimize!(m) +""" + test_SolverName() - m1 = CBLS.Optimizer() - MOI.add_variable(m1) - MOI.add_constraint(m1, VI(1), CBLS.DiscreteSet([1, 2, 3])) +You can also write new tests for solver-specific functionality. Write each new +test as a function with a name beginning with `test_`. +""" +function test_SolverName() + @test MOI.get(CBLS.Optimizer(), MOI.SolverName()) == "CBLS" + return +end - m2 = CBLS.Optimizer() - MOI.add_constrained_variable(m2, CBLS.DiscreteSet([1, 2, 3])) +end # module TestCBLS - # opt = CBLS.sudoku(3, modeler = :MOI) - # MOI.optimize!(opt) - # @info solution(opt) -end +# This line at tne end of the file runs all the tests! +TestCBLS.runtests() From 4ab4c5f0c7ea622028d49ec0e9366bbfd9e632e3 Mon Sep 17 00:00:00 2001 From: Jean-Francois Baffier Date: Fri, 12 Jul 2024 10:13:21 +0200 Subject: [PATCH 6/6] Compat, CI, and parallelism (#34) * Compat, CI, and parallelism * Spell and fix error in tests --- .github/workflows/CI.yml | 116 ++++++++++++++++++++--------- .github/workflows/CompatHelper.yml | 37 +++++++-- .github/workflows/SpellCheck.yml | 13 ++++ .github/workflows/TagBot.yml | 16 ++++ .github/workflows/register.yml | 16 ++++ Project.toml | 7 +- src/CBLS.jl | 2 +- src/attributes.jl | 17 ++++- src/constraints/intention.jl | 2 +- test/Aqua.jl | 37 +++++++++ test/JuMP.jl | 5 ++ test/MOI_wrapper.jl | 2 +- test/runtests.jl | 1 + 13 files changed, 221 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/SpellCheck.yml create mode 100644 .github/workflows/register.yml create mode 100644 test/Aqua.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 67f3220..5a66efc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,7 +1,48 @@ name: CI on: - - pull_request + pull_request: + branches: + - main + - dev + paths-ignore: + - "docs/**" + push: + branches: + - main + paths-ignore: + - "docs/**" jobs: + formatter: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1] + julia-arch: [x86] + os: [ubuntu-latest] + steps: + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + + - uses: actions/checkout@v4 + - name: Install JuliaFormatter and format + # This will use the latest version by default but you can set the version like so: + # + # julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter", version="0.13.0"))' + run: | + julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter", version="1.0.50"))' + julia -e 'using JuliaFormatter; format(".", verbose=true)' + - name: Format check + run: | + julia -e ' + out = Cmd(`git diff`) |> read |> String + if out == "" + exit(0) + else + @error "Some files have not been formatted !!!" + write(stdout, out) + exit(1) + end' test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} @@ -10,28 +51,33 @@ jobs: matrix: version: - "1.9" - - 'nightly' + - "1" # automatically expands to the latest stable 1.x release of Julia + - "pre" os: - ubuntu-latest - - macOS-latest - - windows-latest + threads: + - "2" arch: - x64 - x86 - threads: - - "2" - exclude: + include: + # test macOS and Windows with latest Julia only - os: macOS-latest - arch: x86 + arch: x64 + version: 1 + - os: windows-latest + arch: x64 + version: 1 - os: windows-latest arch: x86 + version: 1 steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: actions/cache@v4 env: cache-name: cache-artifacts with: @@ -46,29 +92,29 @@ jobs: env: JULIA_NUM_THREADS: ${{ matrix.threads }} - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v4 with: file: lcov.info - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 - with: - version: "1" - - run: | - julia --project=docs -e ' - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate()' - - run: | - julia --project=docs -e ' - using Documenter: DocMeta, doctest - using CBLS - DocMeta.setdocmeta!(CBLS, :DocTestSetup, :(using CBLS); recursive=true) - doctest(CBLS)' - - run: julia --project=docs docs/make.jl - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + # docs: + # name: Documentation + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: julia-actions/setup-julia@v1 + # with: + # version: "1" + # - run: | + # julia --project=docs -e ' + # using Pkg + # Pkg.develop(PackageSpec(path=pwd())) + # Pkg.instantiate()' + # - run: | + # julia --project=docs -e ' + # using Documenter: DocMeta, doctest + # using Constraints + # DocMeta.setdocmeta!(Constraints, :DocTestSetup, :(using Constraints); recursive=true) + # doctest(Constraints)' + # - run: julia --project=docs docs/make.jl + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index cba9134..f889647 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,16 +1,43 @@ -name: CompatHelper on: schedule: - cron: 0 0 * * * workflow_dispatch: +permissions: + contents: write + pull-requests: write jobs: CompatHelper: runs-on: ubuntu-latest steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v2 + with: + version: "1" + arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/SpellCheck.yml b/.github/workflows/SpellCheck.yml new file mode 100644 index 0000000..ed4fe17 --- /dev/null +++ b/.github/workflows/SpellCheck.yml @@ -0,0 +1,13 @@ +name: Spell Check + +on: [pull_request] + +jobs: + typos-check: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + - name: Check spelling + uses: crate-ci/typos@v1.18.0 diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b..0cd3114 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -4,6 +4,22 @@ on: types: - created workflow_dispatch: + inputs: + lookback: + default: "3" +permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' diff --git a/.github/workflows/register.yml b/.github/workflows/register.yml new file mode 100644 index 0000000..5b7cd3b --- /dev/null +++ b/.github/workflows/register.yml @@ -0,0 +1,16 @@ +name: Register Package +on: + workflow_dispatch: + inputs: + version: + description: Version to register or component to bump + required: true +jobs: + register: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: julia-actions/RegisterAction@latest + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Project.toml b/Project.toml index 72db1d4..2a0a163 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CBLS" uuid = "a3809bfe-37bb-4d48-a667-bac4c6be8d90" authors = ["Jean-Francois Baffier"] -version = "0.2.1" +version = "0.2.2" [deps] ConstraintCommons = "e37357d9-0691-492f-a822-e5ea6a920954" @@ -24,8 +24,9 @@ JuMP = "1" Lazy = "0.15" LocalSearchSolvers = "0.4" MathOptInterface = "1" -TestItems = "0.1" -julia = "1.6" +Pkg = "1" +TestItems = "0.1, 1" +julia = "1.9" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" diff --git a/src/CBLS.jl b/src/CBLS.jl index 2895f26..1cca870 100644 --- a/src/CBLS.jl +++ b/src/CBLS.jl @@ -49,7 +49,7 @@ export Maximum export MDDConstraint export Minimum export NValues -export NoOverlap, NoOverlapNoZero, NoOverlapWithZero +export NoOverlap#, NoOverlapNoZero, NoOverlapWithZero export Ordered export Regular export Sum diff --git a/src/attributes.jl b/src/attributes.jl index 12490d3..10dca07 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -24,10 +24,19 @@ MOI.set(model::Optimizer, p::MOI.RawOptimizerAttribute, value) = set_option!( model, p.name, value) MOI.get(model::Optimizer, p::MOI.RawOptimizerAttribute) = get_option(model, p.name) -function MOI.set(model::Optimizer, ::MOI.NumberOfThreads, value) - set_option!(model, "threads", isnothing(value) ? typemax(0) : value) +function MOI.set(model::Optimizer, ::MOI.NumberOfThreads, value::Int) + set_option!(model, "threads", value) end +function MOI.set( + model::Optimizer, ::MOI.NumberOfThreads, value::Union{AbstractVector, AbstractDict}) + set_option!(model, "process_threads_map", value) +end + function MOI.get(model::Optimizer, ::MOI.NumberOfThreads) - nt = get_option(model, "threads") - return nt == typemax(0) ? nothing : nt + ptm = get_option(model, "process_threads_map") + if length(ptm) == 0 || (haskey(ptm, 1) && length(ptm) == 1) + nt = get_option(model, "threads") + return nt == typemax(0) ? nothing : nt + end + return ptm end diff --git a/src/constraints/intention.jl b/src/constraints/intention.jl index 65a5c44..bf6788b 100644 --- a/src/constraints/intention.jl +++ b/src/constraints/intention.jl @@ -1,4 +1,4 @@ -# Intention constraints emcompass any generic constraint. DistDifferent is implemented as an example of an intensional constraint. +# Intention constraints encompass any generic constraint. DistDifferent is implemented as an example of an intensional constraint. """ MOIDistDifferent <: MOI.AbstractVectorSet diff --git a/test/Aqua.jl b/test/Aqua.jl new file mode 100644 index 0000000..5f172c8 --- /dev/null +++ b/test/Aqua.jl @@ -0,0 +1,37 @@ +@testset "Aqua.jl" begin + import Aqua + import CBLS + import JuMP + import MathOptInterface + + # TODO: Fix the broken tests and remove the `broken = true` flag + Aqua.test_all( + CBLS; + ambiguities = (broken = true,), + deps_compat = false, + piracies = (broken = true,), + unbound_args = (broken = false) + ) + + @testset "Ambiguities: CBLS" begin + # Aqua.test_ambiguities(CBLS;) + end + + @testset "Piracies: CBLS" begin + Aqua.test_piracies(CBLS; + # Check with JuMP-dev + treat_as_own = [JuMP.build_variable, Base.copy, MathOptInterface.empty!] + ) + end + + @testset "Dependencies compatibility (no extras)" begin + Aqua.test_deps_compat( + CBLS; + check_extras = false # ignore = [:Random] + ) + end + + @testset "Unbound type parameters" begin + # Aqua.test_unbound_args(CBLS;) + end +end diff --git a/test/JuMP.jl b/test/JuMP.jl index 8707bcd..dc22b2b 100644 --- a/test/JuMP.jl +++ b/test/JuMP.jl @@ -21,6 +21,11 @@ end @test get_optimizer_attribute(model, "iteration") == 100 set_time_limit_sec(model, 5.0) @test time_limit_sec(model) == 5.0 + set_optimizer_attribute(model, "threads", 2) + @test get_optimizer_attribute(model, "threads") == 2 + T = Dict(2 => 3, 3 => 1) + set_optimizer_attribute(model, "process_threads_map", T) + @test get_optimizer_attribute(model, "process_threads_map") == T @variable(model, 0≤x≤20, Int) @variable(model, y in DiscreteSet(0:20)) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 7f4a6d7..d20863f 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -79,5 +79,5 @@ end end # module TestCBLS -# This line at tne end of the file runs all the tests! +# This line at the end of the file runs all the tests! TestCBLS.runtests() diff --git a/test/runtests.jl b/test/runtests.jl index 8fb5049..98d3020 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ using CBLS using Test @testset "CBLS.jl" begin + include("Aqua.jl") include("MOI_wrapper.jl") include("JuMP.jl") include("TestItemRunner.jl")