-
Notifications
You must be signed in to change notification settings - Fork 409
/
utils.jl
170 lines (152 loc) · 5.48 KB
/
utils.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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
## macro for argument checking
macro strip_linenos(expr)
return esc(Base.remove_linenums!(expr))
end
"""
@check_args(
D,
@setup(statements...),
(arg₁, cond₁, message₁),
(cond₂, message₂),
...,
)
A convenience macro that generates checks of arguments for a distribution of type `D`.
The macro expects that a boolean variable of name `check_args` is defined and generates
the following Julia code:
```julia
Distributions.check_args(check_args) do
\$(statements...)
cond₁ || throw(DomainError(arg₁, \$(string(D, ": ", message₁))))
cond₂ || throw(ArgumentError(\$(string(D, ": ", message₂))))
...
end
```
The `@setup` argument can be elided if no setup code is needed. Moreover, error messages
can be omitted. In this case the message `"the condition \$(cond) is not satisfied."` is
used.
"""
macro check_args(D, setup_or_check, checks...)
# Extract setup statements
if Meta.isexpr(setup_or_check, :macrocall) && setup_or_check.args[1] == Symbol("@setup")
setup_stmts = Any[esc(ex) for ex in setup_or_check.args[3:end]]
else
setup_stmts = []
checks = (setup_or_check, checks...)
end
# Generate expressions for each condition
conds_exprs = map(checks) do check
if Meta.isexpr(check, :tuple, 3)
# argument, condition, and message specified
arg = check.args[1]
cond = check.args[2]
message = string(D, ": ", check.args[3])
return :(($(esc(cond))) || throw(DomainError($(esc(arg)), $message)))
elseif Meta.isexpr(check, :tuple, 2)
cond_or_message = check.args[2]
if cond_or_message isa String
# only condition and message specified
cond = check.args[1]
message = string(D, ": ", cond_or_message)
return :(($(esc(cond))) || throw(ArgumentError($message)))
else
# only argument and condition specified
arg = check.args[1]
cond = cond_or_message
message = string(D, ": the condition ", cond, " is not satisfied.")
return :(($(esc(cond))) || throw(DomainError($(esc(arg)), $message)))
end
else
# only condition specified
cond = check
message = string(D, ": the condition ", cond, " is not satisfied.")
return :(($(esc(cond))) || throw(ArgumentError($message)))
end
end
return @strip_linenos quote
Distributions.check_args($(esc(:check_args))) do
$(__source__)
$(setup_stmts...)
$(conds_exprs...)
end
end
end
"""
check_args(f, check::Bool)
Perform check of arguments by calling a function `f`.
If `check` is `false`, the checks are skipped.
"""
function check_args(f::F, check::Bool) where {F}
check && f()
nothing
end
##### Utility functions
isunitvec(v::AbstractVector) = (norm(v) - 1.0) < 1.0e-12
isprobvec(p::AbstractVector{<:Real}) =
all(x -> x ≥ zero(x), p) && isapprox(sum(p), one(eltype(p)))
# get a type wide enough to represent all a distributions's parameters
# (if the distribution is parametric)
# if the distribution is not parametric, we need this to be a float so that
# in-place pdf calculations, etc. allocate storage correctly
@inline partype(::Distribution) = Float64
# because X == X' keeps failing due to floating point nonsense
function isApproxSymmmetric(a::AbstractMatrix{Float64})
tmp = true
for j in 2:size(a, 1)
for i in 1:(j - 1)
tmp &= abs(a[i, j] - a[j, i]) < 1e-8
end
end
return tmp
end
"""
ispossemdef(A, k) -> Bool
Test whether a matrix is positive semi-definite with specified rank `k` by
checking that `k` of its eigenvalues are positive and the rest are zero.
# Examples
```jldoctest; setup = :(using Distributions: ispossemdef)
julia> A = [1 0; 0 0]
2×2 Matrix{Int64}:
1 0
0 0
julia> ispossemdef(A, 1)
true
julia> ispossemdef(A, 2)
false
```
"""
function ispossemdef(X::AbstractMatrix, k::Int;
atol::Real=0.0,
rtol::Real=(minimum(size(X))*eps(real(float(one(eltype(X))))))*iszero(atol))
_check_rank_range(k, minimum(size(X)))
ishermitian(X) || return false
dp, dz, dn = eigsigns(Hermitian(X), atol, rtol)
return dn == 0 && dp == k
end
function ispossemdef(X::AbstractMatrix;
atol::Real=0.0,
rtol::Real=(minimum(size(X))*eps(real(float(one(eltype(X))))))*iszero(atol))
ishermitian(X) || return false
dp, dz, dn = eigsigns(Hermitian(X), atol, rtol)
return dn == 0
end
function _check_rank_range(k::Int, n::Int)
0 <= k <= n || throw(ArgumentError("rank must be between 0 and $(n) (inclusive)"))
nothing
end
# return counts of the number of positive, zero, and negative eigenvalues
function eigsigns(X::AbstractMatrix,
atol::Real=0.0,
rtol::Real=(minimum(size(X))*eps(real(float(one(eltype(X))))))*iszero(atol))
eigs = eigvals(X)
eigsigns(eigs, atol, rtol)
end
function eigsigns(eigs::Vector{<: Real}, atol::Real, rtol::Real)
tol = max(atol, rtol * eigs[end])
eigsigns(eigs, tol)
end
function eigsigns(eigs::Vector{<: Real}, tol::Real)
dp = count(x -> tol < x, eigs) # number of positive eigenvalues
dz = count(x -> -tol < x < tol, eigs) # number of numerically zero eigenvalues
dn = count(x -> x < -tol, eigs) # number of negative eigenvalues
return dp, dz, dn
end