Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exponential Cone #29

Merged
merged 33 commits into from
Dec 27, 2020
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2fb3701
Merge pull request #1 from matbesancon/master
tjdiamandis Dec 23, 2020
906e784
add NonlinearSolve dep
tjdiamandis Dec 23, 2020
45bac58
add NonlinearSolve
tjdiamandis Dec 23, 2020
f21cded
add Random, JuMP, SCS for tests
tjdiamandis Dec 23, 2020
052dbe1
add exp cone projection
tjdiamandis Dec 23, 2020
ba899b8
add exp cone proj tests
tjdiamandis Dec 23, 2020
4bf77c4
merge changes
tjdiamandis Dec 23, 2020
3ce2c5e
comment out error message
tjdiamandis Dec 23, 2020
7ead9ad
derivatives for exp cone
tjdiamandis Dec 23, 2020
48d3fcf
tests for Dproj_Kexp
tjdiamandis Dec 23, 2020
52fd09f
add check for bisection interval
tjdiamandis Dec 23, 2020
1988359
remove link
tjdiamandis Dec 23, 2020
cc830fc
update to use _check_dimension
tjdiamandis Dec 23, 2020
2070ab1
move JuMP, SCS to test only
tjdiamandis Dec 23, 2020
b1e922e
check dim with _check_dimension
tjdiamandis Dec 23, 2020
773bf8c
fix loading of SCS and JuMP test dep
tjdiamandis Dec 23, 2020
82fc6f9
remove NonlinearSolve dep
tjdiamandis Dec 24, 2020
b1fa666
use distance_to_set to check membership
tjdiamandis Dec 24, 2020
4e12347
shortcircuit cases for exp cone
tjdiamandis Dec 24, 2020
a61e4ee
switch back to _in_exp_cone() from dist
tjdiamandis Dec 25, 2020
6c275a1
diagm -> Diagonal
tjdiamandis Dec 25, 2020
3912710
Update src/distance_sets.jl
tjdiamandis Dec 25, 2020
276ca15
Update src/distance_sets.jl
tjdiamandis Dec 25, 2020
7f37235
Update src/projections.jl - code review
tjdiamandis Dec 25, 2020
c97645a
Update src/projections.jl - code review
tjdiamandis Dec 25, 2020
25d8518
update bisection rootfinding
tjdiamandis Dec 25, 2020
1b24cf6
remove print on error
tjdiamandis Dec 25, 2020
f7688e1
Update src/projections.jl -- code review
tjdiamandis Dec 25, 2020
9a1f470
fix types -- code review
tjdiamandis Dec 25, 2020
dc2606a
fix types -- code review
tjdiamandis Dec 25, 2020
b547f28
Update src/utils.jl -- code review
tjdiamandis Dec 25, 2020
aee7112
_in_exp for grad; fix return type
tjdiamandis Dec 25, 2020
edad4d4
merge
tjdiamandis Dec 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b"
FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

Expand All @@ -23,7 +24,9 @@ julia = "1"

[extras]
ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont we need Random here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw that Random was already in the package dependencies, but I don't think it's used outside of tests. I can move it to extras, unless there's something I'm missing.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that in a following PR, we can leave it in the deps for now


[targets]
test = ["Test", "ChainRulesTestUtils"]
test = ["Test", "ChainRulesTestUtils", "JuMP", "SCS"]
1 change: 1 addition & 0 deletions src/MathOptSetDistances.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ using LinearAlgebra
import ChainRulesCore
const CRC = ChainRulesCore
import FiniteDifferences
import NonlinearSolve

export distance_to_set, projection_on_set, projection_gradient_on_set

Expand Down
169 changes: 169 additions & 0 deletions src/projections.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# find expression of projections on cones and their derivatives here:
# https://stanford.edu/~boyd/papers/pdf/cone_prog_refine.pdf
# See also
# https://github.com/tjdiamandis/ConeProgramDiff.jl/blob/main/cone_ref.pdf
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
# and references therein
const EXP_CONE_THRESH = 1e-8
const POW_CONE_THRESH = 1e-8


"""
projection_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.Zeros) where {T}
Expand Down Expand Up @@ -152,6 +158,116 @@ function vec_symm(X)
return X[LinearAlgebra.triu(trues(size(X)))]
end

"""
projection_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.ExponentialCone) where {T}

projection of vector `v` on closure of the exponential cone
i.e. `cl(Kexp) = {(x,y,z) | y e^(x/y) <= z, y>0 } U {(x,y,z)| x <= 0, y = 0, z >= 0}`.

