-
Notifications
You must be signed in to change notification settings - Fork 78
/
metricball.jl
124 lines (94 loc) · 3.13 KB
/
metricball.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------
"""
MetricBall(radii, rotation=nothing)
MetricBall(radius, metric=Euclidean())
A metric ball is a neighborhood that can be expressed in terms
of a metric and a set of `radii`. The two main examples are the
Euclidean ball an the Mahalanobis (ellipsoid) ball.
When multiple `radii` are provided, they can be rotated by a
`rotation` specification from the [Rotations.jl]
(https://github.com/JuliaGeometry/Rotations.jl)
package. Alternatively, a metric from the [Distances.jl]
(https://github.com/JuliaStats/Distances.jl) package can
be specified together with a single `radius`.
## Examples
N-dimensional Euclidean ball with radius `1.0`:
```julia
julia> euclidean = MetricBall(1.0)
```
Axis-aligned 3D ellipsoid with radii `(3.0, 2.0, 1.0)`:
```julia
julia> mahalanobis = MetricBall((3.0, 2.0, 1.0))
```
"""
struct MetricBall{L,R,M} <: Neighborhood
radii::L
rotation::R
# state fields
metric::M
end
function MetricBall(radii::SVector{Dim,T}, rotation=default_rotation(Val{Dim}(), T)) where {Dim,T}
# scaling matrix
Λ = Diagonal(one(T) ./ radii .^ 2)
# rotation matrix
R = rotation
# anisotropy matrix
M = Symmetric(R * Λ * R')
# Mahalanobis metric
metric = Mahalanobis(M)
MetricBall(radii, rotation, metric)
end
MetricBall(radii::NTuple{Dim,T}, rotation=default_rotation(Val{Dim}(), T)) where {Dim,T} =
MetricBall(SVector(radii), rotation)
# avoid silent calls to inner constructor
MetricBall(radii::AbstractVector{T}, rotation=default_rotation(Val{length(radii)}(), T)) where {T} =
MetricBall(SVector{length(radii),T}(radii), rotation)
MetricBall(radius::T, metric=Euclidean()) where {T<:Number} = MetricBall(SVector(radius), nothing, metric)
default_rotation(::Val{2}, T) = one(Angle2d{T})
default_rotation(::Val{3}, T) = one(QuatRotation{T})
"""
radii(ball)
Return the radii of the metric `ball`.
"""
radii(ball::MetricBall) = ball.radii
"""
rotation(ball)
Return the rotation of the metric `ball`.
"""
rotation(ball::MetricBall) = isnothing(ball.rotation) ? I : ball.rotation
"""
metric(ball)
Return the metric of the metric `ball`.
"""
metric(ball::MetricBall) = ball.metric
"""
radius(ball)
Return the effective radius of the metric `ball`,
i.e. the value `r` such that `||v|| ≤ r, ∀ v ∈ ball`
and `||v|| > r, ∀ v ∉ ball``.
"""
function radius(ball::MetricBall)
r = first(ball.radii)
ball.metric isa Mahalanobis ? one(r) : r
end
"""
isisotropic(ball)
Tells whether or not the metric `ball` is isotropic,
i.e. if all its radii are equal.
"""
isisotropic(ball::MetricBall) = length(unique(ball.radii)) == 1
function *(α::Real, ball::MetricBall)
if ball.metric isa Mahalanobis
MetricBall(α .* ball.radii, ball.rotation)
else
MetricBall(α .* ball.radii, nothing, ball.metric)
end
end
function Base.show(io::IO, ball::MetricBall)
n = length(ball.radii)
r = n > 1 ? Tuple(ball.radii) : first(ball.radii)
m = nameof(typeof(ball.metric))
print(io, "MetricBall($r, $m)")
end