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

[WIP] Evaluation distance to set #1023

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b606dc3
first sets
matbesancon Feb 11, 2020
6670920
more sets
matbesancon Feb 11, 2020
93ac7f9
forgot subslice
matbesancon Feb 11, 2020
55ed9d6
rewrite as distance function
matbesancon Feb 11, 2020
09663dd
sign confusion
matbesancon Feb 11, 2020
9d4def8
added integers
matbesancon Feb 11, 2020
c02e1ba
function for verification not being toplevel
matbesancon Feb 11, 2020
eea190e
more integer sets
matbesancon Feb 12, 2020
3a78641
moved distances to MOI
matbesancon Feb 13, 2020
016fa9d
fix backslash
matbesancon Feb 13, 2020
46b6a39
unqualified type param
matbesancon Feb 13, 2020
a1d2f6f
documentation, review changes
matbesancon Feb 13, 2020
90d3631
vectorized versions
matbesancon Feb 13, 2020
ae2c2a8
add boilerplate to all methods
matbesancon Feb 13, 2020
b3678b5
check all max locally
matbesancon Feb 20, 2020
73aaa3b
set distance in sets
matbesancon Feb 20, 2020
3cab392
removed file
matbesancon Feb 21, 2020
83a6131
norm on all multi-results
matbesancon Mar 6, 2020
c969068
first tests
matbesancon Mar 6, 2020
891426f
exponential cones
matbesancon Mar 6, 2020
5cde8c8
power cone
matbesancon Mar 6, 2020
b6f979a
dual power cone
matbesancon Mar 6, 2020
b19f884
increased tol
matbesancon Mar 9, 2020
ed1d33b
Merge branch 'master' into eval-set
matbesancon Mar 31, 2020
f80f399
Merge branch 'master' into eval-set
matbesancon May 16, 2020
54ff584
default distance
matbesancon May 16, 2020
68888b5
abstract and default distance for sets, added complementarity and dum…
matbesancon May 16, 2020
21c759f
remove complementarity
matbesancon May 16, 2020
9a2e898
increase tolerance
matbesancon May 19, 2020
13725c3
update doc
matbesancon May 19, 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
1 change: 0 additions & 1 deletion src/MathOptInterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ include("error.jl")
include("indextypes.jl")
include("functions.jl")
include("sets.jl")
include("attributes.jl")
matbesancon marked this conversation as resolved.
Show resolved Hide resolved
include("constraints.jl")
include("modifications.jl")
include("variables.jl")
Expand Down
189 changes: 189 additions & 0 deletions src/sets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ function dual_set_type end

dual_set_type(S::Type{<:AbstractSet}) = error("Dual type of $S is not implemented.")

"""
distance_to_set(v, s)

Compute the distance of a value to a set.
For some vector-valued sets, can return a vector of distances.
Copy link
Member

Choose a reason for hiding this comment

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

A vector of distances isn't a distance, mathematically speaking. What is meant here precisely?

Copy link
Member

Choose a reason for hiding this comment

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

I guess we want distance_to_set(point, set::AbstractSet; norm::Int = 2). Or even just say that we're using the L2 norm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the intuition I had for it is giving an element-wise distance when it makes sense, giving which element to "improve" to reduce the distance to the set.
We can also always normalize as @odow mentions

Copy link
Member

Choose a reason for hiding this comment

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

If we add the distance type parameter we can easily allow the user to switch from the norm2 thing to a norm infinity which is pretty reasonable

Copy link
Member

Choose a reason for hiding this comment

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

This is still an issue.

When `v ∈ s`, the distance is zero (or all individual distances are zero).

Each set `S` implements `distance_to_set(v::T, s::S)` with `T` of appropriate type.
"""
function distance_to_set end

function _check_dimension(v::AbstractVector, s)
length(v) != dimension(s) && throw(DimensionMismatch("Mismatch between value and set"))
return nothing
end

"""
AbstractScalarSet

Expand Down Expand Up @@ -112,6 +128,11 @@ end
dual_set(s::Reals) = Zeros(dimension(s))
dual_set_type(::Type{Reals}) = Zeros

function distance_to_set(v::AbstractVector{<:Real}, s::Reals)
_check_dimension(v, s)
return zeros(length(v))
end

"""
Zeros(dimension)

Expand All @@ -124,6 +145,11 @@ end
dual_set(s::Zeros) = Reals(dimension(s))
dual_set_type(::Type{Zeros}) = Reals

