Skip to content

Commit f9ebf98

Browse files
authored
Spatiotemporal Permutation Entropy (#78)
* Add docstring of spatiotemporal permutation * Working implementation for periodic * Working code both periodic and not * add sanity check for negative ofsets * add tests * allow also negative offsets * correct age for schlemmer reference * refer to symbolize in code * fix everything and the tests * add changelog entry * add one more test and pretty printing
1 parent 875cfbe commit f9ebf98

File tree

6 files changed

+179
-9
lines changed

6 files changed

+179
-9
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
Changelog is kept with respect to version 0.11 of Entropies.jl.
44

5-
## 1.3
5+
## main
6+
* New probability estimator `SpatialSymbolicPermutation` suitable for computing spatial permutation entropies
67
* Introduce Tsallis entropy.
78

89
## 1.2

src/core.jl

+8-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export probabilities, probabilities!
66
export genentropy, genentropy!
77
export Dataset, dimension
88

9-
const Vector_or_Dataset = Union{AbstractVector, AbstractDataset}
9+
const Array_or_Dataset = Union{<:AbstractArray, <:AbstractDataset}
1010

1111
"""
1212
Probabilities(x) → p
@@ -49,7 +49,7 @@ abstract type ProbabilitiesEstimator end
4949
const ProbEst = ProbabilitiesEstimator # shorthand
5050

5151
"""
52-
probabilities(x::Vector_or_Dataset, est::ProbabilitiesEstimator) → p::Probabilities
52+
probabilities(x::Array_or_Dataset, est::ProbabilitiesEstimator) → p::Probabilities
5353
5454
Calculate probabilities representing `x` based on the provided
5555
estimator and return them as a [`Probabilities`](@ref) container (`Vector`-like).
@@ -58,7 +58,7 @@ documentation of the individual estimators for more.
5858
5959
The configuration options are always given as arguments to the chosen estimator.
6060
61-
probabilities(x::Vector_or_Dataset, ε::AbstractFloat) → p::Probabilities
61+
probabilities(x::Array_or_Dataset, ε::AbstractFloat) → p::Probabilities
6262
6363
Convenience syntax which provides probabilities for `x` based on rectangular binning
6464
(i.e. performing a histogram). In short, the state space is divided into boxes of length
@@ -71,11 +71,11 @@ This allows computation of probabilities (histograms) of high-dimensional
7171
datasets and with small box sizes `ε` without memory overflow and with maximum performance.
7272
To obtain the bin information along with `p`, use [`binhist`](@ref).
7373
74-
probabilities(x::Vector_or_Dataset, n::Integer) → p::Probabilities
74+
probabilities(x::Array_or_Dataset, n::Integer) → p::Probabilities
7575
Same as the above method, but now each dimension of the data is binned into `n::Int` equal
7676
sized bins instead of bins of length `ε::AbstractFloat`.
7777
78-
probabilities(x::Vector_or_Dataset) → p::Probabilities
78+
probabilities(x::Array_or_Dataset) → p::Probabilities
7979
Directly count probabilities from the elements of `x` without any discretization,
8080
binning, or other processing (mostly useful when `x` contains categorical or integer data).
8181
"""
@@ -97,7 +97,7 @@ Compute the generalized order-`q` entropy of some probabilities
9797
returned by the [`probabilities`](@ref) function. Alternatively, compute entropy
9898
from pre-computed `Probabilities`.
9999
100-
genentropy(x::Vector_or_Dataset, est; q = 1.0, base)
100+
genentropy(x::Array_or_Dataset, est; q = 1.0, base)
101101
102102
A convenience syntax, which calls first `probabilities(x, est)`
103103
and then calculates the entropy of the result (and thus `est` can be a
@@ -149,10 +149,10 @@ function genentropy(prob::Probabilities; q = 1.0, α = nothing, base = MathConst
149149
end
150150
end
151151

152-
genentropy(x::AbstractArray{<:Real}) =
152+
genentropy(::AbstractArray{<:Real}) =
153153
error("For single-argument input, do `genentropy(Probabilities(x))` instead.")
154154

155-
function genentropy(x::Vector_or_Dataset, est; q = 1.0, α = nothing, base = MathConstants.e)
155+
function genentropy(x::Array_or_Dataset, est; q = 1.0, α = nothing, base = MathConstants.e)
156156
if α nothing
157157
@warn "Keyword `α` is deprecated in favor of `q`."
158158
q = α

src/symbolic/spatial_permutation.jl

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
export SpatialSymbolicPermutation
2+
3+
"""
4+
SpatialSymbolicPermutation(stencil, x, periodic = true)
5+
A symbolic, permutation-based probabilities/entropy estimator for spatiotemporal systems.
6+
The data are a high-dimensional array `x`, such as 2D [^Ribeiro2012] or 3D [^Schlemmer2018].
7+
This approach is also known as _Spatiotemporal Permutation Entropy_.
8+
`x` is given because we need to know its size for optimization and bound checking.
9+
10+
A _stencil_ defines what local area around each pixel to
11+
consider, and compute the ordinal pattern within the stencil. Stencils are given as
12+
vectors of `CartesianIndex` which encode the _offsets_ of the pixes to include in the
13+
stencil, with respect to the current pixel. For example
14+
```julia
15+
data = [rand(50, 50) for _ in 1:50]
16+
x = data[1] # first "time slice" of a spatial system evolution
17+
stencil = CartesianIndex.([(0,1), (1,1), (1,0)])
18+
est = SpatialSymbolicPermutation(stencil, x)
19+
```
20+
Here the stencil creates a 2x2 square extending to the bottom and right of the pixel
21+
(directions here correspond to the way Julia prints matrices by default).
22+
Notice that no offset (meaning the pixel itself) is always included automatically.
23+
The length of the stencil decides the order of the permutation entropy, and the ordering
24+
within the stencil dictates the order that pixels are compared with.
25+
The pixel without any offset is always first in the order.
26+
27+
After having defined `est`, one calculates the permutation entropy of ordinal patterns
28+
by calling [`genentropy`](@ref) with `est`, and with the array data.
29+
To apply this to timeseries of spatial data, simply loop over the call, e.g.:
30+
```julia
31+
entropy = genentropy(x, est)
32+
entropy_vs_time = genentropy.(data, est) # broadcasting with `.`
33+
```
34+
35+
The argument `periodic` decides whether the stencil should wrap around at the end of the
36+
array. If `periodic = false`, pixels whose stencil exceeds the array bounds are skipped.
37+
38+
[^Ribeiro2012]:
39+
Ribeiro et al. (2012). Complexity-entropy causality plane as a complexity measure
40+
for two-dimensional patterns. https://doi.org/10.1371/journal.pone.0040689
41+
42+
[^Schlemmer2018]:
43+
Schlemmer et al. (2018). Spatiotemporal Permutation Entropy as a Measure for
44+
Complexity of Cardiac Arrhythmia. https://doi.org/10.3389/fphy.2018.00039
45+
"""
46+
struct SpatialSymbolicPermutation{D,P,V} <: ProbabilitiesEstimator
47+
stencil::Vector{CartesianIndex{D}}
48+
viewer::Vector{CartesianIndex{D}}
49+
arraysize::Dims{D}
50+
valid::V
51+
end
52+
function SpatialSymbolicPermutation(
53+
stencil::Vector{CartesianIndex{D}}, x::AbstractArray, p::Bool = true
54+
) where {D}
55+
# Ensure that no offset is part of the stencil
56+
stencil = pushfirst!(copy(stencil), CartesianIndex{D}(zeros(Int, D)...))
57+
arraysize = size(x)
58+
@assert length(arraysize) == D "Indices and input array must match dimensionality!"
59+
# Store valid indices for later iteration
60+
if p
61+
valid = CartesianIndices(x)
62+
else
63+
# collect maximum offsets in each dimension for limiting ranges
64+
maxoffsets = [maximum(s[i] for s in stencil) for i in 1:D]
65+
# Safety check
66+
minoffsets = [min(0, minimum(s[i] for s in stencil)) for i in 1:D]
67+
ranges = Iterators.product(
68+
[(1-minoffsets[i]):(arraysize[i]-maxoffsets[i]) for i in 1:D]...
69+
)
70+
valid = Base.Generator(idxs -> CartesianIndex{D}(idxs), ranges)
71+
end
72+
SpatialSymbolicPermutation{D, p, typeof(valid)}(stencil, copy(stencil), arraysize, valid)
73+
end
74+
75+
# This source code is a modification of the code of Agents.jl that finds neighbors
76+
# in grid-like spaces. It's the code of `nearby_positions` in `grid_general.jl`.
77+
function pixels_in_stencil(pixel, spatperm::SpatialSymbolicPermutation{D,false}) where {D}
78+
@inbounds for i in eachindex(spatperm.stencil)
79+
spatperm.viewer[i] = spatperm.stencil[i] + pixel
80+
end
81+
return spatperm.viewer
82+
end
83+
84+
function pixels_in_stencil(pixel, spatperm::SpatialSymbolicPermutation{D,true}) where {D}
85+
@inbounds for i in eachindex(spatperm.stencil)
86+
# It's annoying that we have to change to tuple and then to CartesianIndex
87+
# because iteration over cartesian indices is not allowed. But oh well.
88+
spatperm.viewer[i] = CartesianIndex{D}(
89+
mod1.(Tuple(spatperm.stencil[i] + pixel), spatperm.arraysize)
90+
)
91+
end
92+
return spatperm.viewer
93+
end
94+
95+
function Entropies.probabilities(x, est::SpatialSymbolicPermutation)
96+
# TODO: This can be literally a call to `symbolize` and then
97+
# calling probabilities on it. Should do once the `symbolize` refactoring is done.
98+
s = zeros(Int, length(est.valid))
99+
probabilities!(s, x, est)
100+
end
101+
102+
function Entropies.probabilities!(s, x, est::SpatialSymbolicPermutation)
103+
m = length(est.stencil)
104+
for (i, pixel) in enumerate(est.valid)
105+
pixels = pixels_in_stencil(pixel, est)
106+
s[i] = Entropies.encode_motif(view(x, pixels), m)
107+
end
108+
@show s
109+
return probabilities(s)
110+
end
111+
112+
# Pretty printing
113+
function Base.show(io::IO, est::SpatialSymbolicPermutation{D}) where {D}
114+
print(io, "Spatial permutation estimator for $D-dimensional data. Stencil:")
115+
print(io, "\n")
116+
print(io, est.stencil)
117+
end

src/symbolic/symbolic.jl

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ include("utils.jl")
1010
include("SymbolicPermutation.jl")
1111
include("SymbolicWeightedPermutation.jl")
1212
include("SymbolicAmplitudeAware.jl")
13+
include("spatial_permutation.jl")
1314

1415
"""
1516
permentropy(x; τ = 1, m = 3, base = MathConstants.e)

test/runtests.jl

+2
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,5 @@ end
382382
@assert round(tsallisentropy(p, q = -1/2, k = 1), digits = 2) 6.79
383383
end
384384
end
385+
386+
include("spatial_permutation_tests.jl")

test/spatial_permutation_tests.jl

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Entropies, Test
2+
3+
@testset "Spatial Permutation Entropy" begin
4+
x = [1 2 1; 8 3 4; 6 7 5]
5+
# Re-create the Ribeiro et al, 2012 using stencil
6+
# (you get 4 symbols in a 3x3 matrix. For this matrix, the upper left
7+
# and bottom right are the same symbol. So three probabilities in the end).
8+
stencil = CartesianIndex.([(1,0), (0,1), (1,1)])
9+
est = SpatialSymbolicPermutation(stencil, x, false)
10+
11+
# Generic tests
12+
ps = probabilities(x, est)
13+
@test ps isa Probabilities
14+
@test length(ps) == 3
15+
@test sort(ps) == [0.25, 0.25, 0.5]
16+
17+
h = genentropy(x, est, base = 2)
18+
@test h == 1.5
19+
20+
# In fact, doesn't matter how we order the stencil,
21+
# the symbols will always be equal in top-left and bottom-right
22+
stencil = CartesianIndex.([(1,0), (1,1), (0,1)])
23+
est = SpatialSymbolicPermutation(stencil, x, false)
24+
@test genentropy(x, est, base = 2) == 1.5
25+
26+
# But for sanity, let's ensure we get a different number
27+
# for a different stencil
28+
stencil = CartesianIndex.([(1,0), (2,0)])
29+
est = SpatialSymbolicPermutation(stencil, x, false)
30+
ps = sort(probabilities(x, est))
31+
@test ps[1] == 1/3
32+
@test ps[2] == 2/3
33+
34+
# TODO: Symbolize tests once its part of the API.
35+
36+
# Also test that it works for arbitrarily high-dimensional arrays
37+
stencil = CartesianIndex.([(0,1,0), (0,0,1), (1,0,0)])
38+
z = reshape(1:125, (5,5,5))
39+
est = SpatialSymbolicPermutation(stencil, z, false)
40+
# Analytically the total stencils are of length 4*4*4 = 64
41+
# but all of them given the same probabilities because of the layout
42+
ps = probabilities(z, est)
43+
@test ps == [1]
44+
# if we shuffle, we get random stuff
45+
using Random
46+
w = shuffle!(Random.MersenneTwister(42), collect(z))
47+
ps = probabilities(w, est)
48+
@test length(ps) > 1
49+
end

0 commit comments

Comments
 (0)