diff --git a/CHANGELOG.md b/CHANGELOG.md index 2056b2f5..3020753b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The changelog here therefore lists either major changes to the overarching Dynam The changelogs of individual sub-packages are self-contained for each package. +# v3.6 + +- New interactive GUI function: `interactive_2d_clicker`. # v3.5 diff --git a/Project.toml b/Project.toml index b8fd6977..a6abc82d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DynamicalSystems" uuid = "61744808-ddfa-5f27-97ff-6e42cc95d634" repo = "https://github.com/JuliaDynamics/DynamicalSystems.jl.git" -version = "3.5.0" +version = "3.6.0" [deps] Attractors = "f3fd9213-ca85-4dba-9dfd-7fc91308fec7" @@ -11,13 +11,14 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f" DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" FractalDimensions = "4665ce21-e117-4649-aed8-08bbe5ccbead" +PeriodicOrbits = "41be5fce-5647-450b-ae37-a6739b881a1c" PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" RecurrenceAnalysis = "639c3291-70d9-5ea2-8c5b-839eba1ee399" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955" StateSpaceSets = "40b095a5-5852-4c12-98c7-d43bf788e795" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TimeseriesSurrogates = "c804724b-8c18-5caa-8579-6025a0767c70" -SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955" -PeriodicOrbits = "41be5fce-5647-450b-ae37-a6739b881a1c" [weakdeps] Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" @@ -34,13 +35,14 @@ DelayEmbeddings = "2.7" DynamicalSystemsBase = "3.11.0" FractalDimensions = "1" Makie = "≥ 0.19" +PeriodicOrbits = "0.1" PredefinedDynamicalSystems = "1" RecurrenceAnalysis = "2" Reexport = "1" +SignalDecomposition = "1.2" StateSpaceSets = "2" +Statistics = "1" TimeseriesSurrogates = "2.1" -SignalDecomposition = "1.2" -PeriodicOrbits = "0.1" julia = "1.9" [extras] diff --git a/docs/src/visualizations.md b/docs/src/visualizations.md index b5d00c23..51fbdf2f 100644 --- a/docs/src/visualizations.md +++ b/docs/src/visualizations.md @@ -240,6 +240,25 @@ oddata = interactive_orbitdiagram(ds, p_index, p_min, p_max, i; ps, us = scaleod(oddata) ``` +## Interactive 2D clicker + +```@docs +interactive_2d_clicker +``` + +Example: + +```julia +using GLMakie, DynamicalSystems + +lorenz = Systems.lorenz() +projection = [1, 2] +complete_state = [12.0] +projected_ds = ProjectedDynamicalSystem(lorenz, projection, complete_state) + +interactive_2d_clicker(projected_ds; Δt = 0.01, times = 10:100) +``` + ## Interactive Poincaré Surface of Section ```@raw html @@ -254,14 +273,12 @@ interactive_poincaresos To generate the animation at the start of this section you can run ```julia -using InteractiveDynamics, GLMakie, OrdinaryDiffEq, DynamicalSystems -diffeq = (alg = Vern9(), abstol = 1e-9, reltol = 1e-9) - +using DynamicalSystems, GLMakie hh = Systems.henonheiles() - +hh = CoupledODEs(hh, (abstol = 1e-9, reltol = 1e-9)) potential(x, y) = 0.5(x^2 + y^2) + (x^2*y - (y^3)/3) energy(x,y,px,py) = 0.5(px^2 + py^2) + potential(x,y) -const E = energy(get_state(hh)...) +const E = energy(current_state(hh)...) function complete(y, py, x) V = potential(x, y) @@ -276,13 +293,18 @@ plane = (1, 0.0) # first variable crossing 0 # Coloring points using the Lyapunov exponent function λcolor(u) - λ = lyapunovs(hh, 4000; u0 = u)[1] + u0 = complete(u..., 0.0) + λ = lyapunov(hh, 4000; u0) λmax = 0.1 - return RGBf(0, 0, clamp(λ/λmax, 0, 1)) + level = clamp(λ/λmax, 0, 1) + return RGBf(level, 0, level) end -state, scene = interactive_poincaresos(hh, plane, (2, 4), complete; -labels = ("q₂" , "p₂"), color = λcolor, diffeq...) +figure, state = interactive_poincaresos(hh, plane, (2, 4), complete; color = λcolor) + +ax = content(figure[1,1][1,1]) +ax.xlabel, ax.ylabel = ("q₂" , "p₂") +figure ``` ## Scanning a Poincaré Surface of Section @@ -318,3 +340,28 @@ j = 2 # the dimension of the plane interactive_poincaresos_scan(trs, j; linekw = (transparency = true,)) ``` + +## Interactive 2D dynamical system + +```@docs +interactive_clicker +``` + +The `interactive_clicker` function can be used to spin up a GUI +for interactively exploring the state space of a 2D dynamical system. + +For example, the following code show how to interactively explore a +[`ProjectedDynamicalSystem`](@ref): + +```julia +using GLMakie, DynamicalSystems + +# This is the 3D Lorenz model +lorenz = Systems.lorenz() + +projection = [1, 2] +complete_state = [0.0] +projected_ds = ProjectedDynamicalSystem(lorenz, projection, complete_state) + +interactive_clicker(projected_ds; tfinal = (10.0, 150.0)) +``` diff --git a/ext/DynamicalSystemsVisualizations.jl b/ext/DynamicalSystemsVisualizations.jl index 293065f5..12c51415 100644 --- a/ext/DynamicalSystemsVisualizations.jl +++ b/ext/DynamicalSystemsVisualizations.jl @@ -8,9 +8,9 @@ include("src/dynamicalsystemobservable.jl") include("src/interactive_trajectory.jl") include("src/cobweb.jl") include("src/orbitdiagram.jl") -include("src/poincareclick.jl") include("src/brainscan.jl") +include("src/2dclicker.jl") subscript = DynamicalSystemsVisualizations.subscript -end \ No newline at end of file +end diff --git a/ext/src/2dclicker.jl b/ext/src/2dclicker.jl new file mode 100644 index 00000000..e7d22c7b --- /dev/null +++ b/ext/src/2dclicker.jl @@ -0,0 +1,93 @@ +function DynamicalSystems.interactive_2d_clicker(ds; + # DynamicalSystems kwargs: + times = 100:10_000, + Δt = 1, + # Makie kwargs: + color = randomcolor, + plotkwargs = () + ) + + figure = Figure(size = (1000, 800)) + + T_slider, m_slider = _add_clicker_controls!(figure, times) + ax = figure[1, :] = Axis(figure; tellheight = true) + + # Compute the initial plot + u0 = DynamicalSystems.current_state(ds) + data, = trajectory(ds, T_slider[]; Δt) + positions_node = Observable(data) + colors = (c = color(u0); [c for _ in 1:length(data)]) + colors_node = Observable(colors) + + if isdiscretetime(ds) + scatter!( + ax, positions_node, color = colors_node, + markersize = lift(o -> o*px, m_slider), marker = :circle, plotkwargs... + ) + else + scatterlines!( + ax, positions_node, color = colors_node, + markersize = lift(o -> o*px, m_slider), marker = :circle, plotkwargs... + ) + end + + # Interactive clicking on the phase space: + laststate = Observable(u0) + Makie.deactivate_interaction!(ax, :rectanglezoom) + spoint = select_point(ax.scene) + on(spoint) do newstate + data, = trajectory(ds, T_slider[], newstate; Δt) + pushfirst!(vec(data), fill(NaN, dimension(data))) # ensures break for scatterlines + positions = positions_node[]; colors = colors_node[] + append!(positions, data) + c = color(newstate) + append!(colors, fill(c, length(data))) + # Update all the observables with Array as value: + positions_node[], colors_node[], laststate[] = positions, colors, newstate + end + + display(figure) + return figure, laststate +end + +function _add_clicker_controls!(figure, times) + sg1 = SliderGrid(figure[2, :][1, 1], + (label = "T", range = times, + format = x -> string(round(x)), + startvalue = times[1]) + ) + sg2 = SliderGrid(figure[2, :][1, 2], + (label = "ms", range = 10.0 .^ range(0, 2, length = 100), + format = x -> string(round(x)), startvalue = 10) + ) + return sg1.sliders[1].value, sg2.sliders[1].value +end + +# interactive psos is based in the 2D clicker +function DynamicalSystems.interactive_poincaresos(ds, plane, idxs, complete; + # PSOS kwargs: + direction = -1, + rootkw = (xrtol = 1e-6, atol = 1e-6), + Tmax = 1e3, + kw... + ) + + # Basic sanity checks on the method arguments + @assert typeof(plane) <: Tuple + @assert length(idxs) == 2 + @assert eltype(idxs) == Int + @assert plane[1] ∉ idxs + + i = DynamicalSystems.SVector{2, Int}(idxs) + + # Construct a new `PoincareMap` structure with the given parameters + pmap = DynamicalSystems.DynamicalSystemsBase.PoincareMap(ds, plane; + direction, rootkw, Tmax) + + # construct a 2d projected system compatible with the clicker + z = plane[2] # third variable comes from plane + complete_state = u -> complete(u..., z) + project = i + newds = ProjectedDynamicalSystem(pmap, project, complete_state) + return interactive_2d_clicker(newds; kw...) +end diff --git a/ext/src/poincareclick.jl b/ext/src/poincareclick.jl deleted file mode 100644 index 473254a8..00000000 --- a/ext/src/poincareclick.jl +++ /dev/null @@ -1,83 +0,0 @@ -ChaosTools = DynamicalSystems.ChaosTools - -function DynamicalSystems.interactive_poincaresos(ds, plane, idxs, complete; - # PSOS kwargs: - direction = -1, - tfinal = (1000.0, 10.0^4), - rootkw = (xrtol = 1e-6, atol = 1e-6), - # Makie kwargs: - color = randomcolor, - scatterkwargs = (), - labels = ("u₁", "u₂") - ) - - # Basic sanity checks on the method arguments - @assert typeof(plane) <: Tuple - @assert length(idxs) == 2 - @assert eltype(idxs) == Int - @assert plane[1] ∉ idxs - u0 = DynamicalSystems.get_state(ds) - - i = DynamicalSystems.SVector{2, Int}(idxs) - - figure = Figure(size = (1000, 800), backgroundcolor = :white) - - T_slider, m_slider = _add_psos_controls!(figure, tfinal) - ax = figure[0, :] = Axis(figure) - - # Construct a new `PoincareMap` structure with the given parameters - pmap = DynamicalSystems.DynamicalSystemsBase.PoincareMap(ds, plane; - direction, u0, rootkw, Tmax = tfinal[2]) - - # Compute the initial section - psos, = trajectory(pmap, T_slider[]; t0 = 0) - data = psos[:, i] - length(data) == 0 && error(ChaosTools.PSOS_ERROR) - - positions_node = Observable(data) - colors = (c = color(u0); [c for i in 1:length(data)]) - colors_node = Observable(colors) - scatter!( - ax, positions_node, color = colors_node, - markersize = lift(o -> o*px, m_slider), marker = :circle, scatterkwargs... - ) - - ax.xlabel, ax.ylabel = labels - laststate = Observable(u0) - - # Interactive clicking on the psos: - Makie.deactivate_interaction!(ax, :rectanglezoom) - spoint = select_point(ax.scene) - on(spoint) do pos - x, y = pos; z = plane[2] # third variable comes from plane - newstate = try - complete(x, y, z) - catch err - @error "Could not get state, got error: " exception=err - return - end - - psos, = trajectory(pmap, T_slider[], newstate; t0 = 0) - data = psos[:, i] - positions = positions_node[]; colors = colors_node[] - append!(positions, data) - c = color(newstate) - append!(colors, fill(c, length(data))) - # Update all the observables with Array as value: - positions_node[], colors_node[], laststate[] = positions, colors, newstate - end - display(figure) - return figure, laststate -end - -function _add_psos_controls!(figure, tfinal) - sg1 = SliderGrid(figure[1, :][1,1], - (label = "T", range = range(tfinal[1], tfinal[2], length = 1000), - format = x -> string(round(x)), ) - ) - sg2 = SliderGrid(figure[1, :][1,2], - (label = "ms", range = 10.0 .^ range(0, 2, length = 100), - format = x -> string(round(x)), startvalue = 10) - ) - return sg1.sliders[1].value, sg2.sliders[1].value -end diff --git a/src/visualizations.jl b/src/visualizations.jl index 1b068268..33f9c073 100644 --- a/src/visualizations.jl +++ b/src/visualizations.jl @@ -1,5 +1,6 @@ export interactive_trajectory, interactive_cobweb, interactive_orbitdiagram, scaleod, - interactive_poincaresos_scan, interactive_poincaresos, interactive_trajectory_timeseries + interactive_poincaresos_scan, interactive_poincaresos, interactive_trajectory_timeseries, + interactive_2d_clicker """ interactive_trajectory_timeseries(ds::DynamicalSystem, fs, [, u0s]; kwargs...) → fig, dsobs @@ -265,6 +266,7 @@ function interactive_poincaresos_scan end """ interactive_poincaresos(cds, plane, idxs, complete; kwargs...) + Launch an interactive application for exploring a Poincaré surface of section (PSOS) of the continuous dynamical system `cds`. Requires `DynamicalSystems`. @@ -282,16 +284,10 @@ an observable containing the latest initial `state`. ## Keyword Arguments * `direction, rootkw` : Same use as in `DynamicalSystems.poincaresos`. -* `tfinal = (1000.0, 10.0^4)` : A 2-element tuple for the range of values - for the total integration time (chosen interactively). -* `color` : A **function** of the system's initial condition, that returns a color to - plot the new points with. The color must be `RGBf/RGBAf`. - A random color is chosen by default. -* `labels = ("u₁" , "u₂")` : Scatter plot labels. -* `scatterkwargs = ()`: Named tuple of keywords passed to `scatter`. -* `diffeq = NamedTuple()` : Any extra keyword arguments are passed into `init` of DiffEq. +* All other keywords are propagated to [`interactive_2d_clicker`](@ref). ## Interaction + The application is a standard scatterplot, which shows the PSOS of the system, initially using the system's `u0`. Two sliders control the total evolution time and the size of the marker points (which is always in pixels). @@ -313,4 +309,44 @@ This will be properly handled instead of breaking the application. This `newstate` is also given to the function `color` that gets a new color for the new points. """ -function interactive_poincaresos end \ No newline at end of file +function interactive_poincaresos end + +""" + interactive_2d_clicker(ds; kwargs...) + +Launch an interactive application for exploring the state space of a +two dimensional dynamical system `ds`. usually derived from a continuous dynamical system. +Requires `DynamicalSystems`. + +Return: `figure, last_state` with the latter being +the latest-clicked initial condition. + +**Note:** this function works for any system that qualifies as two dimensional, +this includes projected dynamical systems that can be in reality arbitrarily dimensional. + +## Keyword Arguments + +* `times = 10:10_000`: A vector of potential total integration times + (chosen interactively). +* `Δt = 1`: trajectory time step. +* `color` : A **function** of the system's initial condition, that returns a color to + plot the new points with. The color must be `RGBf/RGBAf`. + A random color is chosen by default. +* `plotkwargs = ()`: Keywords passed to the plotting function. + +## Interaction + +The application is plotting the trajectory of the system starting from `u0` +which is clicked on the 2D axis. +The plot is using scatter or scatterlines depending on whether +the dynamical system is discrete time or not. +Two sliders control the total evolution time +and the size of the marker points (which is always in pixels). + +Upon clicking within the bounds of the scatter plot your click is transformed into +a new initial condition, which is further evolved and then plotted into the scatter plot. + +The `complete` function can throw an error for ill-conditioned `u0`. +This will be properly handled instead of breaking the application. +""" +function interactive_2d_clicker end