References:
* [Proximal Algorithms, 6.3.4](https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf)
by Neal Parikh and Stephen Boyd.
* [Projection, presolve in MOSEK: exponential, and power cones]
(https://docs.mosek.com/slides/2018/ismp2018/ismp-friberg.pdf) by Henrik Friberg
"""
function projection_on_set(::DefaultDistance, v::AbstractVector{T}, s::MOI.ExponentialCone) where {T}
_check_dimension(v, s)

tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
if _in_exp_cone(v; dual=false)
return v
elseif _in_exp_cone(-v; dual=true)
# if in polar cone Ko = -K*
return zeros(3)
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
elseif v[1] <= 0 && v[2] <= 0
return [v[1]; 0.0; max(v[3],0)]
else
return _exp_cone_proj_case_4(v)
end
end

function _in_exp_cone(v::AbstractVector{T}; dual=false) where {T}
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
# See pg. 184 https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf
# TODO: Tol for == 0 to avoid denom blowing up? see case 4 in deriv
if dual
return (
(v[1] == 0 && v[2] >= 0 && v[3] >= 0) ||
(v[1] < 0 && v[1]*exp(v[2]/v[1]) + ℯ*v[3] >= EXP_CONE_THRESH)
)
else
return (
(v[1] <= 0 && v[2] == 0 && v[3] >= 0) ||
(v[2] > 0 && v[2] * exp(v[1] / v[2]) - v[3] <= EXP_CONE_THRESH)
)
end
end

function _exp_cone_proj_case_4(v::AbstractVector{T}) where {T}
# https://docs.mosek.com/slides/2018/ismp2018/ismp-friberg.pdf
# Thm: h(x) is smooth, strictly increasing, and changes sign on domain
r, s, t = v[1], v[2], v[3]
h(x,p) = (((x-1)*r + s) * exp(x) - (r - x*s)*exp(-x))/(x^2 - x + 1) - t

# Note: won't both be Inf by case 3 of projection
lb = r > 0 ? 1 - s/r : -Inf
ub = s > 0 ? r/s : Inf

# Deal with ±Inf bounds
if isinf(lb)
lb = min(ub-0.125, -0.125)
for _ in 1:10
h(lb, nothing) < 0 && break
ub = lb
lb *= 2
end
elseif isinf(ub)
ub = max(lb+0.125, 0.125)
for _ in 1:10
h(ub, nothing) > 0 && break
lb = ub
ub *= 2
end
end

if !(h(lb, nothing) < 0 && h(ub, nothing) > 0)
error("Failure to find bracketing interval for exp cone")
end

# Solve with Bisection
prob = NonlinearSolve.NonlinearProblem(h, (lb, ub))
sol = NonlinearSolve.solve(prob, NonlinearSolve.Bisection())
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved

if sol.retcode == NonlinearSolve.MAXITERS_EXCEED
error("Numerical error in exp cone projection")
elseif sol.retcode == NonlinearSolve.FLOATING_POINT_LIMIT
# left == mid or right == mid, and (left, right) still bracketing
x = (sol.left + sol.right)/2
elseif sol.retcode == NonlinearSolve.EXACT_SOLUTION_LEFT
x = sol.left
elseif sol.retcode == NonlinearSolve.EXACT_SOLUTION_RIGHT
x = sol.right
else
error("NonlinearSolve.jl retcode not recognized in exp cone")
end

return ((x - 1)*r + s)/(x^2 - x + 1) * [x; 1.0; exp(x)]
end

"""
projection_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.DualExponentialCone) where {T}

projection of vector `v` on the dual exponential cone
i.e. `Kexp^* = {(u,v,w) | u < 0, -u*exp(v/u) <= ew } U {(u,v,w)| u == 0, v >= 0, w >= 0}`.

