Skip to content

Commit d1bae2d

Browse files
Implement interactive 2D dynamical system clicker (#252)
* Implement interactive 2D dynamical system clicker * Rewrite `interactive_poincaresos` in terms of `interactive_clicker` * Address review comments * Fix `interactive_poincaresos` * Update docs * Update visualizations doc * Remove accidentally commited generated file * finish and generalize 2D clicker * changelog * add example --------- Co-authored-by: Datseris <[email protected]>
1 parent 1345758 commit d1bae2d

File tree

7 files changed

+207
-109
lines changed

7 files changed

+207
-109
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ The changelog here therefore lists either major changes to the overarching Dynam
77

88
The changelogs of individual sub-packages are self-contained for each package.
99

10+
# v3.6
11+
12+
- New interactive GUI function: `interactive_2d_clicker`.
1013

1114
# v3.5
1215

Project.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DynamicalSystems"
22
uuid = "61744808-ddfa-5f27-97ff-6e42cc95d634"
33
repo = "https://github.com/JuliaDynamics/DynamicalSystems.jl.git"
4-
version = "3.5.0"
4+
version = "3.6.0"
55

66
[deps]
77
Attractors = "f3fd9213-ca85-4dba-9dfd-7fc91308fec7"
@@ -11,13 +11,14 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
1111
DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f"
1212
DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4"
1313
FractalDimensions = "4665ce21-e117-4649-aed8-08bbe5ccbead"
14+
PeriodicOrbits = "41be5fce-5647-450b-ae37-a6739b881a1c"
1415
PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14"
1516
RecurrenceAnalysis = "639c3291-70d9-5ea2-8c5b-839eba1ee399"
1617
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
18+
SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955"
1719
StateSpaceSets = "40b095a5-5852-4c12-98c7-d43bf788e795"
20+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1821
TimeseriesSurrogates = "c804724b-8c18-5caa-8579-6025a0767c70"
19-
SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955"
20-
PeriodicOrbits = "41be5fce-5647-450b-ae37-a6739b881a1c"
2122

2223
[weakdeps]
2324
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
@@ -34,13 +35,14 @@ DelayEmbeddings = "2.7"
3435
DynamicalSystemsBase = "3.11.0"
3536
FractalDimensions = "1"
3637
Makie = "≥ 0.19"
38+
PeriodicOrbits = "0.1"
3739
PredefinedDynamicalSystems = "1"
3840
RecurrenceAnalysis = "2"
3941
Reexport = "1"
42+
SignalDecomposition = "1.2"
4043
StateSpaceSets = "2"
44+
Statistics = "1"
4145
TimeseriesSurrogates = "2.1"
42-
SignalDecomposition = "1.2"
43-
PeriodicOrbits = "0.1"
4446
julia = "1.9"
4547

4648
[extras]

docs/src/visualizations.md

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,25 @@ oddata = interactive_orbitdiagram(ds, p_index, p_min, p_max, i;
240240
ps, us = scaleod(oddata)
241241
```
242242

243+
## Interactive 2D clicker
244+
245+
```@docs
246+
interactive_2d_clicker
247+
```
248+
249+
Example:
250+
251+
```julia
252+
using GLMakie, DynamicalSystems
253+
254+
lorenz = Systems.lorenz()
255+
projection = [1, 2]
256+
complete_state = [12.0]
257+
projected_ds = ProjectedDynamicalSystem(lorenz, projection, complete_state)
258+
259+
interactive_2d_clicker(projected_ds; Δt = 0.01, times = 10:100)
260+
```
261+
243262
## Interactive Poincaré Surface of Section
244263

245264
```@raw html
@@ -254,14 +273,12 @@ interactive_poincaresos
254273

255274
To generate the animation at the start of this section you can run
256275
```julia
257-
using InteractiveDynamics, GLMakie, OrdinaryDiffEq, DynamicalSystems
258-
diffeq = (alg = Vern9(), abstol = 1e-9, reltol = 1e-9)
259-
276+
using DynamicalSystems, GLMakie
260277
hh = Systems.henonheiles()
261-
278+
hh = CoupledODEs(hh, (abstol = 1e-9, reltol = 1e-9))
262279
potential(x, y) = 0.5(x^2 + y^2) + (x^2*y - (y^3)/3)
263280
energy(x,y,px,py) = 0.5(px^2 + py^2) + potential(x,y)
264-
const E = energy(get_state(hh)...)
281+
const E = energy(current_state(hh)...)
265282

266283
function complete(y, py, x)
267284
V = potential(x, y)
@@ -276,13 +293,18 @@ plane = (1, 0.0) # first variable crossing 0
276293

277294
# Coloring points using the Lyapunov exponent
278295
function λcolor(u)
279-
λ = lyapunovs(hh, 4000; u0 = u)[1]
296+
u0 = complete(u..., 0.0)
297+
λ = lyapunov(hh, 4000; u0)
280298
λmax = 0.1
281-
return RGBf(0, 0, clamp/λmax, 0, 1))
299+
level = clamp/λmax, 0, 1)
300+
return RGBf(level, 0, level)
282301
end
283302

284-
state, scene = interactive_poincaresos(hh, plane, (2, 4), complete;
285-
labels = ("q₂" , "p₂"), color = λcolor, diffeq...)
303+
figure, state = interactive_poincaresos(hh, plane, (2, 4), complete; color = λcolor)
304+
305+
ax = content(figure[1,1][1,1])
306+
ax.xlabel, ax.ylabel = ("q₂" , "p₂")
307+
figure
286308
```
287309

288310
## Scanning a Poincaré Surface of Section
@@ -318,3 +340,28 @@ j = 2 # the dimension of the plane
318340

319341
interactive_poincaresos_scan(trs, j; linekw = (transparency = true,))
320342
```
343+
344+
## Interactive 2D dynamical system
345+
346+
```@docs
347+
interactive_clicker
348+
```
349+
350+
The `interactive_clicker` function can be used to spin up a GUI
351+
for interactively exploring the state space of a 2D dynamical system.
352+
353+
For example, the following code show how to interactively explore a
354+
[`ProjectedDynamicalSystem`](@ref):
355+
356+
```julia
357+
using GLMakie, DynamicalSystems
358+
359+
# This is the 3D Lorenz model
360+
lorenz = Systems.lorenz()
361+
362+
projection = [1, 2]
363+
complete_state = [0.0]
364+
projected_ds = ProjectedDynamicalSystem(lorenz, projection, complete_state)
365+
366+
interactive_clicker(projected_ds; tfinal = (10.0, 150.0))
367+
```

ext/DynamicalSystemsVisualizations.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ include("src/dynamicalsystemobservable.jl")
88
include("src/interactive_trajectory.jl")
99
include("src/cobweb.jl")
1010
include("src/orbitdiagram.jl")
11-
include("src/poincareclick.jl")
1211
include("src/brainscan.jl")
12+
include("src/2dclicker.jl")
1313

1414
subscript = DynamicalSystemsVisualizations.subscript
1515

16-
end
16+
end

ext/src/2dclicker.jl

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
function DynamicalSystems.interactive_2d_clicker(ds;
2+
# DynamicalSystems kwargs:
3+
times = 100:10_000,
4+
Δt = 1,
5+
# Makie kwargs:
6+
color = randomcolor,
7+
plotkwargs = ()
8+
)
9+
10+
figure = Figure(size = (1000, 800))
11+
12+
T_slider, m_slider = _add_clicker_controls!(figure, times)
13+
ax = figure[1, :] = Axis(figure; tellheight = true)
14+
15+
# Compute the initial plot
16+
u0 = DynamicalSystems.current_state(ds)
17+
data, = trajectory(ds, T_slider[]; Δt)
18+
positions_node = Observable(data)
19+
colors = (c = color(u0); [c for _ in 1:length(data)])
20+
colors_node = Observable(colors)
21+
22+
if isdiscretetime(ds)
23+
scatter!(
24+
ax, positions_node, color = colors_node,
25+
markersize = lift(o -> o*px, m_slider), marker = :circle, plotkwargs...
26+
)
27+
else
28+
scatterlines!(
29+
ax, positions_node, color = colors_node,
30+
markersize = lift(o -> o*px, m_slider), marker = :circle, plotkwargs...
31+
)
32+
end
33+
34+
# Interactive clicking on the phase space:
35+
laststate = Observable(u0)
36+
Makie.deactivate_interaction!(ax, :rectanglezoom)
37+
spoint = select_point(ax.scene)
38+
on(spoint) do newstate
39+
data, = trajectory(ds, T_slider[], newstate; Δt)
40+
pushfirst!(vec(data), fill(NaN, dimension(data))) # ensures break for scatterlines
41+
positions = positions_node[]; colors = colors_node[]
42+
append!(positions, data)
43+
c = color(newstate)
44+
append!(colors, fill(c, length(data)))
45+
# Update all the observables with Array as value:
46+
positions_node[], colors_node[], laststate[] = positions, colors, newstate
47+
end
48+
49+
display(figure)
50+
return figure, laststate
51+
end
52+
53+
function _add_clicker_controls!(figure, times)
54+
sg1 = SliderGrid(figure[2, :][1, 1],
55+
(label = "T", range = times,
56+
format = x -> string(round(x)),
57+
startvalue = times[1])
58+
)
59+
sg2 = SliderGrid(figure[2, :][1, 2],
60+
(label = "ms", range = 10.0 .^ range(0, 2, length = 100),
61+
format = x -> string(round(x)), startvalue = 10)
62+
)
63+
return sg1.sliders[1].value, sg2.sliders[1].value
64+
end
65+
66+
# interactive psos is based in the 2D clicker
67+
function DynamicalSystems.interactive_poincaresos(ds, plane, idxs, complete;
68+
# PSOS kwargs:
69+
direction = -1,
70+
rootkw = (xrtol = 1e-6, atol = 1e-6),
71+
Tmax = 1e3,
72+
kw...
73+
)
74+
75+
# Basic sanity checks on the method arguments
76+
@assert typeof(plane) <: Tuple
77+
@assert length(idxs) == 2
78+
@assert eltype(idxs) == Int
79+
@assert plane[1] idxs
80+
81+
i = DynamicalSystems.SVector{2, Int}(idxs)
82+
83+
# Construct a new `PoincareMap` structure with the given parameters
84+
pmap = DynamicalSystems.DynamicalSystemsBase.PoincareMap(ds, plane;
85+
direction, rootkw, Tmax)
86+
87+
# construct a 2d projected system compatible with the clicker
88+
z = plane[2] # third variable comes from plane
89+
complete_state = u -> complete(u..., z)
90+
project = i
91+
newds = ProjectedDynamicalSystem(pmap, project, complete_state)
92+
return interactive_2d_clicker(newds; kw...)
93+
end

ext/src/poincareclick.jl

Lines changed: 0 additions & 83 deletions
This file was deleted.

0 commit comments

Comments
 (0)