From 6adf6213780afd23cf8ac94b6b54eafd7490284a Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 7 Oct 2019 20:01:20 -0500 Subject: [PATCH 01/14] Add Complements set --- src/Test/UnitTests/basic_constraint_tests.jl | 2 +- src/Test/nlp.jl | 41 ++++++++++++++ src/Utilities/model.jl | 2 +- src/sets.jl | 57 ++++++++++++++++---- test/Test/nlp.jl | 19 +++++++ 5 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 test/Test/nlp.jl diff --git a/src/Test/UnitTests/basic_constraint_tests.jl b/src/Test/UnitTests/basic_constraint_tests.jl index 99436febf7..6e4249bd5c 100644 --- a/src/Test/UnitTests/basic_constraint_tests.jl +++ b/src/Test/UnitTests/basic_constraint_tests.jl @@ -64,7 +64,7 @@ const BasicConstraintTests = Dict( (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => ( dummy_vector_affine, 2, MOI.Zeros(2) ), (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => ( dummy_vector_affine, 2, MOI.Nonpositives(2) ), (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => ( dummy_vector_affine, 2, MOI.Nonnegatives(2) ), - + (MOI.VectorAffineFunction{Float64}, MOI.Complements) => (dummy_vector_affine, 2, MOI.Complements(1)), (MOI.VectorAffineFunction{Float64}, MOI.NormInfinityCone) => ( dummy_vector_affine, 3, MOI.NormInfinityCone(3) ), (MOI.VectorAffineFunction{Float64}, MOI.NormOneCone) => ( dummy_vector_affine, 3, MOI.NormOneCone(3) ), (MOI.VectorAffineFunction{Float64}, MOI.SecondOrderCone) => ( dummy_vector_affine, 3, MOI.SecondOrderCone(3) ), diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 26484441b6..184c92f2a9 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -337,3 +337,44 @@ const nlptests = Dict("hs071" => hs071_test, ) @moitestset nlp + +function test_linear_mcp(model::MOI.ModelLike, config::TestConfig) + MOI.empty!(model) + x = MOI.add_variables(model, 4) + MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.Interval(0.0, 10.0)) + MOI.set.(model, MOI.VariablePrimalStart(), x, 0.0) + M = Float64[0 0 -1 -1; 0 0 1 -2; 1 -1 2 -2; 1 2 -2 4] + q = [2; 2; -2; -6] + terms = MOI.VectorAffineTerm{Float64}[] + for i = 1:4 + push!( + terms, + MOI.VectorAffineTerm(4 + i, MOI.ScalarAffineTerm(1.0, x[i])) + ) + for j = 1:4 + iszero(M[i, j]) && continue + push!( + terms, + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(M[i, j], x[j])) + ) + end + end + MOI.add_constraint( + model, + MOI.VectorAffineFunction(terms, [q; 0.0; 0.0; 0.0; 0.0]), + MOI.Complements(4) + ) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED + x_val = MOI.get.(model, MOI.VariablePrimal(), x) + @test isapprox( + x_val, [2.8, 0.0, 0.8, 1.2], atol = config.atol, rtol = config.rtol + ) +end + +const complementaritytests = Dict( + "linear_mcp" => test_linear_mcp, +) + +@moitestset complementarity diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index f1d7d3a1fb..7cdcd1b69d 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -1027,7 +1027,7 @@ const LessThanIndicatorSetZero{T} = MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO, MOI.L (MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval, MOI.Semicontinuous, MOI.Semiinteger), (MOI.Reals, MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives, - MOI.NormInfinityCone, MOI.NormOneCone, + MOI.Complements, MOI.NormInfinityCone, MOI.NormOneCone, MOI.SecondOrderCone, MOI.RotatedSecondOrderCone, MOI.GeometricMeanCone, MOI.ExponentialCone, MOI.DualExponentialCone, MOI.PositiveSemidefiniteConeTriangle, MOI.PositiveSemidefiniteConeSquare, diff --git a/src/sets.jl b/src/sets.jl index e97a2316cc..20d6cf2f0b 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -669,18 +669,53 @@ end Base.:(==)(set1::IndicatorSet{A, S}, set2::IndicatorSet{A, S}) where {A, S} = set1.set == set2.set +""" + Complements(dimension::Int) + +The set corresponding to a mixed complementarity constraint. + +Complementarity constraints should be specified with an +[`AbstractVectorFunction`](@ref)-in-`Complements(dimension)` constraint. + +If `F` function, then the dimension of `F` must be `2 * dimension`. This defines +a complementarity constraint between `F[i]` and `F[i + dimension]`. Thus, +`F[i + dimension]` must be interpretable as a single variable (e.g., `1.0 * x + +0.0`). + +If a variable `x_i` is constrained in `Interval(lb, ub)`, then mathematically, +the mixed complementarity problem is to find a solution such that at least one +of the following holds: + + 1. F_i(x) = 0, lb <= x_i <= ub_i + 2. F_i(x) > 0, lb == x_i + 3. F_i(x) < 0, x_i == ub_i + +Classically, the bounding set for `x_i` is `Interval(0, Inf)`, which recovers: +0 <= F_i(x) ⟂ x >= 0, where the `⟂` operator implies F_i(x) * x = 0. + +### Examples + + [x, y] -in- Complements(1) + [x, y, u, w] -in- Complements(2) + [2 * x - 3, x] -in- Complements(1) +""" +struct Complements <: AbstractVectorSet + dimension::Int +end + # isbits types, nothing to copy -function Base.copy(set::Union{Reals, Zeros, Nonnegatives, Nonpositives, - GreaterThan, LessThan, EqualTo, Interval, - NormInfinityCone, NormOneCone, - SecondOrderCone, RotatedSecondOrderCone, - GeometricMeanCone, ExponentialCone, - DualExponentialCone, PowerCone, DualPowerCone, - PositiveSemidefiniteConeTriangle, - PositiveSemidefiniteConeSquare, - LogDetConeTriangle, LogDetConeSquare, - RootDetConeTriangle, RootDetConeSquare, - Integer, ZeroOne, Semicontinuous, Semiinteger}) +function Base.copy( + set::Union{ + Reals, Zeros, Nonnegatives, Nonpositives, GreaterThan, LessThan, + EqualTo, Interval, NormInfinityCone, NormOneCone, SecondOrderCone, + RotatedSecondOrderCone, GeometricMeanCone, ExponentialCone, + DualExponentialCone, PowerCone, DualPowerCone, + PositiveSemidefiniteConeTriangle, PositiveSemidefiniteConeSquare, + LogDetConeTriangle, LogDetConeSquare, RootDetConeTriangle, + RootDetConeSquare, Complements, Integer, ZeroOne, Semicontinuous, + Semiinteger + } +) return set end Base.copy(set::S) where {S <: Union{SOS1, SOS2}} = S(copy(set.weights)) diff --git a/test/Test/nlp.jl b/test/Test/nlp.jl new file mode 100644 index 0000000000..6efdcc9728 --- /dev/null +++ b/test/Test/nlp.jl @@ -0,0 +1,19 @@ +using Test + +import MathOptInterface +const MOI = MathOptInterface + +@testset "complementarity" begin + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + ) + config = MOI.Test.TestConfig() + + MOI.Utilities.set_mock_optimize!( + mock, + (mock) -> MOI.Utilities.mock_optimize!( + mock, MOI.LOCALLY_SOLVED, [2.8, 0.0, 0.8, 1.2] + ) + ) + MOI.Test.test_linear_mcp(mock, config) +end From 097811e473a6cb085e1d2160a301f741bb4c6385 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 7 Oct 2019 20:04:47 -0500 Subject: [PATCH 02/14] Update docstring --- src/sets.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sets.jl b/src/sets.jl index 20d6cf2f0b..c4bcaa2528 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -677,12 +677,12 @@ The set corresponding to a mixed complementarity constraint. Complementarity constraints should be specified with an [`AbstractVectorFunction`](@ref)-in-`Complements(dimension)` constraint. -If `F` function, then the dimension of `F` must be `2 * dimension`. This defines -a complementarity constraint between `F[i]` and `F[i + dimension]`. Thus, -`F[i + dimension]` must be interpretable as a single variable (e.g., `1.0 * x + -0.0`). +The dimension of the vector-valued function `F` must be `2 * dimension`. This +defines a complementarity constraint between the scalar function `F[i]` and the +variable in `F[i + dimension]`. Thus, `F[i + dimension]` must be interpretable +as a single variable `x_i` (e.g., `1.0 * x + 0.0`). -If a variable `x_i` is constrained in `Interval(lb, ub)`, then mathematically, +If the variable `x_i` is constrained in `Interval(lb, ub)`, then mathematically, the mixed complementarity problem is to find a solution such that at least one of the following holds: From 85c5a4f7d65619b017d92e81d352ec67e884b251 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 7 Oct 2019 21:52:40 -0500 Subject: [PATCH 03/14] Fix test/Test/Tests.jl --- test/Test/Test.jl | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/test/Test/Test.jl b/test/Test/Test.jl index e40ee09f2b..1e26b3d94d 100644 --- a/test/Test/Test.jl +++ b/test/Test/Test.jl @@ -1,23 +1,8 @@ using Test -@testset "Config" begin - include("config.jl") -end -@testset "Unit" begin - include("unit.jl") -end -@testset "Continuous Linear" begin - include("contlinear.jl") -end -@testset "Continuous Conic" begin - include("contconic.jl") -end -@testset "Continuous Quadratic" begin - include("contquadratic.jl") -end -@testset "Integer Linear" begin - include("intlinear.jl") -end -@testset "Integer Conic" begin - include("intconic.jl") +@testset "$(file)" for file in readdir(@__DIR__) + if file == "Test.jl" + continue + end + include(file) end From 730cc086851655e8d49ae412fe3e97e6b4f6e3ae Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Oct 2019 08:24:09 -0500 Subject: [PATCH 04/14] Update docstrings --- src/Test/nlp.jl | 10 ++++++++-- test/Test/nlp.jl | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 184c92f2a9..8c5e82ef69 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -338,7 +338,13 @@ const nlptests = Dict("hs071" => hs071_test, @moitestset nlp -function test_linear_mcp(model::MOI.ModelLike, config::TestConfig) +""" + test_linear_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) + +Test the solution of the linear mixed-complementarity problem: +`F(x) complements x`, where `F(x) = M * x .+ q` and `0 <= x <= 10`. +""" +function test_linear_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) MOI.empty!(model) x = MOI.add_variables(model, 4) MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.Interval(0.0, 10.0)) @@ -374,7 +380,7 @@ function test_linear_mcp(model::MOI.ModelLike, config::TestConfig) end const complementaritytests = Dict( - "linear_mcp" => test_linear_mcp, + "linear_mixed_complementarity" => test_linear_mixed_complementarity, ) @moitestset complementarity diff --git a/test/Test/nlp.jl b/test/Test/nlp.jl index 6efdcc9728..8085ec9cc7 100644 --- a/test/Test/nlp.jl +++ b/test/Test/nlp.jl @@ -7,13 +7,13 @@ const MOI = MathOptInterface mock = MOI.Utilities.MockOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) ) - config = MOI.Test.TestConfig() + config = MOI.Test.TestConfig(optimal_status = MOI.LOCALLY_SOLVED) MOI.Utilities.set_mock_optimize!( mock, (mock) -> MOI.Utilities.mock_optimize!( - mock, MOI.LOCALLY_SOLVED, [2.8, 0.0, 0.8, 1.2] + mock, config.optimal_status, [2.8, 0.0, 0.8, 1.2] ) ) - MOI.Test.test_linear_mcp(mock, config) + MOI.Test.test_linear_mixed_complementarity(mock, config) end From b47d0ebb65324db945f5092fdaf4073fd4d4b7a3 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Oct 2019 08:29:23 -0500 Subject: [PATCH 05/14] Fix optimal_status --- src/Test/nlp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 8c5e82ef69..226cc4ce78 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -372,7 +372,7 @@ function test_linear_mixed_complementarity(model::MOI.ModelLike, config::TestCon ) @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED + @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status x_val = MOI.get.(model, MOI.VariablePrimal(), x) @test isapprox( x_val, [2.8, 0.0, 0.8, 1.2], atol = config.atol, rtol = config.rtol From 04731689580dc9d3d0eab9c0f6786b2abfc068e1 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Oct 2019 09:41:06 -0500 Subject: [PATCH 06/14] Update examples in docstring --- src/sets.jl | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/sets.jl b/src/sets.jl index c4bcaa2528..d0ac71ffad 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -682,22 +682,38 @@ defines a complementarity constraint between the scalar function `F[i]` and the variable in `F[i + dimension]`. Thus, `F[i + dimension]` must be interpretable as a single variable `x_i` (e.g., `1.0 * x + 0.0`). -If the variable `x_i` is constrained in `Interval(lb, ub)`, then mathematically, -the mixed complementarity problem is to find a solution such that at least one -of the following holds: +If the variable `x_i` is constrained in `Interval(lb_i, ub_i)`, then +mathematically, the mixed complementarity problem is to find a solution such +that at least one of the following holds: - 1. F_i(x) = 0, lb <= x_i <= ub_i - 2. F_i(x) > 0, lb == x_i - 3. F_i(x) < 0, x_i == ub_i + 1. F_i(x) = 0, lb_i <= x_i <= ub_i + 2. F_i(x) > 0, lb_i == x_i + 3. F_i(x) < 0, x_i == ub_i Classically, the bounding set for `x_i` is `Interval(0, Inf)`, which recovers: 0 <= F_i(x) ⟂ x >= 0, where the `⟂` operator implies F_i(x) * x = 0. ### Examples - [x, y] -in- Complements(1) - [x, y, u, w] -in- Complements(2) +The problem: + + x -in- Interval(-1, 1) [2 * x - 3, x] -in- Complements(1) + +defines the mixed complementarity problem where at least one of the following +holds: + + 1. `2 * x - 3 = 0` if `-1 <= x <= 1` + 2. `2 * x - 3 > 0` if `x == -1` + 3. `2 * x - 3 < 0` if `x == 1` + +The problem: + + [x_3, x_4] -in- Nonnegatives(2) + [x_1, x_2, x_3, x_4] -in- Complements(2) + +defines the complementarity problem where `0 <= x_1 ⟂ x_3 >= 0` and +`0 <= x_2 ⟂ x_4 >= 0`. """ struct Complements <: AbstractVectorSet dimension::Int From a2a18f5878c87d83ba73dbffbadbbebfc1cbae47 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Oct 2019 09:43:50 -0500 Subject: [PATCH 07/14] More updates to docstring --- src/sets.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sets.jl b/src/sets.jl index d0ac71ffad..f9168e38de 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -686,12 +686,12 @@ If the variable `x_i` is constrained in `Interval(lb_i, ub_i)`, then mathematically, the mixed complementarity problem is to find a solution such that at least one of the following holds: - 1. F_i(x) = 0, lb_i <= x_i <= ub_i - 2. F_i(x) > 0, lb_i == x_i - 3. F_i(x) < 0, x_i == ub_i + 1. `F_i(x) = 0` if `lb_i <= x_i <= ub_i` + 2. `F_i(x) > 0` if `lb_i == x_i` + 3. `F_i(x) < 0` if `x_i == ub_i` Classically, the bounding set for `x_i` is `Interval(0, Inf)`, which recovers: -0 <= F_i(x) ⟂ x >= 0, where the `⟂` operator implies F_i(x) * x = 0. +`0 <= F_i(x) ⟂ x_i >= 0`, where the `⟂` operator implies `F_i(x) * x_i = 0`. ### Examples From 9abb78e70d9960ad229553915c16c0a1624696cb Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Oct 2019 10:38:30 -0500 Subject: [PATCH 08/14] Add Complements to docs --- docs/src/apireference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/apireference.md b/docs/src/apireference.md index 66616c14f2..81fc5acefd 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -349,6 +349,7 @@ DualPowerCone SOS1 SOS2 IndicatorSet +Complements ``` ### Matrix sets From d0b6f36178ef6d008e1aa56b5d3b579fd1b7d836 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 8 Oct 2019 13:29:42 -0500 Subject: [PATCH 09/14] Add AbstractVectorFunction to docs --- docs/src/apireference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/apireference.md b/docs/src/apireference.md index 81fc5acefd..969b8243f1 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -273,6 +273,7 @@ SettingSingleVariableFunctionNotAllowed List of recognized functions. ```@docs AbstractFunction +AbstractVectorFunction SingleVariable VectorOfVariables ScalarAffineTerm From f812d3495294001580ca0ba2873672caa9138cb3 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 10 Oct 2019 15:51:51 -0500 Subject: [PATCH 10/14] Tweak definition of complements --- src/sets.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sets.jl b/src/sets.jl index f9168e38de..5a2a4fa389 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -686,9 +686,9 @@ If the variable `x_i` is constrained in `Interval(lb_i, ub_i)`, then mathematically, the mixed complementarity problem is to find a solution such that at least one of the following holds: - 1. `F_i(x) = 0` if `lb_i <= x_i <= ub_i` - 2. `F_i(x) > 0` if `lb_i == x_i` - 3. `F_i(x) < 0` if `x_i == ub_i` + 1. `F_i(x) == 0` if `lb_i < x_i < ub_i` + 2. `F_i(x) >= 0` if `lb_i == x_i` + 3. `F_i(x) <= 0` if `x_i == ub_i` Classically, the bounding set for `x_i` is `Interval(0, Inf)`, which recovers: `0 <= F_i(x) ⟂ x_i >= 0`, where the `⟂` operator implies `F_i(x) * x_i = 0`. @@ -703,9 +703,9 @@ The problem: defines the mixed complementarity problem where at least one of the following holds: - 1. `2 * x - 3 = 0` if `-1 <= x <= 1` - 2. `2 * x - 3 > 0` if `x == -1` - 3. `2 * x - 3 < 0` if `x == 1` + 1. `2 * x - 3 == 0` if `-1 < x < 1` + 2. `2 * x - 3 >= 0` if `x == -1` + 3. `2 * x - 3 <= 0` if `x == 1` The problem: From 2bc4f898aca05662bed82da460f47dc4023d35cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Pacaud?= Date: Thu, 10 Oct 2019 22:57:34 +0200 Subject: [PATCH 11/14] Add QP complementarity test (#918) * Test: add QP complementarity test * Add Mock test for QP complementarity test * Fix with comments in PR#918 --- src/Test/nlp.jl | 102 ++++++++++++++++++++++++++++++++++++++++++++--- test/Test/nlp.jl | 8 ++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 226cc4ce78..4cf62a5956 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -371,16 +371,108 @@ function test_linear_mixed_complementarity(model::MOI.ModelLike, config::TestCon MOI.Complements(4) ) @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status - x_val = MOI.get.(model, MOI.VariablePrimal(), x) - @test isapprox( - x_val, [2.8, 0.0, 0.8, 1.2], atol = config.atol, rtol = config.rtol + if config.solve + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + x_val = MOI.get.(model, MOI.VariablePrimal(), x) + @test isapprox( + x_val, [2.8, 0.0, 0.8, 1.2], atol = config.atol, rtol = config.rtol + ) + end +end + +""" + test_qp_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) + +Test the solution of the quadratic mixed-complementarity problem: + +``` + min (x0 - 5)^2 +(2 x1 + 1)^2 + s.t. -1.5 x0 + 2 x1 + x2 - 0.5 x3 + x4 = 2 + x2 complements(3 x0 - x1 - 3) + x3 complements(-x0 + 0.5 x1 + 4) + x4 complements(-x0 - x1 + 7) + x0, x1, x2, x3, x4 >= 0 + +``` +which rewrites, with auxiliary variables + +``` + min (x0 - 5)^2 +(2 x1 + 1)^2 + s.t. -1.5 x0 + 2 x1 + x2 - 0.5 x3 + x4 = 2 (cf1) + 3 x0 - x1 - 3 - x5 = 0 (cf2) + -x0 + 0.5 x1 + 4 - x6 = 0 (cf3) + -x0 - x1 + 7 - x7 = 0 (cf4) + x2 complements x5 + x3 complements x6 + x4 complements x7 + x0, x1, x2, x3, x4, x5, x6, x7 >= 0 + +``` +""" +function test_qp_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) + MOI.empty!(model) + x = MOI.add_variables(model, 8) + MOI.set.(model, MOI.VariablePrimalStart(), x, 0.0) + for i in 1:8 + MOI.add_constraint(model, MOI.SingleVariable(x[i]), MOI.GreaterThan(0.0)) + end + + obj = MOI.ScalarQuadraticFunction( + [MOI.ScalarAffineTerm(-10.0, x[1]), MOI.ScalarAffineTerm(4.0, x[2])], + [MOI.ScalarQuadraticTerm(2.0, x[1], x[1]), MOI.ScalarQuadraticTerm(8.0, x[2], x[2])], + 26.0) + + MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj) + + cf1 = MOI.ScalarAffineFunction([ + MOI.ScalarAffineTerm(-1.5, x[1]), + MOI.ScalarAffineTerm(2.0, x[2]), + MOI.ScalarAffineTerm(1.0, x[3]), + MOI.ScalarAffineTerm(0.5, x[4]), + MOI.ScalarAffineTerm(1.0, x[5]) + ], 0.0) + cf2 = MOI.ScalarAffineFunction([ + MOI.ScalarAffineTerm(3.0, x[1]), + MOI.ScalarAffineTerm(-1.0, x[2]), + MOI.ScalarAffineTerm(-1.0, x[6]), + ], 0.0) + cf3 = MOI.ScalarAffineFunction([ + MOI.ScalarAffineTerm(-1.0, x[1]), + MOI.ScalarAffineTerm(0.5, x[2]), + MOI.ScalarAffineTerm(-1.0, x[7]), + ], 0.0) + cf4 = MOI.ScalarAffineFunction([ + MOI.ScalarAffineTerm(-1.0, x[1]), + MOI.ScalarAffineTerm(-1.0, x[2]), + MOI.ScalarAffineTerm(-1.0, x[8]), + ], 0.0) + + MOI.add_constraint(model, cf1, MOI.EqualTo(2.0)) + MOI.add_constraint(model, cf2, MOI.EqualTo(3.0)) + MOI.add_constraint(model, cf3, MOI.EqualTo(-4.0)) + MOI.add_constraint(model, cf4, MOI.EqualTo(-7.0)) + + MOI.add_constraint( + model, + MOI.VectorOfVariables([x[3], x[4], x[5], x[6], x[7], x[8]]), + MOI.Complements(3) ) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + if config.solve + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status + x_val = MOI.get.(model, MOI.VariablePrimal(), x) + @test isapprox( + x_val, [1.0, 0.0, 3.5, 0.0, 0.0, 0.0, 3.0, 6.0], atol = config.atol, rtol = config.rtol + ) + @test MOI.get(model, MOI.ObjectiveValue()) ≈ 17.0 atol=config.atol rtol=config.rtol + end end const complementaritytests = Dict( "linear_mixed_complementarity" => test_linear_mixed_complementarity, + "qp_mixed_complementarity" => test_qp_mixed_complementarity, ) @moitestset complementarity diff --git a/test/Test/nlp.jl b/test/Test/nlp.jl index 8085ec9cc7..1938d02a9e 100644 --- a/test/Test/nlp.jl +++ b/test/Test/nlp.jl @@ -16,4 +16,12 @@ const MOI = MathOptInterface ) ) MOI.Test.test_linear_mixed_complementarity(mock, config) + + MOI.Utilities.set_mock_optimize!( + mock, + (mock) -> MOI.Utilities.mock_optimize!( + mock, config.optimal_status, [1.0, 0.0, 3.5, 0.0, 0.0, 0.0, 3.0, 6.0] + ) + ) + MOI.Test.test_qp_mixed_complementarity(mock, config) end From e720cb512d3d175ddebb9e1717b010bb805c5562 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 11 Oct 2019 11:19:27 -0500 Subject: [PATCH 12/14] Update nlp.jl --- src/Test/nlp.jl | 61 +++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 4cf62a5956..6275259225 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -361,7 +361,7 @@ function test_linear_mixed_complementarity(model::MOI.ModelLike, config::TestCon iszero(M[i, j]) && continue push!( terms, - MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(M[i, j], x[j])) + MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(M[i, j], x[j])) ) end end @@ -421,38 +421,39 @@ function test_qp_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) obj = MOI.ScalarQuadraticFunction( [MOI.ScalarAffineTerm(-10.0, x[1]), MOI.ScalarAffineTerm(4.0, x[2])], [MOI.ScalarQuadraticTerm(2.0, x[1], x[1]), MOI.ScalarQuadraticTerm(8.0, x[2], x[2])], - 26.0) + 26.0 + ) MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj) - cf1 = MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(-1.5, x[1]), - MOI.ScalarAffineTerm(2.0, x[2]), - MOI.ScalarAffineTerm(1.0, x[3]), - MOI.ScalarAffineTerm(0.5, x[4]), - MOI.ScalarAffineTerm(1.0, x[5]) - ], 0.0) - cf2 = MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(3.0, x[1]), - MOI.ScalarAffineTerm(-1.0, x[2]), - MOI.ScalarAffineTerm(-1.0, x[6]), - ], 0.0) - cf3 = MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(-1.0, x[1]), - MOI.ScalarAffineTerm(0.5, x[2]), - MOI.ScalarAffineTerm(-1.0, x[7]), - ], 0.0) - cf4 = MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(-1.0, x[1]), - MOI.ScalarAffineTerm(-1.0, x[2]), - MOI.ScalarAffineTerm(-1.0, x[8]), - ], 0.0) - - MOI.add_constraint(model, cf1, MOI.EqualTo(2.0)) - MOI.add_constraint(model, cf2, MOI.EqualTo(3.0)) - MOI.add_constraint(model, cf3, MOI.EqualTo(-4.0)) - MOI.add_constraint(model, cf4, MOI.EqualTo(-7.0)) - + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.([-1.5, 2.0, 1.0, 0.5, 1.0], x[1:5]), 0.0 + ), + MOI.EqualTo(2.0) + ) + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.([3.0, -1.0, -1.0], x[[1, 2, 6]]), 0.0 + ), + MOI.EqualTo(3.0) + ) + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.([-1.0, 0.5, -1.0], x[[1, 2, 7]]), 0.0 + ), + MOI.EqualTo(-4.0) + ) + MOI.add_constraint( + model, + MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.(-1.0, x[[1, 2, 8]]), 0.0 + ), + MOI.EqualTo(-7.0) + ) MOI.add_constraint( model, MOI.VectorOfVariables([x[3], x[4], x[5], x[6], x[7], x[8]]), From 532cb68e3e66be787b1d79576c1b4a77ffd9a09d Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 14 Oct 2019 08:29:06 -0500 Subject: [PATCH 13/14] Re-word definition of Complements and break tests into CP and MPCC --- src/Test/nlp.jl | 60 +++++++++++++++++++++++++++++------------------- src/sets.jl | 8 +++---- test/Test/nlp.jl | 13 +++++++---- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 6275259225..151a06bc15 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -381,10 +381,16 @@ function test_linear_mixed_complementarity(model::MOI.ModelLike, config::TestCon end end +const mixed_complementaritytests = Dict( + "test_linear_mixed_complementarity" => test_linear_mixed_complementarity, +) + +@moitestset mixed_complementarity + """ - test_qp_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) + test_qp_complementarity_constraint(model::MOI.ModelLike, config::TestConfig) -Test the solution of the quadratic mixed-complementarity problem: +Test the solution of the quadratic program with complementarity constraints: ``` min (x0 - 5)^2 +(2 x1 + 1)^2 @@ -410,45 +416,45 @@ which rewrites, with auxiliary variables ``` """ -function test_qp_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) +function test_qp_complementarity_constraint( + model::MOI.ModelLike, config::TestConfig +) MOI.empty!(model) x = MOI.add_variables(model, 8) MOI.set.(model, MOI.VariablePrimalStart(), x, 0.0) - for i in 1:8 - MOI.add_constraint(model, MOI.SingleVariable(x[i]), MOI.GreaterThan(0.0)) - end - - obj = MOI.ScalarQuadraticFunction( - [MOI.ScalarAffineTerm(-10.0, x[1]), MOI.ScalarAffineTerm(4.0, x[2])], - [MOI.ScalarQuadraticTerm(2.0, x[1], x[1]), MOI.ScalarQuadraticTerm(8.0, x[2], x[2])], - 26.0 + MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.GreaterThan(0.0)) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), + MOI.ScalarQuadraticFunction( + MOI.ScalarAffineTerm.([-10.0, 4.0], x[[1, 2]]), + MOI.ScalarQuadraticTerm.([2.0, 8.0], x[1:2], x[1:2]), + 26.0 + ) ) - - MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj) - MOI.add_constraint( - model, + model, MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([-1.5, 2.0, 1.0, 0.5, 1.0], x[1:5]), 0.0 ), MOI.EqualTo(2.0) ) MOI.add_constraint( - model, + model, MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([3.0, -1.0, -1.0], x[[1, 2, 6]]), 0.0 ), MOI.EqualTo(3.0) ) MOI.add_constraint( - model, + model, MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([-1.0, 0.5, -1.0], x[[1, 2, 7]]), 0.0 ), MOI.EqualTo(-4.0) ) MOI.add_constraint( - model, + model, MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.(-1.0, x[[1, 2, 8]]), 0.0 ), @@ -465,15 +471,21 @@ function test_qp_mixed_complementarity(model::MOI.ModelLike, config::TestConfig) @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status x_val = MOI.get.(model, MOI.VariablePrimal(), x) @test isapprox( - x_val, [1.0, 0.0, 3.5, 0.0, 0.0, 0.0, 3.0, 6.0], atol = config.atol, rtol = config.rtol + x_val, + [1.0, 0.0, 3.5, 0.0, 0.0, 0.0, 3.0, 6.0], + atol = config.atol, rtol = config.rtol + ) + @test isapprox( + MOI.get(model, MOI.ObjectiveValue()), + 17.0, + atol = config.atol, + rtol = config.rtol ) - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 17.0 atol=config.atol rtol=config.rtol end end -const complementaritytests = Dict( - "linear_mixed_complementarity" => test_linear_mixed_complementarity, - "qp_mixed_complementarity" => test_qp_mixed_complementarity, +const math_program_complementarity_constraintstests = Dict( + "test_qp_complementarity_constraint" => test_qp_complementarity_constraint, ) -@moitestset complementarity +@moitestset math_program_complementarity_constraints diff --git a/src/sets.jl b/src/sets.jl index 5a2a4fa389..f9b76e7201 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -682,9 +682,8 @@ defines a complementarity constraint between the scalar function `F[i]` and the variable in `F[i + dimension]`. Thus, `F[i + dimension]` must be interpretable as a single variable `x_i` (e.g., `1.0 * x + 0.0`). -If the variable `x_i` is constrained in `Interval(lb_i, ub_i)`, then -mathematically, the mixed complementarity problem is to find a solution such -that at least one of the following holds: +The mixed complementarity problem consists of finding `x_i` in the interval +`[lb, ub]` (i.e., in the set `Interval(lb, ub)`), such that the following holds: 1. `F_i(x) == 0` if `lb_i < x_i < ub_i` 2. `F_i(x) >= 0` if `lb_i == x_i` @@ -700,8 +699,7 @@ The problem: x -in- Interval(-1, 1) [2 * x - 3, x] -in- Complements(1) -defines the mixed complementarity problem where at least one of the following -holds: +defines the mixed complementarity problem where the following holds: 1. `2 * x - 3 == 0` if `-1 < x < 1` 2. `2 * x - 3 >= 0` if `x == -1` diff --git a/test/Test/nlp.jl b/test/Test/nlp.jl index 1938d02a9e..ee8d94cf5e 100644 --- a/test/Test/nlp.jl +++ b/test/Test/nlp.jl @@ -3,25 +3,30 @@ using Test import MathOptInterface const MOI = MathOptInterface -@testset "complementarity" begin +@testset "mixed_complementarity" begin mock = MOI.Utilities.MockOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) ) config = MOI.Test.TestConfig(optimal_status = MOI.LOCALLY_SOLVED) - MOI.Utilities.set_mock_optimize!( mock, (mock) -> MOI.Utilities.mock_optimize!( mock, config.optimal_status, [2.8, 0.0, 0.8, 1.2] ) ) - MOI.Test.test_linear_mixed_complementarity(mock, config) + MOI.Test.mixed_complementaritytest(mock, config) +end +@testset "math_program_complementarity_constraints" begin + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + ) + config = MOI.Test.TestConfig(optimal_status = MOI.LOCALLY_SOLVED) MOI.Utilities.set_mock_optimize!( mock, (mock) -> MOI.Utilities.mock_optimize!( mock, config.optimal_status, [1.0, 0.0, 3.5, 0.0, 0.0, 0.0, 3.0, 6.0] ) ) - MOI.Test.test_qp_mixed_complementarity(mock, config) + MOI.Test.math_program_complementarity_constraintstest(mock, config) end From ad1d56b37114263935fabced4477aee05f1d562e Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 14 Oct 2019 08:32:37 -0500 Subject: [PATCH 14/14] Update example in Complements docstring --- src/Test/nlp.jl | 3 ++- src/sets.jl | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Test/nlp.jl b/src/Test/nlp.jl index 151a06bc15..8cf211cb5b 100644 --- a/src/Test/nlp.jl +++ b/src/Test/nlp.jl @@ -473,7 +473,8 @@ function test_qp_complementarity_constraint( @test isapprox( x_val, [1.0, 0.0, 3.5, 0.0, 0.0, 0.0, 3.0, 6.0], - atol = config.atol, rtol = config.rtol + atol = config.atol, + rtol = config.rtol ) @test isapprox( MOI.get(model, MOI.ObjectiveValue()), diff --git a/src/sets.jl b/src/sets.jl index f9b76e7201..ba5b36171c 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -697,15 +697,22 @@ Classically, the bounding set for `x_i` is `Interval(0, Inf)`, which recovers: The problem: x -in- Interval(-1, 1) - [2 * x - 3, x] -in- Complements(1) + [-4 * x - 3, x] -in- Complements(1) defines the mixed complementarity problem where the following holds: - 1. `2 * x - 3 == 0` if `-1 < x < 1` - 2. `2 * x - 3 >= 0` if `x == -1` - 3. `2 * x - 3 <= 0` if `x == 1` + 1. `-4 * x - 3 == 0` if `-1 < x < 1` + 2. `-4 * x - 3 >= 0` if `x == -1` + 3. `-4 * x - 3 <= 0` if `x == 1` -The problem: +There are three solutions: + + 1. `x = -3/4` with `F(x) = 0` + 2. `x = -1` with `F(x) = 1` + 3. `x = 1` with `F(x) = -7` + +The function `F` can also be defined in terms of single variables. For example, +the problem: [x_3, x_4] -in- Nonnegatives(2) [x_1, x_2, x_3, x_4] -in- Complements(2)