From 78edb39c2a11fb9a92f4c57662334f1158d205cd Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 24 Jun 2026 16:42:03 -0400 Subject: [PATCH 1/3] Inital addition of MOI.SecondOrderCone, MOI.RotatedSecondOrderCone, MOI.ExponentialCone, MOI.PowerCone as supported types --- Project.toml | 6 +++- src/bigm.jl | 44 +++++++++++++++++++++++ src/constraints.jl | 5 ++- src/hull.jl | 18 ++++++++++ test/constraints/bigm.jl | 76 ++++++++++++++++++++++++++++++++++++++++ test/constraints/hull.jl | 59 +++++++++++++++++++++++++++++++ test/solve.jl | 36 ++++++++++++++++++- 7 files changed, 241 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index bdec9be4..6bfe0db2 100644 --- a/Project.toml +++ b/Project.toml @@ -21,6 +21,8 @@ julia = "1.10" Juniper = "0.9.3" Ipopt = "1.9.0" InfiniteOpt = "0.6.3" +Hypatia = "0.10" +Pajarito = "0.8" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" @@ -28,6 +30,8 @@ HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" +Hypatia = "b99e6be6-89ff-11e8-14f8-45c827f4f8f2" +Pajarito = "2f354839-79df-5901-9f0a-cdb2aac6fe30" [targets] -test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "InfiniteOpt"] +test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "InfiniteOpt", "Hypatia", "Pajarito"] diff --git a/src/bigm.jl b/src/bigm.jl index 81386b3f..facad11a 100644 --- a/src/bigm.jl +++ b/src/bigm.jl @@ -251,3 +251,47 @@ function reformulate_disjunct_constraint( reform_con_np = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(con.set.dimension)) return [reform_con_nn, reform_con_np] end + +################################################################################ +# BIG-M FOR CONIC CONSTRAINTS +################################################################################ +# Big-M for `func in K`: add slack `M*(1 - y)*d` along a fixed interior +# direction `d` of the cone, so `func + M*d in K`. + +# SOC: (t, x...) with t >= ||x||. d = e1 = (1, 0, ...); interior since +# 1 > ||0|| = 0. Bumping t alone makes (t + M) >= ||x|| hold for big M. +_conic_bigm_direction(set::_MOI.SecondOrderCone) = + [i == 1 ? 1.0 : 0.0 for i in 1:_MOI.dimension(set)] +# Rotated SOC: (t, u, x...) with 2*t*u >= ||x||^2, t,u >= 0. d = +# (1,1,0,...); interior since 2*1*1 = 2 > 0. Both t,u must grow so +# 2(t+M)(u+M) ~ 2*M^2 dominates ||x||^2; bumping one leaves 2*t*0 = 0. +_conic_bigm_direction(set::_MOI.RotatedSecondOrderCone) = + [i <= 2 ? 1.0 : 0.0 for i in 1:_MOI.dimension(set)] +# Exp cone: (x, y, z) with z >= y*exp(x/y), y >= 0. d = (0, 1, 2); +# interior since 2 > 1*exp(0) = 1. As M grows, exp(x/(y+M)) -> 1 so the +# RHS ~ y + M ~ M while z grows as 2M; the factor 2 keeps z above it. +_conic_bigm_direction(::_MOI.ExponentialCone) = [0.0, 1.0, 2.0] +# Power cone: (x, y, z) with x^a * y^(1-a) >= |z|, x,y >= 0, a the cone +# exponent. d = (1,1,0); interior since 1^a * 1^(1-a) = 1 > 0. Bumping +# x,y makes (x+M)^a (y+M)^(1-a) ~ M dominate the fixed |z|; z untouched. +_conic_bigm_direction(::_MOI.PowerCone) = [1.0, 1.0, 0.0] + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::BigM +) where { + T <: Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + S <: Union{_MOI.SecondOrderCone, _MOI.RotatedSecondOrderCone, + _MOI.ExponentialCone, _MOI.PowerCone}, + R +} + M = method.value + d = _conic_bigm_direction(con.set) + new_func = JuMP.@expression(model, [i=1:_MOI.dimension(con.set)], + con.func[i] + M*(1 - bvref)*d[i] + ) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end diff --git a/src/constraints.jl b/src/constraints.jl index a25270d4..bec5c5ed 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -210,7 +210,10 @@ end # Allows for building DisjunctConstraints for VectorConstraints since these get parsed differently by JuMP (JuMP changes the set to a MOI.AbstractScalarSet) for SetType in ( JuMP.Nonnegatives, JuMP.Nonpositives, JuMP.Zeros, - _MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros + _MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros, + JuMP.SecondOrderCone, JuMP.RotatedSecondOrderCone, + _MOI.SecondOrderCone, _MOI.RotatedSecondOrderCone, + _MOI.ExponentialCone, _MOI.PowerCone ) @eval begin @doc """ diff --git a/src/hull.jl b/src/hull.jl index 779369e9..e94bd8bc 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -281,6 +281,24 @@ function reformulate_disjunct_constraint( reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::_Hull +) where { + T <: Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + S <: Union{_MOI.SecondOrderCone, _MOI.RotatedSecondOrderCone, + _MOI.ExponentialCone, _MOI.PowerCone}, + R +} + new_func = JuMP.@expression(model, [i=1:_MOI.dimension(con.set)], + disaggregate_expression(model, con.func[i], bvref, method) + ) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, diff --git a/test/constraints/bigm.jl b/test/constraints/bigm.jl index ac1281c9..7b5d8491 100644 --- a/test/constraints/bigm.jl +++ b/test/constraints/bigm.jl @@ -313,6 +313,77 @@ function test_extension_bigm() @test refcons[4].set == MOI.GreaterThan(10.0 - 110) end +function test_soc_bigm() + model = GDPModel() + @variable(model, x) + @variable(model, t) + @variable(model, y, Logical) + @constraint(model, con, [t, x] in SecondOrderCone(), Disjunct(y)) + bvref = binary_variable(y) + ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) + @test length(ref) == 1 + @test isequal_canonical(ref[1].func[1], t + 100*(1 - bvref)) + @test isequal_canonical(ref[1].func[2], 1.0*x) + @test ref[1].set == MOI.SecondOrderCone(2) +end + +function test_rsoc_bigm() + model = GDPModel() + @variable(model, x) + @variable(model, t) + @variable(model, y, Logical) + @constraint(model, con, [0.5, t, x] in RotatedSecondOrderCone(), Disjunct(y)) + bvref = binary_variable(y) + ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) + @test length(ref) == 1 + @test isequal_canonical(ref[1].func[1], 0.5 + 100*(1 - bvref)) + @test isequal_canonical(ref[1].func[2], t + 100*(1 - bvref)) + @test isequal_canonical(ref[1].func[3], 1.0*x) + @test ref[1].set == MOI.RotatedSecondOrderCone(3) +end + +function test_exp_bigm() + model = GDPModel() + @variable(model, x) + @variable(model, t) + @variable(model, w) + @variable(model, y, Logical) + @constraint(model, con, [x, t, w] in MOI.ExponentialCone(), Disjunct(y)) + bvref = binary_variable(y) + ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) + @test length(ref) == 1 + @test isequal_canonical(ref[1].func[1], 1.0*x) + @test isequal_canonical(ref[1].func[2], t + 100*(1 - bvref)) + @test isequal_canonical(ref[1].func[3], w + 200*(1 - bvref)) + @test ref[1].set == MOI.ExponentialCone() +end + +function test_power_bigm() + model = GDPModel() + @variable(model, x) + @variable(model, t) + @variable(model, w) + @variable(model, y, Logical) + @constraint(model, con, [x, t, w] in MOI.PowerCone(0.5), Disjunct(y)) + bvref = binary_variable(y) + ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) + @test length(ref) == 1 + @test isequal_canonical(ref[1].func[1], x + 100*(1 - bvref)) + @test isequal_canonical(ref[1].func[2], t + 100*(1 - bvref)) + @test isequal_canonical(ref[1].func[3], 1.0*w) + @test ref[1].set == MOI.PowerCone(0.5) +end + +function test_conic_bigm_inf_error() + model = GDPModel() + @variable(model, x) + @variable(model, t) + @variable(model, y, Logical) + @constraint(model, con, [t, x] in SecondOrderCone(), Disjunct(y)) + bvref = binary_variable(y) + @test_throws ErrorException reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(Inf, false)) +end + @testset "BigM Reformulation" begin test_default_bigm() test_default_tighten_bigm() @@ -333,4 +404,9 @@ end test_zeros_bigm() test_nested_bigm() test_extension_bigm() + test_soc_bigm() + test_rsoc_bigm() + test_exp_bigm() + test_power_bigm() + test_conic_bigm_inf_error() end \ No newline at end of file diff --git a/test/constraints/hull.jl b/test/constraints/hull.jl index ddc5d85e..b10e3375 100644 --- a/test/constraints/hull.jl +++ b/test/constraints/hull.jl @@ -662,6 +662,62 @@ function test_extension_hull() # TODO add more tests end +function test_vector_soc_hull() + model = GDPModel() + @variable(model, 10 <= x <= 100) + @variable(model, 10 <= t <= 100) + @variable(model, z, Logical) + @constraint(model, con, [t - 5, x - 5] in SecondOrderCone(), Disjunct(z)) + zbin = variable_by_name(model, "z") + method = DP._Hull(Hull(1e-3), Set([x, t])) + prep_bounds([x, t], model, Hull()) + @test DP._disaggregate_variables(model, z, Set([x, t]), method) isa Nothing + x_z = variable_by_name(model, "x_z") + t_z = variable_by_name(model, "t_z") + ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) + @test length(ref) == 1 + @test ref[1].func == [t_z - 5*zbin, x_z - 5*zbin] + @test ref[1].set == MOI.SecondOrderCone(2) +end + +function test_vector_rsoc_hull() + model = GDPModel() + @variable(model, 10 <= x <= 100) + @variable(model, 10 <= t <= 100) + @variable(model, z, Logical) + @constraint(model, con, [0.5, t - 2, x - 3] in RotatedSecondOrderCone(), Disjunct(z)) + zbin = variable_by_name(model, "z") + method = DP._Hull(Hull(1e-3), Set([x, t])) + prep_bounds([x, t], model, Hull()) + @test DP._disaggregate_variables(model, z, Set([x, t]), method) isa Nothing + x_z = variable_by_name(model, "x_z") + t_z = variable_by_name(model, "t_z") + ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) + @test length(ref) == 1 + @test ref[1].func == [0.5*zbin, t_z - 2*zbin, x_z - 3*zbin] + @test ref[1].set == MOI.RotatedSecondOrderCone(3) +end + +function test_vector_exp_hull() + model = GDPModel() + @variable(model, 10 <= x <= 100) + @variable(model, 10 <= t <= 100) + @variable(model, 10 <= w <= 100) + @variable(model, z, Logical) + @constraint(model, con, [x - 1, t - 1, w - 1] in MOI.ExponentialCone(), Disjunct(z)) + zbin = variable_by_name(model, "z") + method = DP._Hull(Hull(1e-3), Set([x, t, w])) + prep_bounds([x, t, w], model, Hull()) + @test DP._disaggregate_variables(model, z, Set([x, t, w]), method) isa Nothing + x_z = variable_by_name(model, "x_z") + t_z = variable_by_name(model, "t_z") + w_z = variable_by_name(model, "w_z") + ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) + @test length(ref) == 1 + @test ref[1].func == [x_z - zbin, t_z - zbin, w_z - zbin] + @test ref[1].set == MOI.ExponentialCone() +end + @testset "Hull Reformulation" begin test_default_hull() test_set_hull() @@ -703,4 +759,7 @@ end test_scalar_nonlinear_hull_2sided_error() test_exactly1_error() test_extension_hull() + test_vector_soc_hull() + test_vector_rsoc_hull() + test_vector_exp_hull() end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index c3ca9441..7e0bc5ea 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -1,4 +1,4 @@ -using HiGHS, Ipopt, Juniper +using HiGHS, Ipopt, Juniper, Pajarito, Hypatia function test_linear_gdp_example(m, use_complements = false) set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) @@ -168,3 +168,37 @@ end test_quadratic_gdp_example() test_generic_model(GDPModel{Float32}(mockoptimizer)) end + +function test_conic_gdp_example() + # Circle-containment idea from Bernal Neira & Grossmann (2021), + # Eq. 4.3: the point (x, y) must lie in the unit circle around + # (0, 0) OR around (5, 0), expressed as second-order cones. The + # disjunction picks one circle; minimizing x selects the first and + # lands on its leftmost point, (x, y) = (-1, 0). + oa = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) + cs = optimizer_with_attributes(Hypatia.Optimizer, MOI.Silent() => true) + paj = optimizer_with_attributes(Pajarito.Optimizer, + "oa_solver" => oa, "conic_solver" => cs, "verbose" => false) + for meth in (BigM(100), Hull()) + m = GDPModel(paj) + set_attribute(m, MOI.Silent(), true) + @variable(m, -10 <= x <= 10) + @variable(m, -10 <= y <= 10) + @variable(m, Y[1:2], Logical) + @objective(m, Min, x) + @constraint(m, c1, [1, x, y] in SecondOrderCone(), Disjunct(Y[1])) + @constraint(m, c2, [1, x - 5, y] in SecondOrderCone(), Disjunct(Y[2])) + @disjunction(m, Y) + @test optimize!(m, gdp_method = meth) isa Nothing + @test termination_status(m) == MOI.OPTIMAL + @test isapprox(objective_value(m), -1, atol = 1e-4) + @test isapprox(value(x), -1, atol = 1e-4) + @test isapprox(value(y), 0, atol = 1e-4) + @test value(Y[1]) + @test !value(Y[2]) + end +end + +@testset "Solve Conic GDP" begin + test_conic_gdp_example() +end From 984a91930ba1dc0d852ab79a238041045295c9a1 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 24 Jun 2026 17:21:29 -0400 Subject: [PATCH 2/3] Fix tests --- test/constraints/bigm.jl | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/constraints/bigm.jl b/test/constraints/bigm.jl index 7b5d8491..16a387ab 100644 --- a/test/constraints/bigm.jl +++ b/test/constraints/bigm.jl @@ -374,16 +374,6 @@ function test_power_bigm() @test ref[1].set == MOI.PowerCone(0.5) end -function test_conic_bigm_inf_error() - model = GDPModel() - @variable(model, x) - @variable(model, t) - @variable(model, y, Logical) - @constraint(model, con, [t, x] in SecondOrderCone(), Disjunct(y)) - bvref = binary_variable(y) - @test_throws ErrorException reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(Inf, false)) -end - @testset "BigM Reformulation" begin test_default_bigm() test_default_tighten_bigm() @@ -408,5 +398,4 @@ end test_rsoc_bigm() test_exp_bigm() test_power_bigm() - test_conic_bigm_inf_error() end \ No newline at end of file From 3a83a41601682c80eb8c71d07f85e6c9188016b3 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 26 Jun 2026 12:27:52 -0400 Subject: [PATCH 3/3] _ConicSets type --- src/bigm.jl | 4 +--- src/constraints.jl | 10 ++++++++++ src/hull.jl | 4 +--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/bigm.jl b/src/bigm.jl index facad11a..4e2228a4 100644 --- a/src/bigm.jl +++ b/src/bigm.jl @@ -283,9 +283,7 @@ function reformulate_disjunct_constraint( method::BigM ) where { T <: Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, - S <: Union{_MOI.SecondOrderCone, _MOI.RotatedSecondOrderCone, - _MOI.ExponentialCone, _MOI.PowerCone}, - R + S <: _ConicSets, R } M = method.value d = _conic_bigm_direction(con.set) diff --git a/src/constraints.jl b/src/constraints.jl index bec5c5ed..2bcecc72 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -207,6 +207,16 @@ function JuMP.build_constraint( return _DisjunctConstraint(constr, tag.indicator) end +# MOI conic sets handled by the BigM and Hull conic reformulations +# (Bernal Neira & Grossmann 2021). The affine map A*x - b sits inside +# the cone; the nonlinearity is carried by the cone itself. +const _ConicSets = Union{ + _MOI.SecondOrderCone, + _MOI.RotatedSecondOrderCone, + _MOI.ExponentialCone, + _MOI.PowerCone, +} + # Allows for building DisjunctConstraints for VectorConstraints since these get parsed differently by JuMP (JuMP changes the set to a MOI.AbstractScalarSet) for SetType in ( JuMP.Nonnegatives, JuMP.Nonpositives, JuMP.Zeros, diff --git a/src/hull.jl b/src/hull.jl index e94bd8bc..9066826e 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -289,9 +289,7 @@ function reformulate_disjunct_constraint( method::_Hull ) where { T <: Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, - S <: Union{_MOI.SecondOrderCone, _MOI.RotatedSecondOrderCone, - _MOI.ExponentialCone, _MOI.PowerCone}, - R + S <: _ConicSets, R } new_func = JuMP.@expression(model, [i=1:_MOI.dimension(con.set)], disaggregate_expression(model, con.func[i], bvref, method)