function distance_to_set(v::AbstractVector{<:Real}, s::Zeros)
_check_dimension(v, s)
return abs.(v)
end

"""
Nonnegatives(dimension)

Expand All @@ -136,6 +162,11 @@ end
dual_set(s::Nonnegatives) = copy(s)
dual_set_type(::Type{Nonnegatives}) = Nonnegatives

function distance_to_set(v::AbstractVector{<:Real}, s::Nonnegatives)
_check_dimension(v, s)
return [ifelse(vi < 0, -vi, zero(vi)) for vi in v]
end

"""
Nonpositives(dimension)

Expand All @@ -148,6 +179,11 @@ end
dual_set(s::Nonpositives) = copy(s)
dual_set_type(::Type{Nonpositives}) = Nonpositives

function distance_to_set(v::AbstractVector{<:Real}, s::Nonpositives)
_check_dimension(v, s)
return [ifelse(vi > 0, vi, zero(vi)) for vi in v]
end

"""
GreaterThan{T <: Real}(lower::T)

Expand Down Expand Up @@ -179,6 +215,10 @@ function Base.:(==)(set1::S, set2::S) where S <: Union{GreaterThan, LessThan, Eq
return constant(set1) == constant(set2)
end

distance_to_set(v::Real, s::LessThan) = max(v - s.upper, zero(v))
distance_to_set(v::Real, s::GreaterThan) = max(s.lower - v, zero(v))
distance_to_set(v::Real, s::EqualTo) = abs(v - s.value)

"""
Interval{T <: Real}(lower::T,upper::T)

Expand Down Expand Up @@ -206,6 +246,8 @@ Interval(s::LessThan{<:AbstractFloat}) = Interval(typemin(s.upper), s.upper)
Interval(s::EqualTo{<:Real}) = Interval(s.value, s.value)
Interval(s::Interval) = s

distance_to_set(v::T, s::Interval) where {T <: Real} = max(s.lower - v, v - s.upper, zero(T))

"""
constant(s::Union{EqualTo, GreaterThan, LessThan})

Expand Down Expand Up @@ -239,6 +281,14 @@ end
dual_set(s::NormOneCone) = NormInfinityCone(dimension(s))
dual_set_type(::Type{NormOneCone}) = NormInfinityCone

function distance_to_set(v::AbstractVector{<:Real}, s::NormOneCone)
_check_dimension(v, s)
t = v[1]
xs = v[2:end]
result = t - sum(abs, xs)
return max(result, zero(result))
end

"""
SecondOrderCone(dimension)

Expand All @@ -251,6 +301,14 @@ end
dual_set(s::SecondOrderCone) = copy(s)
dual_set_type(::Type{SecondOrderCone}) = SecondOrderCone

function distance_to_set(v::AbstractVector{<:Real}, s::SecondOrderCone)
_check_dimension(v, s)
t = v[1]
xs = v[2:end]
result = LinearAlgebra.norm(xs) - t
return max(result, zero(result)) # avoids sqrt
odow marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

What is the comment # avoids sqrt for?

I have a general unresolved concern over our choice of distance. It seems like this is the "epigraph violation" distance, which is not the same as the Euclidean distance to the set. What is the precedent for how other solvers measure constraint feasibility? (@chriscoey / @lkapelevich how does Hypatia?)

Copy link
Contributor

Choose a reason for hiding this comment

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

