|
| 1 | +module TestQCQPExtra |
| 2 | + |
| 3 | +using Test |
| 4 | + |
| 5 | +import MathOptInterface as MOI |
| 6 | +import MultivariatePolynomials as MP |
| 7 | +import PolyJuMP |
| 8 | +import Random |
| 9 | + |
| 10 | +MOI.Utilities.@model( |
| 11 | + Model, |
| 12 | + (), |
| 13 | + (MOI.LessThan, MOI.GreaterThan, MOI.EqualTo, MOI.Interval), |
| 14 | + (), |
| 15 | + (), |
| 16 | + (), |
| 17 | + (MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction), |
| 18 | + (), |
| 19 | + (), |
| 20 | +) |
| 21 | + |
| 22 | +function MOI.supports( |
| 23 | + ::Model, |
| 24 | + ::MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}, |
| 25 | +) |
| 26 | + return false |
| 27 | +end |
| 28 | + |
| 29 | +function run_tests_e2e() |
| 30 | + for name in names(@__MODULE__; all = true) |
| 31 | + if startswith("$name", "test_e2e") |
| 32 | + @testset "$(name) $T" for T in [Int, Float64] |
| 33 | + getfield(@__MODULE__, name)(T) |
| 34 | + end |
| 35 | + end |
| 36 | + end |
| 37 | + return |
| 38 | +end |
| 39 | + |
| 40 | +function run_test_scalar_polynomial_function(xs, samples) |
| 41 | + @testset "qcqp extra $T" for T in [Float64, BigFloat] |
| 42 | + for i in 1:samples |
| 43 | + test_scalar_polynomial_function(xs, T) |
| 44 | + end |
| 45 | + end |
| 46 | + return |
| 47 | +end |
| 48 | + |
| 49 | +function run_tests_subs(xs, ys, samples, TS) |
| 50 | + Random.seed!(2024) |
| 51 | + for name in names(@__MODULE__; all = true) |
| 52 | + if startswith("$name", "test_subs") |
| 53 | + @testset "$(name) $T" for T in TS |
| 54 | + for i in 1:samples |
| 55 | + getfield(@__MODULE__, name)(xs, ys, T) |
| 56 | + end |
| 57 | + end |
| 58 | + end |
| 59 | + end |
| 60 | + return |
| 61 | +end |
| 62 | + |
| 63 | +function test_unconstrained_before_projection(T) |
| 64 | + inner = Model{T}() |
| 65 | + optimizer = MOI.Utilities.MockOptimizer(inner) |
| 66 | + model = PolyJuMP.JuMP.GenericModel{T}( |
| 67 | + () -> PolyJuMP.QCQP.Optimizer{T}(optimizer), |
| 68 | + ) |
| 69 | + PolyJuMP.@variable(model, -1 <= a[1:2] <= 1) |
| 70 | + PolyJuMP.@objective(model, Min, a[1]^2 * a[2]^2) |
| 71 | + PolyJuMP.optimize!(model) |
| 72 | + vis = MOI.get(inner, MOI.ListOfVariableIndices()) |
| 73 | + @test length(vis) == 4 |
| 74 | + F = MOI.ScalarQuadraticFunction{T} |
| 75 | + S = MOI.EqualTo{T} |
| 76 | + cis = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) |
| 77 | + @test length(cis) == 2 |
| 78 | + return |
| 79 | +end |
| 80 | + |
| 81 | +function test_unconstrained_after_projection(T) |
| 82 | + inner = Model{T}() |
| 83 | + optimizer = MOI.Utilities.MockOptimizer(inner) |
| 84 | + model = PolyJuMP.JuMP.GenericModel{T}( |
| 85 | + () -> PolyJuMP.QCQP.Optimizer{T}(optimizer), |
| 86 | + ) |
| 87 | + PolyJuMP.@variable(model, -1 <= a <= 1) |
| 88 | + PolyJuMP.@objective(model, Min, a^2) |
| 89 | + PolyJuMP.optimize!(model) |
| 90 | + vis = MOI.get(inner, MOI.ListOfVariableIndices()) |
| 91 | + @test length(vis) == 1 |
| 92 | + F = MOI.ScalarQuadraticFunction{T} |
| 93 | + S = MOI.EqualTo{T} |
| 94 | + cis = MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()) |
| 95 | + @test length(cis) == 0 |
| 96 | + return |
| 97 | +end |
| 98 | + |
| 99 | +function _random_polynomial(vars, T) |
| 100 | + ms = Random.shuffle(MP.monomials(vars, 1:length(vars))) |
| 101 | + return sum(ms[i] * T(randn()) for i in eachindex(ms) if rand(Bool)) |
| 102 | +end |
| 103 | + |
| 104 | +function test_subs!_preserves_moi_sync(xs, ys, T) |
| 105 | + p = _random_polynomial(xs, T) |
| 106 | + mois = MOI.VariableIndex.(eachindex(xs)) |
| 107 | + vals = T.(randn(length(mois))) |
| 108 | + mask = rand(Bool, length(xs)) |
| 109 | + is = Random.shuffle(eachindex(xs)[mask]) |
| 110 | + index = Dict{eltype(mois),eltype(xs)}(zip(mois[is], ys[is])) |
| 111 | + moi_map = Dict(zip(xs, mois)) |
| 112 | + moivars = [moi_map[v] for v in MP.variables(p)] |
| 113 | + before = PolyJuMP.ScalarPolynomialFunction(p, moivars) |
| 114 | + after, _ = PolyJuMP.QCQP._subs!(before, index) |
| 115 | + bmap = [vals[v.value] for v in before.variables] |
| 116 | + amap = [vals[v.value] for v in after.variables] |
| 117 | + bvalue = before.polynomial(MP.variables(before.polynomial) => bmap) |
| 118 | + avalue = after.polynomial(MP.variables(after.polynomial) => amap) |
| 119 | + # avoid verbose fails |
| 120 | + @test isapprox(Float64(bvalue), Float64(avalue)) |
| 121 | + return |
| 122 | +end |
| 123 | + |
| 124 | +function test_scalar_polynomial_function(xs, T) |
| 125 | + pick = rand(eachindex(xs)) |
| 126 | + ids = Random.shuffle(eachindex(xs)) |
| 127 | + poly = sum(T(randn()) * xs[i] for i in ids if i != pick) |
| 128 | + mois = MOI.VariableIndex.(eachindex(xs)) |
| 129 | + moi_to_vars = Dict(zip(mois, xs)) |
| 130 | + spf = PolyJuMP._scalar_polynomial(moi_to_vars, Any, poly) |
| 131 | + expected = MP.variables(poly) |
| 132 | + actual = [moi_to_vars[m] for m in spf.variables] |
| 133 | + @test length(MP.variables(spf.polynomial)) == length(spf.variables) |
| 134 | + @test expected == actual |
| 135 | + return |
| 136 | +end |
| 137 | + |
| 138 | +end # module |
| 139 | + |
| 140 | +using Test |
| 141 | + |
| 142 | +@testset "TestQCQPFinalTouch" begin |
| 143 | + TestQCQPExtra.run_tests_e2e() |
| 144 | +end |
| 145 | + |
| 146 | +import DynamicPolynomials |
| 147 | +@testset "DynamicPolynomials" begin |
| 148 | + ids = 1:4 |
| 149 | + DynamicPolynomials.@polyvar(x[ids]) |
| 150 | + DynamicPolynomials.@polyvar(y[ids]) |
| 151 | + samples = 10 |
| 152 | + types = [Float64, BigFloat] # Rational fails with DynamicPolynomials |
| 153 | + TestQCQPExtra.run_tests_subs(x, y, samples, types) |
| 154 | + TestQCQPExtra.run_test_scalar_polynomial_function(x, samples) |
| 155 | +end |
| 156 | + |
| 157 | +import TypedPolynomials |
| 158 | +@testset "TypedPolynomials" begin |
| 159 | + TypedPolynomials.@polyvar(z[1:4]) |
| 160 | + TypedPolynomials.@polyvar(w[1:4]) |
| 161 | + types = [Float64, BigFloat, Rational{BigInt}] |
| 162 | + samples = 10 |
| 163 | + TestQCQPExtra.run_tests_subs(z, w, samples, types) |
| 164 | +end |
0 commit comments