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
0 commit comments