Skip to content

Commit 70f61e2

Browse files
authored
Merge pull request #90 from JuliaDynamics/as/∥recurrence
Parallelize recurrence plots v2.0
2 parents 935a584 + 16d3580 commit 70f61e2

File tree

5 files changed

+129
-17
lines changed

5 files changed

+129
-17
lines changed

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# RecurrenceAnalysis.jl News
22

3+
## v1.2.0
4+
*(no notable changes until so far)*
5+
6+
- Thread-parallelized computation of recurrence plots is now possible.
7+
38
## v0.12.0
49
- Doc improvements
510
- Return type changes in `rqa`

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "RecurrenceAnalysis"
22
uuid = "639c3291-70d9-5ea2-8c5b-839eba1ee399"
33
repo = "https://github.com/JuliaDynamics/RecurrenceAnalysis.jl.git"
4-
version = "1.1.1"
4+
version = "1.2.0"
55

66
[deps]
77
DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f"

benchmarks/parallel_vs_serial.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#=
2+
This file benchmarks the parallel and serial computation of a recurrence matrix
3+
=#
4+
using DynamicalSystemsBase, RecurrenceAnalysis
5+
using Statistics, BenchmarkTools, Base.Threads
6+
7+
Ns = round.(Int, 10.0 .^ (2:0.5:4.5))
8+
ro = Systems.roessler()
9+
10+
println("I am using $(nthreads()) threads. I am reporting results as:")
11+
println("time of (parallel/serial) for 3D and 1D trajectories.")
12+
for N in Ns
13+
tr = trajectory(ro, N*0.1; dt = 0.1, Tr = 10.0)
14+
println("For N = $(length(tr))...")
15+
x = tr[:, 1]
16+
b_serial = @benchmark RecurrenceMatrix($(tr), 5.0; metric = Euclidean(), parallel = false)
17+
b_parallel = @benchmark RecurrenceMatrix($(tr), 5.0; metric = Euclidean(), parallel = true)
18+
b_s_1 = @benchmark RecurrenceMatrix($(x), 2.0; parallel = false)
19+
b_p_1 = @benchmark RecurrenceMatrix($(x), 2.0; parallel = true)
20+
threeD = median(b_serial.times)/median(b_parallel.times)
21+
oneD = median(b_s_1.times)/median(b_p_1.times)
22+
threeD, oneD = round.((threeD, oneD); sigdigits = 3)
23+
println("3D=$(threeD), 1D=$(oneD)")
24+
end