I personally use the epigraph violation d = x1 - norm(x[2:end], 2), mainly because

  • it's easy to compute
  • it tells by how much the conic constraint should be "relaxed" for the point to be feasible (where "relaxed" means that you shift the cone's apex)
  • if the SOC constraint is written as x2^2 + ... xn^2 <= x1^2, the epigraph violation is the square root of this quadratic constraint's absolute violation.

Looks like COSMO uses euclidean distance for computing projections.

Copy link
Member

Choose a reason for hiding this comment

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

What about sets like the exponential cone needing domain constraints like y > 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is the comment # avoids sqrt for?

legacy comment, just removed. For domain constraints it can be an open question, because some parts of the distance may be non-computable / infinite or complex if the domain is not respected

Copy link
Member

Choose a reason for hiding this comment

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

Okay, so for conic sets "Epigraph violation or Inf if point fails feasibility bounds" seems like an easily computable and sensible definition.

Copy link
Member

Choose a reason for hiding this comment

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

And you call it EpigraphDistance or something like...

Copy link
Member

Choose a reason for hiding this comment

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

For norm two distances you can use what proximal solvers do:
https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf page 183 (section 6.3)

Copy link
Member

Choose a reason for hiding this comment

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

So, SOC is easy, PSD you just need eigen decomposition, Power and Exponential are well posed even with the extra constraints, but need a Newton...

end

"""
RotatedSecondOrderCone(dimension)

Expand All @@ -263,6 +321,18 @@ end
dual_set(s::RotatedSecondOrderCone) = copy(s)
dual_set_type(::Type{RotatedSecondOrderCone}) = RotatedSecondOrderCone

function distance_to_set(v::AbstractVector{<:Real}, s::RotatedSecondOrderCone)
_check_dimension(v, s)
t = v[1]
u = v[2]
xs = v[3:end]
return [
odow marked this conversation as resolved.
Show resolved Hide resolved
max(-t, zero(t)),
max(-u, zero(u)),
max(dot(xs,xs) - 2 * t * u),
]
end

"""
GeometricMeanCone(dimension)

Expand All @@ -272,6 +342,18 @@ struct GeometricMeanCone <: AbstractVectorSet
dimension::Int
end

function distance_to_set(v::AbstractVector{<:Real}, s::GeometricMeanCone)
_check_dimension(v, s)
t = v[1]
xs = v[2:end]
n = dimension(s) - 1
result = t^n - prod(xs)
return push!(
max.(xs, zero(result)), # x >= 0
max(result, zero(result)),
)
end

"""
ExponentialCone()

Expand All @@ -282,6 +364,15 @@ struct ExponentialCone <: AbstractVectorSet end
dual_set(s::ExponentialCone) = DualExponentialCone()
dual_set_type(::Type{ExponentialCone}) = DualExponentialCone

function distance_to_set(v::AbstractVector{<:Real}, s::ExponentialCone)
_check_dimension(v, s)
x = v[1]
y = v[2]
z = v[3]
result = y * exp(x/y) - z
return [max(y, zero(result)), max(result, zero(result))]
end

"""
DualExponentialCone()

Expand All @@ -292,6 +383,15 @@ struct DualExponentialCone <: AbstractVectorSet end
dual_set(s::DualExponentialCone) = ExponentialCone()
dual_set_type(::Type{DualExponentialCone}) = ExponentialCone

function distance_to_set(v::AbstractVector{<:Real}, s::DualExponentialCone)
_check_dimension(v, s)
u = v[1]
v = v[2]
w = v[3]
result = -u*exp(v/u) - ℯ * w
return [max(-u, zero(result)), max(result, zero(result))]
end

"""
PowerCone{T <: Real}(exponent::T)

Expand All @@ -304,6 +404,16 @@ end
dual_set(s::PowerCone{T}) where T <: Real = DualPowerCone{T}(s.exponent)
dual_set_type(::Type{PowerCone{T}}) where T <: Real = DualPowerCone{T}

function distance_to_set(v::AbstractVector{<:Real}, s::PowerCone)
_check_dimension(v, s)
x = v[1]
y = v[2]
z = v[3]
e = s.exponent
result = abs(z) - x^e * y^(1-e)
return [max(-x, zero(result)), max(-y, zero(result)), max(result, zero(result))]
end

"""
DualPowerCone{T <: Real}(exponent::T)

Expand All @@ -316,6 +426,17 @@ end
dual_set(s::DualPowerCone{T}) where T <: Real = PowerCone{T}(s.exponent)
dual_set_type(::Type{DualPowerCone{T}}) where T <: Real = PowerCone{T}

function distance_to_set(v::AbstractVector{<:Real}, s::DualPowerCone)
_check_dimension(v, s)
u = v[1]
v = v[2]
w = v[3]
e = s.exponent
ce = 1-e
result = abs(w) - (u/e)^e * (v/ce)^ce
return [max(-u, zero(result)), max(-v, zero(result)), max(result, zero(result))]
end

dimension(s::Union{ExponentialCone, DualExponentialCone, PowerCone, DualPowerCone}) = 3

function Base.:(==)(set1::S, set2::S) where S <: Union{PowerCone, DualPowerCone}
Expand All @@ -331,6 +452,20 @@ struct RelativeEntropyCone <: AbstractVectorSet
dimension::Int
end

function distance_to_set(v::AbstractVector{<:Real}, set::RelativeEntropyCone)
_check_dimension(v, s)
n = (dimension(set)-1) ÷ 2
u = v[1]
v = v[2:(n+1)]
w = v[(n+2):end]
s = sum(w[i] * log(w[i]/v[i]) for i in eachindex(w))
result = s - u
return push!(
max.(v[2:end], zero(result)),
max(result, zero(result)),
)
end

"""
NormSpectralCone(row_dim, column_dim)

Expand All @@ -345,6 +480,15 @@ end
dual_set(s::NormSpectralCone) = NormNuclearCone(s.row_dim, s.column_dim)
dual_set_type(::Type{NormSpectralCone}) = NormNuclearCone

function distance_to_set(v::AbstractVector{<:Real}, s::NormSpectralCone)
_check_dimension(v, s)
t = v[1]
m = reshape(v[2:end], (s.row_dim, s.column_dim))
s1 = LinearAlgebra.svd(m).S[1]
Copy link
Member

Choose a reason for hiding this comment

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

I'm not an expert on this, but I know that computing the full SVD is an extremely expensive way to compute the smallest singular value. This is probably not suitable for non-toy applications. There are a few packages that do this, e.g., https://jutho.github.io/KrylovKit.jl/latest/man/svd/. We should evaluate which, if any, are suitable to add as dependencies of MOI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed, I just kept the dependency-free version for now. MOI can either add a dependency or vendor (copy and acknowledge) a single function from another package

result = s1 - t
return max(result, zero(result))
end

"""
NormNuclearCone(row_dim, column_dim)

Expand All @@ -359,6 +503,15 @@ end
dual_set(s::NormNuclearCone) = NormSpectralCone(s.row_dim, s.column_dim)
dual_set_type(::Type{NormNuclearCone}) = NormSpectralCone

function distance_to_set(v::AbstractVector{<:Real}, s::NormNuclearCone)
_check_dimension(v, s)
t = v[1]
m = reshape(v[2:end], (s.row_dim, s.column_dim))
s1 = sum(LinearAlgebra.svd(m).S)
result = s1 - t
return max(result, zero(result))
end

dimension(s::Union{NormSpectralCone, NormNuclearCone}) = 1 + s.row_dim * s.column_dim

"""
Expand Down Expand Up @@ -642,6 +795,14 @@ The set ``\\{ 0, 1 \\}``.
"""
struct ZeroOne <: AbstractScalarSet end

function distance_to_set(v::T, ::ZeroOne) where {T <: Real}
return min(abs(v - zero(T)), abs(v - one(T)))
end

function distance_to_set(v::Real, ::Integer)
return min(abs(v - floor(v)), abs(v - ceil(v)))
end

"""
Semicontinuous{T <: Real}(lower::T,upper::T)

Expand Down Expand Up @@ -679,6 +840,23 @@ struct SOS1{T <: Real} <: AbstractVectorSet
weights::Vector{T}
end

# return the element-wise distance to zero, with the greatest element to 0
function distance_to_set(v::AbstractVector{T}, ::SOS1) where {T <: Real}
_check_dimension(v, s)
d = distance_to_set(v, Zeros(length(v)))
m = maximum(d)
if m ≈ zero(T)
return d
end
# removing greatest distance
for i in eachindex(d)
@inbounds if d[i] == m
d[i] = zero(T)
return d
end
end
end
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this function is correct. Something like:

function distance_to_set(::DefaultDistance, v::AbstractVector{T}, ::SOS1{T}) where {T <: Real}
    _check_dimension(v, s)
    _, i = findmax(abs.(v))
    return LinearAlgebra.norm2([v[j] for j = 1:length(v) if j != i])
end


"""
SOS2{T <: Real}(weights::Vector{T})

Expand Down Expand Up @@ -744,6 +922,17 @@ end

dimension(::IndicatorSet) = 2

# takes in input [z, f(x)]
function distance_to_set(v::AbstractVector{T}, s::IndicatorSet{A}) where {A, T <: Real}
_check_dimension(v, s)
z = v[1]
# inactive constraint
if A === ACTIVATE_ON_ONE && isapprox(z, 0) || A === ACTIVATE_ON_ZERO && isapprox(z, 1)
return zeros(T, 2)
end
return [distance_to_set(z, ZeroOne()), distance_to_set(v[2], s.set)]
end

function Base.copy(set::IndicatorSet{A,S}) where {A,S}
return IndicatorSet{A}(copy(set.set))
end
Expand Down