References:
* [Proximal Algorithms, 6.3.4](https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf)
by Neal Parikh and Stephen Boyd.
* [Projection, presolve in MOSEK: exponential, and power cones](https://docs.mosek.com/slides/2018/ismp2018/ismp-friberg.pdf)
by Henrik Friberg
"""
function projection_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.DualExponentialCone) where {T}
return v + projection_on_set(DefaultDistance(), -v, MOI.ExponentialCone())
end
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved

"""
projection_on_set(::DefaultDistance, v::AbstractVector{T}, sets::Array{<:MOI.AbstractSet})

Expand Down Expand Up @@ -295,6 +411,59 @@ function projection_gradient_on_set(::DefaultDistance, v::AbstractVector{T}, ::M
return D
end

"""
projection_gradient_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.ExponentialCone) where {T}

derivative of projection of vector `v` on closure of the exponential cone,
i.e. `cl(Kexp) = {(x,y,z) | y e^(x/y) <= z, y>0 } U {(x,y,z)| x <= 0, y = 0, z >= 0}`.

References:
* [Solution Refinement at Regular Points of Conic Problems](https://stanford.edu/~boyd/papers/cone_prog_refine.html)
by Enzo Busseti, Walaa M. Moursi, and Stephen Boyd
"""
function projection_gradient_on_set(::DefaultDistance, v::AbstractVector{T}, s::MOI.ExponentialCone) where {T}
_check_dimension(v, s)
Ip(z) = z >= 0 ? 1 : 0

if _in_exp_cone(v; dual=false)
return Matrix{Float64}(I, 3, 3)
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
elseif _in_exp_cone(-v; dual=true)
# if in polar cone Ko = -K*
return zeros(3,3) #FillArrays.Zeros(3, 3)
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
elseif v[1] <= 0 && v[2] <= 0
return diagm([1; Ip(v[2]); Ip(v[3])])
else
z1, z2, z3 = _exp_cone_proj_case_4(v)
nu = z3 - v[3]
rs = z1/z2
exp_rs = exp(rs)

mat = inv([
1+nu*exp_rs/z2 -nu*exp_rs*rs/z2 0 exp_rs;
-nu*exp_rs*rs/z2 1+nu*exp_rs*rs^2/z2 0 (1-rs)*exp_rs;
0 0 1 -1
exp_rs (1-rs)*exp_rs -1 0
])
return @view(mat[1:3,1:3])
end
end

"""
projection_gradient_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.DualExponentialCone) where {T}

derivative of projection of vector `v` on the dual exponential cone,
i.e. `Kexp^* = {(u,v,w) | u < 0, -u*exp(v/u) <= ew } U {(u,v,w)| u == 0, v >= 0, w >= 0}`.

References:
* [Solution Refinement at Regular Points of Conic Problems]
(https://stanford.edu/~boyd/papers/cone_prog_refine.html)
by Enzo Busseti, Walaa M. Moursi, and Stephen Boyd
"""
function projection_gradient_on_set(::DefaultDistance, v::AbstractVector{T}, ::MOI.DualExponentialCone) where {T}
# from Moreau decomposition: x = P_K(x) + P_-K*(x)
return I - projection_gradient_on_set(DefaultDistance(), -v, MOI.ExponentialCone())
end

"""
projection_gradient_on_set(::DefaultDistance, v::AbstractVector{T}, sets::Array{<:MOI.AbstractSet})

Expand Down
55 changes: 55 additions & 0 deletions test/projection_gradients.jl
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,59 @@ end
@test ≈(dfor, dΠ, atol=1e-5) || ≈(dback, dΠ, atol=1e-5)
end
end

@testset "Exp Cone" begin
function det_case_exp_cone(v; dual=false)
v = dual ? -v : v
if MOD._in_exp_cone(v; dual=false)
return 1
elseif MOD._in_exp_cone(-v; dual=true)
return 2
elseif v[1] <= 0 && v[2] <= 0 #TODO: threshold here??
return 3
else
return 4
end
end

Random.seed!(0)
s = MOI.ExponentialCone()
sd = MOI.DualExponentialCone()
case_p = zeros(4)
case_d = zeros(4)
# Adjust tolerance down because a 1-2 errors when projection ends up
# very close to the z axis
# For intuition, see Fig 5.1 https://docs.mosek.com/modeling-cookbook/expo.html
# Note that their order is reversed: (x, y, z) = (x3, x2, x1) [theirs]
tol = 1e-6
for ii in 1:100
v = 5*randn(3)
@testset "Primal Cone" begin
case_p[det_case_exp_cone(v; dual=false)] += 1
dΠ = MOD.projection_gradient_on_set(MOD.DefaultDistance(), v, s)
grad_fdm1 = FiniteDifferences.jacobian(ffdm, x -> MOD.projection_on_set(MOD.DefaultDistance(), x, s), v)[1]'
grad_fdm2 = FiniteDifferences.jacobian(bfdm, x -> MOD.projection_on_set(MOD.DefaultDistance(), x, s), v)[1]'
@test size(grad_fdm1) == size(grad_fdm2) == size(dΠ)
@test ≈(dΠ, grad_fdm1,atol=tol) || ≈(dΠ, grad_fdm2, atol=tol)
if !(≈(dΠ, grad_fdm1,atol=tol) || ≈(dΠ, grad_fdm2, atol=tol))
println("v = $v")
tjdiamandis marked this conversation as resolved.
Show resolved Hide resolved
println("n1: $(norm(dΠ - grad_fdm1))\nn2: $(norm(dΠ - grad_fdm2))")
end
end

@testset "Dual Cone" begin
case_d[det_case_exp_cone(v; dual=true)] += 1
dΠ = MOD.projection_gradient_on_set(MOD.DefaultDistance(), v, sd)
grad_fdm1 = FiniteDifferences.jacobian(ffdm, x -> MOD.projection_on_set(MOD.DefaultDistance(), x, sd), v)[1]'
grad_fdm2 = FiniteDifferences.jacobian(bfdm, x -> MOD.projection_on_set(MOD.DefaultDistance(), x, sd), v)[1]'
@test size(grad_fdm1) == size(grad_fdm2) == size(dΠ)
@test ≈(dΠ, grad_fdm1,atol=tol) || ≈(dΠ, grad_fdm2, atol=tol)
if !(≈(dΠ, grad_fdm1,atol=tol) || ≈(dΠ, grad_fdm2, atol=tol))
println("v = $v")
println("n1: $(norm(dΠ - grad_fdm1))\nn2: $(norm(dΠ - grad_fdm2))")
end
end
end
@test all(case_p .> 0) && all(case_d .> 0)
end
end
54 changes: 54 additions & 0 deletions test/projections.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using JuMP, SCS
const DD = MOD.DefaultDistance()

@testset "Test projections distance on vector sets" begin
Expand Down Expand Up @@ -76,3 +77,56 @@ end
output_joint = MOD.projection_gradient_on_set(DD, [v1, v2], [c1, c2])
@test output_joint ≈ BlockDiagonal([output_1, output_2])
end


@testset "Exponential Cone Projections" begin
function det_case_exp_cone(v; dual=false)
v = dual ? -v : v
if MOD._in_exp_cone(v; dual=false)
return 1
elseif MOD._in_exp_cone(-v; dual=true)
return 2
elseif v[1] <= 0 && v[2] <= 0 #TODO: threshold here??
return 3
else
return 4
end
end

function _test_proj_exp_cone_help(x, tol; dual=false)
cone = dual ? MOI.DualExponentialCone() : MOI.ExponentialCone()
model = Model()
set_optimizer(model, optimizer_with_attributes(
SCS.Optimizer, "eps" => 1e-10, "max_iters" => 10000, "verbose" => 0))
@variable(model, z[1:3])
@variable(model, t)
@objective(model, Min, t)
@constraint(model, sum((x-z).^2) <= t)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random note:
interesting, I didn't think this way of writing the squared norm would be faster than dot:

julia> @btime @constraint($model, sum(($x-$z).^2) <= $t)
  4.522 μs (112 allocations: 9.39 KiB)
julia> @btime @constraint($model, ($x-$z) ⋅ ($x-$z)  <= $t)
  6.419 μs (168 allocations: 13.28 KiB)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interesting. Admittedly, I didn't think to check the latter.

@constraint(model, z in cone)
optimize!(model)
z_star = value.(z)
px = MOD.projection_on_set(DD, x, cone)
if !isapprox(px, z_star, atol=tol)
# error("Exp cone projection failed:\n x = $x\nMOD: $px\nJuMP: $z_star
# \nnorm: $(norm(px - z_star))")
return false
end
return true
end

Random.seed!(0)
n = 3
atol = 1e-7
case_p = zeros(4)
case_d = zeros(4)
for _ in 1:100
x = randn(3)

case_p[det_case_exp_cone(x; dual=false)] += 1
@test _test_proj_exp_cone_help(x, atol; dual=false)

case_d[det_case_exp_cone(x; dual=true)] += 1
@test _test_proj_exp_cone_help(x, atol; dual=true)
end
@test all(case_p .> 0) && all(case_d .> 0)
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const MOD = MathOptSetDistances
const MOI = MathOptSetDistances.MOI

using LinearAlgebra
using Random
import BlockDiagonals: BlockDiagonal

include("distances.jl")
Expand Down