src/matrices.jl

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ end
7979
################################################################################
8080
abstract type AbstractRecurrenceMatrix end
8181
const ARM = AbstractRecurrenceMatrix
82+
8283
struct RecurrenceMatrix <: AbstractRecurrenceMatrix
8384
data::SparseMatrixCSC{Bool,Int}
8485
end
@@ -133,7 +134,7 @@ Objects of type `<:AbstractRecurrenceMatrix` are displayed as a [`recurrenceplot
133134
134135
## Description
135136
136-
The recurrence matrix is a numeric representation of a "recurrence plot" [1, 2],
137+
The recurrence matrix is a numeric representation of a "recurrence plot" [^1, ^2],
137138
in the form of a sparse square matrix of Boolean values.
138139
139140
`x` must be a `Vector` or a `Dataset` or a `Matrix` with data points in rows
@@ -155,22 +156,26 @@ by the following keyword arguments:
155156
and `scale` is ignored.
156157
* `metric="euclidean"` : metric of the distances, either `Metric` or a string,
157158
as in [`distancematrix`](@ref).
159+
* `parallel=false` : whether to parallelize the computation of the recurrence
160+
matrix. This will split the computation of the matrix across multiple threads.
161+
This **may not work on Julia versions before v1.3**, so be warned!
158162
159163
See also: [`CrossRecurrenceMatrix`](@ref), [`JointRecurrenceMatrix`](@ref) and
160164
use [`recurrenceplot`](@ref) to turn the result of these functions into a plottable format.
161165
162166
## References
163-
[1] : N. Marwan *et al.*, "Recurrence plots for the analysis of complex systems",
167+
[^1] : N. Marwan *et al.*, "Recurrence plots for the analysis of complex systems",
164168
*Phys. Reports 438*(5-6), 237-329 (2007).
165169
166-
[2] : N. Marwan & C.L. Webber, "Mathematical and computational foundations of
170+
[^2] : N. Marwan & C.L. Webber, "Mathematical and computational foundations of
167171
recurrence quantifications", in: Webber, C.L. & N. Marwan (eds.), *Recurrence
168172
Quantification Analysis. Theory and Best Practices*, Springer, pp. 3-43 (2015).
169173
"""
170-
function RecurrenceMatrix(x, ε; metric = DEFAULT_METRIC, kwargs...)
174+
function RecurrenceMatrix(x, ε; metric = DEFAULT_METRIC,
175+
parallel::Bool = length(x) > 500 && Threads.nthreads() > 1, kwargs...)
171176
m = getmetric(metric)
172177
s = resolve_scale(x, m, ε; kwargs...)
173-
m = recurrence_matrix(x, m, s)
178+
m = recurrence_matrix(x, m, s, Val(parallel))
174179
return RecurrenceMatrix(m)
175180
end
176181

@@ -187,10 +192,11 @@ then the cell `(i, j)` of the matrix will have a `true` value.
187192
See [`RecurrenceMatrix`](@ref) for details, references and keywords.
188193
See also: [`JointRecurrenceMatrix`](@ref).
189194
"""
190-
function CrossRecurrenceMatrix(x, y, ε; metric = DEFAULT_METRIC, kwargs...)
195+
function CrossRecurrenceMatrix(x, y, ε; metric = DEFAULT_METRIC,
196+
parallel::Bool = length(x) > 500 && Threads.nthreads() > 1, kwargs...)
191197
m = getmetric(metric)
192198
s = resolve_scale(x, y, m, ε; kwargs...)
193-
m = recurrence_matrix(x, y, m, s)
199+
m = recurrence_matrix(x, y, m, s, Val(parallel))
194200
return CrossRecurrenceMatrix(m)
195201
end
196202

@@ -293,18 +299,28 @@ end
293299
################################################################################
294300
# recurrence_matrix - Low level interface
295301
################################################################################
296-
# TODO: increase the efficiency here by not computing everything:
297-
recurrence_matrix(x, metric::Metric, ε::Real) =
298-
recurrence_matrix(x, x, metric, ε)
302+
# TODO: increase the efficiency here by not computing everything!
303+
304+
"""
305+
recurrence_matrix(x, y, metric::Metric, ε::Real, parallel::Val)
306+
307+
Return a sparse matrix which encodes recurrence points.
308+
309+
Note that `parallel` may be either `Val(true)` or `Val(false)`.
310+
"""
311+
recurrence_matrix(x, metric::Metric, ε::Real, parallel::Val) =
312+
recurrence_matrix(x, x, metric, ε, parallel)
299313

300314
# Convert Matrices to Datasets (better performance in all cases)
301315
function recurrence_matrix(x::AbstractMatrix, y::AbstractMatrix,
302-
metric::Metric, ε)
303-
return recurrence_matrix(Dataset(x), Dataset(y), metric, ε)
316+
metric::Metric, ε, parallel::Val)
317+
return recurrence_matrix(Dataset(x), Dataset(y), metric, ε, parallel)
304318
end
305319

320+
# First, we define the non-parallel versions.
321+
306322
# Core function
307-
function recurrence_matrix(xx::Dataset, yy::Dataset, metric::Metric, ε)
323+
function recurrence_matrix(xx::Dataset, yy::Dataset, metric::Metric, ε, parallel::Val{false})
308324
x = xx.data
309325
y = yy.data
310326
rowvals = Vector{Int}()
@@ -324,7 +340,7 @@ function recurrence_matrix(xx::Dataset, yy::Dataset, metric::Metric, ε)
324340
end
325341

326342
# Vector version can be more specialized (and metric is irrelevant)
327-
function recurrence_matrix(x::AbstractVector, y::AbstractVector, metric, ε)
343+
function recurrence_matrix(x::AbstractVector, y::AbstractVector, metric, ε, parallel::Val{false})
328344
rowvals = Vector{Int}()
329345
colvals = Vector{Int}()
330346
for j in 1:length(y)
@@ -340,3 +356,59 @@ function recurrence_matrix(x::AbstractVector, y::AbstractVector, metric, ε)
340356
nzvals = fill(true, (length(rowvals),))
341357
return sparse(rowvals, colvals, nzvals, length(x), length(y))
342358
end
359+
360+
# Now, we define the parallel versions of these functions.
361+
362+
# Core function
363+
function recurrence_matrix(xx::Dataset, yy::Dataset, metric::Metric, ε, parallel::Val{true})
364+
x = xx.data
365+
y = yy.data
366+
# We create an `Array` of `Array`s, for each thread to have its
367+
# own array to push to. This avoids race conditions with
368+
# multiple threads pushing to the same `Array` (`Array`s are not atomic).
369+
rowvals = [Vector{Int}() for _ in 1:Threads.nthreads()]
370+
colvals = [Vector{Int}() for _ in 1:Threads.nthreads()]
371+
372+
# This is the same logic as the serial function, but parallelized.
373+
Threads.@threads for j in 1:length(y)
374+
threadn = Threads.threadid()
375+
nzcol = 0
376+
for i in 1:length(x)
377+
@inbounds if evaluate(metric, x[i], y[j]) ε
378+
push!(rowvals[threadn], i) # push to the thread-specific row array
379+
nzcol += 1
380+
end
381+
end
382+
append!(colvals[threadn], fill(j, (nzcol,)))
383+
end
384+
finalrows = vcat(rowvals...) # merge into one array
385+
finalcols = vcat(colvals...) # merge into one array
386+
nzvals = fill(true, (length(finalrows),))
387+
return sparse(finalrows, finalcols, nzvals, length(x), length(y))
388+
end
389+
390+
# Vector version can be more specialized (and metric is irrelevant)
391+
function recurrence_matrix(x::AbstractVector, y::AbstractVector, metric, ε, parallel::Val{true})
392+
# We create an `Array` of `Array`s, for each thread to have its
393+
# own array to push to. This avoids race conditions with
394+
# multiple threads pushing to the same `Array` (`Array`s are not atomic).
395+
rowvals = [Vector{Int}() for _ in 1:Threads.nthreads()]
396+
colvals = [Vector{Int}() for _ in 1:Threads.nthreads()]
397+
398+
# This is the same logic as the serial function, but parallelized.
399+
Threads.@threads for j in 1:length(y)
400+
threadn = Threads.threadid()
401+
nzcol = 0
402+
for i in 1:length(x)
403+
@inbounds if abs(x[i] - y[j]) ε
404+
push!(rowvals[threadn], i) # push to the thread-specific row array
405+
nzcol += 1
406+
end
407+
end
408+
append!(colvals[threadn], fill(j, (nzcol,)))
409+
end
410+
finalrows = vcat(rowvals...) # merge into one array
411+
finalcols = vcat(colvals...) # merge into one array
412+
nzvals = fill(true, (length(finalrows),))
413+
return sparse(finalrows, finalcols, nzvals, length(x), length(y))
414+
end

test/dynamicalsystems.jl

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,34 @@ dict_keys = ["Sine wave","White noise","Hénon (chaotic)","Hénon (periodic)"]
4242
dmat = distancematrix(xe, ye)
4343
crmat = CrossRecurrenceMatrix(xe, ye, ε)
4444
@test Matrix(crmat.data) == (dmat .≤ ε)
45-
rmat = RecurrenceMatrix(xe, ε)
46-
jrmat = JointRecurrenceMatrix(xe, ye, ε)
45+
rmat = RecurrenceMatrix(xe, ε; parallel = false)
46+
rmat_p = RecurrenceMatrix(xe, ε; parallel = true)
47+
jrmat = JointRecurrenceMatrix(xe, ye, ε; parallel = false)
4748
sz = size(jrmat)
4849
@test (jrmat.data .& rmat.data[1:sz[1],1:sz[2]]) == jrmat.data
50+
@test (jrmat.data .& rmat_p.data[1:sz[1],1:sz[2]]) == jrmat.data
4951
# Compare metrics
5052
m_euc = rmat
53+
m_euc_p = rmat_p
5154
m_max = RecurrenceMatrix(xe, ε, metric="max")
5255
m_min = RecurrenceMatrix(xe, ε, metric="manhattan")
5356
@test (m_max.data .& m_euc.data) == m_euc.data
5457
@test (m_euc.data .& m_min.data) == m_min.data
58+
@test (m_max.data .& m_euc_p.data) == m_euc.data
59+
@test (m_euc_p.data .& m_min.data) == m_min.data
5560
# Compare scales
5661
m_scalemax = CrossRecurrenceMatrix(xe, ye, 0.1, scale=maximum)
5762
m_scalemean = CrossRecurrenceMatrix(xe, ye, 0.1, scale=mean)
63+
m_scalemax_p = CrossRecurrenceMatrix(xe, ye, 0.1, scale=maximum, parallel = true)
64+
m_scalemean_p = CrossRecurrenceMatrix(xe, ye, 0.1, scale=mean, parallel = true)
5865
@test (m_scalemax.data .& m_scalemean.data) == m_scalemean.data
66+
@test (m_scalemax_p.data .& m_scalemean_p.data) == m_scalemean.data
5967
# Fixed rate for recurrence matrix
6068
crmat_fixed = CrossRecurrenceMatrix(xe, ye, 0.05; fixedrate=true)
69+
crmat_fixed_p = CrossRecurrenceMatrix(xe, ye, 0.05; fixedrate=true, parallel = true)
6170
@test .04 < recurrencerate(crmat_fixed) < .06
71+
@test .04 < recurrencerate(crmat_fixed_p) < .06
72+
@test recurrencerate(crmat_fixed) recurrencerate(crmat_fixed_p)
6273

6374
# Recurrence plot
6475
crp = grayscale(crmat, width=125)

0 commit comments

Comments
 (0)