Skip to content

Commit 3379dea

Browse files
authored
[Utilities] add distance_to_set (#2048)
1 parent 8ded581 commit 3379dea

File tree

4 files changed

+340
-0
lines changed

4 files changed

+340
-0
lines changed

docs/src/submodules/Utilities/reference.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,17 @@ Utilities.is_diagonal_vectorized_index
220220
Utilities.side_dimension_for_vectorized_dimension
221221
```
222222

223+
## Set utilities
224+
225+
The following utilities are available for sets:
226+
227+
```@docs
228+
Utilities.AbstractDistance
229+
Utilities.ProjectionUpperBoundDistance
230+
Utilities.distance_to_set
231+
Utilities.set_dot
232+
```
233+
223234
## DoubleDicts
224235

225236
```@docs

src/Utilities/Utilities.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ include("print.jl")
8080
include("penalty_relaxation.jl")
8181
include("lazy_iterators.jl")
8282

83+
include("distance_to_set.jl")
84+
8385
include("precompile.jl")
8486
_precompile_()
8587

src/Utilities/distance_to_set.jl

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Copyright (c) 2017: Miles Lubin and contributors
2+
# Copyright (c) 2017: Google Inc.
3+
#
4+
# Use of this source code is governed by an MIT-style license that can be found
5+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
6+
7+
"""
8+
abstract type AbstractDistance end
9+
10+
An abstract type used to enabble dispatch of
11+
[`Utilities.distance_to_set`](@ref).
12+
"""
13+
abstract type AbstractDistance end
14+
15+
"""
16+
ProjectionUpperBoundDistance() <: AbstractDistance
17+
18+
An upper bound on the minimum distance between `point` and the closest
19+
feasible point in `set`.
20+
21+
## Definition of distance
22+
23+
The minimum distance is computed as:
24+
```math
25+
d(x, \\mathcal{K}) = \\min_{y \\in \\mathcal{K}} || x - y ||
26+
```
27+
where ``x`` is `point` and ``\\mathcal{K}`` is `set`. The norm is computed as:
28+
```math
29+
||x|| = \\sqrt{f(x, x, \\mathcal{K})}
30+
```
31+
where ``f`` is [`Utilities.set_dot`](@ref).
32+
33+
In the default case, where the set does not have a specialized method for
34+
[`Utilities.set_dot`](@ref), the norm is equivalent to the Euclidean norm
35+
``||x|| = \\sqrt{\\sum x_i^2}``.
36+
37+
## Why an upper bound?
38+
39+
In most cases, `distance_to_set` should return the smallest upper bound, but it
40+
may return a larger value if the smallest upper bound is expensive to compute.
41+
42+
For example, given an epigraph from of a conic set, ``\\{(t, x) | f(x) \\le t\\}``,
43+
it may be simpler to return ``\\delta`` such that ``f(x) \\le t + \\delta``,
44+
rather than computing the nearest projection onto the set.
45+
46+
If the distance is not the smallest upper bound, the docstring of the
47+
appropriate `distance_to_set` method _must_ describe the way that the distance
48+
is computed.
49+
"""
50+
struct ProjectionUpperBoundDistance <: AbstractDistance end
51+
52+
"""
53+
distance_to_set(
54+
[d::AbstractDistance = ProjectionUpperBoundDistance()],]
55+
point::T,
56+
set::MOI.AbstractScalarSet,
57+
) where {T}
58+
59+
distance_to_set(
60+
[d::AbstractDistance = ProjectionUpperBoundDistance(),]
61+
point::AbstractVector{T},
62+
set::MOI.AbstractVectorSet,
63+
) where {T}
64+
65+
Compute the distance between `point` and `set` using the distance metric `d`. If
66+
`point` is in the set `set`, this function _must_ return `zero(T)`.
67+
68+
If `d` is omitted, the default distance is [`Utilities.ProjectionUpperBoundDistance`](@ref).
69+
"""
70+
function distance_to_set(point, set)
71+
return distance_to_set(ProjectionUpperBoundDistance(), point, set)
72+
end
73+
74+
function distance_to_set(d::AbstractDistance, ::Any, set::MOI.AbstractSet)
75+
return error(
76+
"distance_to_set using the distance metric $d for set type " *
77+
"$(typeof(set)) has not been implemented yet.",
78+
)
79+
end
80+
81+
###
82+
### MOI.AbstractScalarSets
83+
###
84+
85+
function distance_to_set(
86+
::ProjectionUpperBoundDistance,
87+
x::T,
88+
set::MOI.LessThan{T},
89+
) where {T<:Real}
90+
return max(x - set.upper, zero(T))
91+
end
92+
93+
function distance_to_set(
94+
::ProjectionUpperBoundDistance,
95+
x::T,
96+
set::MOI.GreaterThan{T},
97+
) where {T<:Real}
98+
return max(set.lower - x, zero(T))
99+
end
100+
101+
function distance_to_set(
102+
::ProjectionUpperBoundDistance,
103+
x::T,
104+
set::MOI.EqualTo{T},
105+
) where {T<:Number}
106+
return abs(set.value - x)
107+
end
108+
109+
function distance_to_set(
110+
::ProjectionUpperBoundDistance,
111+
x::T,
112+
set::MOI.Interval{T},
113+
) where {T<:Real}
114+
return max(x - set.upper, set.lower - x, zero(T))
115+
end
116+
117+
function distance_to_set(
118+
::ProjectionUpperBoundDistance,
119+
x::T,
120+
::MOI.ZeroOne,
121+
) where {T<:Real}
122+
return min(abs(x - zero(T)), abs(x - one(T)))
123+
end
124+
125+
function distance_to_set(
126+
::ProjectionUpperBoundDistance,
127+
x::T,
128+
::MOI.Integer,
129+
) where {T<:Real}
130+
return abs(x - round(x))
131+
end
132+
133+
function distance_to_set(
134+
::ProjectionUpperBoundDistance,
135+
x::T,
136+
set::MOI.Semicontinuous{T},
137+
) where {T<:Real}
138+
return min(max(x - set.upper, set.lower - x, zero(T)), abs(x))
139+
end
140+
141+
function distance_to_set(
142+
::ProjectionUpperBoundDistance,
143+
x::T,
144+
set::MOI.Semiinteger{T},
145+
) where {T<:Real}
146+
d = max(ceil(set.lower) - x, x - floor(set.upper), abs(x - round(x)))
147+
return min(d, abs(x))
148+
end
149+
150+
###
151+
### MOI.AbstractVectorSets
152+
###
153+
154+
function _check_dimension(v::AbstractVector, s)
155+
if length(v) != MOI.dimension(s)
156+
throw(DimensionMismatch("Mismatch between value and set"))
157+
end
158+
return
159+
end
160+
161+
function distance_to_set(
162+
::ProjectionUpperBoundDistance,
163+
x::AbstractVector{T},
164+
set::MOI.Nonnegatives,
165+
) where {T<:Real}
166+
_check_dimension(x, set)
167+
return LinearAlgebra.norm(max(-xi, zero(T)) for xi in x)
168+
end
169+
170+
function distance_to_set(
171+
::ProjectionUpperBoundDistance,
172+
x::AbstractVector{T},
173+
set::MOI.Nonpositives,
174+
) where {T<:Real}
175+
_check_dimension(x, set)
176+
return LinearAlgebra.norm(max(xi, zero(T)) for xi in x)
177+
end
178+
179+
function distance_to_set(
180+
::ProjectionUpperBoundDistance,
181+
x::AbstractVector{T},
182+
set::MOI.Zeros,
183+
) where {T<:Number}
184+
_check_dimension(x, set)
185+
return LinearAlgebra.norm(x)
186+
end
187+
188+
function distance_to_set(
189+
::ProjectionUpperBoundDistance,
190+
x::AbstractVector{T},
191+
set::MOI.Reals,
192+
) where {T<:Real}
193+
_check_dimension(x, set)
194+
return zero(T)
195+
end

test/Utilities/distance_to_set.jl

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Copyright (c) 2017: Miles Lubin and contributors
2+
# Copyright (c) 2017: Google Inc.
3+
#
4+
# Use of this source code is governed by an MIT-style license that can be found
5+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
6+
7+
module TestFeasibilityChecker
8+
9+
using Test
10+
11+
import MathOptInterface
12+
13+
const MOI = MathOptInterface
14+
15+
function runtests()
16+
for name in names(@__MODULE__; all = true)
17+
if startswith("$(name)", "test_")
18+
@testset "$(name)" begin
19+
getfield(@__MODULE__, name)()
20+
end
21+
end
22+
end
23+
return
24+
end
25+
26+
function test_unsupported()
27+
@test_throws(
28+
ErrorException,
29+
MOI.Utilities.distance_to_set([1.0, 1.0], MOI.Complements(2)),
30+
)
31+
return
32+
end
33+
34+
function test_lessthan()
35+
@test MOI.Utilities.distance_to_set(1.0, MOI.LessThan(2.0)) 0.0
36+
@test MOI.Utilities.distance_to_set(1.0, MOI.LessThan(0.5)) 0.5
37+
return
38+
end
39+
40+
function test_greaterthan()
41+
@test MOI.Utilities.distance_to_set(1.0, MOI.GreaterThan(2.0)) 1.0
42+
@test MOI.Utilities.distance_to_set(1.0, MOI.GreaterThan(0.5)) 0.0
43+
return
44+
end
45+
46+
function test_equalto()
47+
@test MOI.Utilities.distance_to_set(1.0, MOI.EqualTo(2.0)) 1.0
48+
@test MOI.Utilities.distance_to_set(1.0, MOI.EqualTo(0.5)) 0.5
49+
return
50+
end
51+
52+
function test_interval()
53+
@test MOI.Utilities.distance_to_set(1.0, MOI.Interval(1.0, 2.0)) 0.0
54+
@test MOI.Utilities.distance_to_set(0.5, MOI.Interval(1.0, 2.0)) 0.5
55+
@test MOI.Utilities.distance_to_set(2.75, MOI.Interval(1.0, 2.0)) 0.75
56+
return
57+
end
58+
59+
function test_zeroone()
60+
@test MOI.Utilities.distance_to_set(0.6, MOI.ZeroOne()) 0.4
61+
@test MOI.Utilities.distance_to_set(-0.01, MOI.ZeroOne()) 0.01
62+
@test MOI.Utilities.distance_to_set(1.01, MOI.ZeroOne()) 0.01
63+
return
64+
end
65+
66+
function test_integer()
67+
@test MOI.Utilities.distance_to_set(0.6, MOI.Integer()) 0.4
68+
@test MOI.Utilities.distance_to_set(3.1, MOI.Integer()) 0.1
69+
@test MOI.Utilities.distance_to_set(-0.01, MOI.Integer()) 0.01
70+
@test MOI.Utilities.distance_to_set(1.01, MOI.Integer()) 0.01
71+
return
72+
end
73+
74+
function test_semicontinuous()
75+
s = MOI.Semicontinuous(2.0, 4.0)
76+
@test MOI.Utilities.distance_to_set(-2.0, s) 2.0
77+
@test MOI.Utilities.distance_to_set(0.5, s) 0.5
78+
@test MOI.Utilities.distance_to_set(1.9, s) 0.1
79+
@test MOI.Utilities.distance_to_set(2.1, s) 0.0
80+
@test MOI.Utilities.distance_to_set(4.1, s) 0.1
81+
return
82+
end
83+
84+
function test_semiintger()
85+
s = MOI.Semiinteger(1.9, 4.0)
86+
@test MOI.Utilities.distance_to_set(-2.0, s) 2.0
87+
@test MOI.Utilities.distance_to_set(0.5, s) 0.5
88+
@test MOI.Utilities.distance_to_set(1.9, s) 0.1
89+
@test MOI.Utilities.distance_to_set(2.1, s) 0.1
90+
@test MOI.Utilities.distance_to_set(4.1, s) 0.1
91+
return
92+
end
93+
94+
function test_nonnegatives()
95+
@test_throws(
96+
DimensionMismatch,
97+
MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Nonnegatives(1))
98+
)
99+
@test MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Nonnegatives(2)) 1.0
100+
return
101+
end
102+
103+
function test_nonpositives()
104+
@test_throws(
105+
DimensionMismatch,
106+
MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Nonpositives(1))
107+
)
108+
@test MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Nonpositives(2)) 1.0
109+
return
110+
end
111+
112+
function test_reals()
113+
@test_throws(
114+
DimensionMismatch,
115+
MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Reals(1))
116+
)
117+
@test MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Reals(2)) 0.0
118+
return
119+
end
120+
121+
function test_zeros()
122+
@test_throws(
123+
DimensionMismatch,
124+
MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Zeros(1))
125+
)
126+
@test MOI.Utilities.distance_to_set([-1.0, 1.0], MOI.Zeros(2)) sqrt(2)
127+
return
128+
end
129+
130+
end
131+
132+
TestFeasibilityChecker.runtests()

0 commit comments

Comments
 (0)