diff --git a/docs/src/visualizations.md b/docs/src/visualizations.md index b5d00c23..07d8a8cb 100644 --- a/docs/src/visualizations.md +++ b/docs/src/visualizations.md @@ -318,3 +318,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..ac74bc4a 100644 --- a/ext/DynamicalSystemsVisualizations.jl +++ b/ext/DynamicalSystemsVisualizations.jl @@ -10,7 +10,8 @@ include("src/cobweb.jl") include("src/orbitdiagram.jl") include("src/poincareclick.jl") include("src/brainscan.jl") +include("src/clicker.jl") subscript = DynamicalSystemsVisualizations.subscript -end \ No newline at end of file +end diff --git a/ext/src/clicker.jl b/ext/src/clicker.jl new file mode 100644 index 00000000..234addac --- /dev/null +++ b/ext/src/clicker.jl @@ -0,0 +1,75 @@ +function DynamicalSystems.interactive_clicker(ds; + # DynamicalSystems kwargs: + tfinal = (1000.0, 10.0^4), + complete = (x, y) -> [x, y], + project = identity, + # Makie kwargs: + color = randomcolor, + labels = ("x", "y"), + scatterkwargs = () + ) + + u0 = DynamicalSystems.get_state(ds) + + figure = Figure(size = (1000, 800), backgroundcolor = :white) + + T_slider, m_slider = _add_clicker_controls!(figure, tfinal) + ax = figure[0, :] = Axis(figure) + + # Compute the initial section + tr, = trajectory(ds, T_slider[]; t0 = 0) + length(tr) == 0 && error("Initial computed trajectory is empty") + + data = project(tr) + length(data[1]) != 2 && error("(Projected) trajectory is not 2D") + + positions_node = Observable(data) + colors = (c = color(u0); [c for _ 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 phase space: + Makie.deactivate_interaction!(ax, :rectanglezoom) + spoint = select_point(ax.scene) + on(spoint) do pos + x, y = pos; + newstate = try + complete(x, y) + catch err + @error "Could not complete state, got error: " exception=err + return + end + + tr, = trajectory(ds, T_slider[], newstate; t0 = 0) + data = project(tr) + + 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, 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/ext/src/poincareclick.jl b/ext/src/poincareclick.jl index 473254a8..a6114b6a 100644 --- a/ext/src/poincareclick.jl +++ b/ext/src/poincareclick.jl @@ -20,64 +20,18 @@ function DynamicalSystems.interactive_poincaresos(ds, plane, idxs, complete; 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... + z = plane[2] # third variable comes from plane + return interactive_clicker(pmap; + tfinal = tfinal, + complete = (x, y) -> complete(x, y, z), + project = tr -> tr[:, i], + color = color, + scatterkwargs = scatterkwargs, + labels = labels ) - - 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..2646a0f3 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_clicker """ interactive_trajectory_timeseries(ds::DynamicalSystem, fs, [, u0s]; kwargs...) → fig, dsobs @@ -313,4 +314,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_clicker(ds; kwargs...) +Launch an interactive application for exploring the state space of a +discrete dynamical system `ds`, usually derived from a continuous dynamical system. +Requires `DynamicalSystems`. + +The function returns: `figure, laststate` with the latter being +an observable containing the latest initial `state`. + +## Keyword Arguments +* `tfinal = (1000.0, 10.0^4)`: A 2-element tuple for the range of values + for the total integration time (chosen interactively). +* `complete`: A **function** to construct the new initial state of the system, + after the user clicks on the screen. Receives two parameters, + the `x` and `y` coordinates of the click, + and must return a vector with the same dimension as the system's state. + If not specified, by default it's just `(x, y) -> [x, y]`. +* `project`: A **function** to project a system's state down to two dimensions, + in order to be able to represent it graphically. + If not specified, the default is the identity function. +* `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 = ("x" , "y")` : Scatter plot labels. +* `scatterkwargs = ()`: Named tuple of keywords passed to `scatter`. + +## Interaction +The application is a standard scatterplot, which shows the state space of the dynamical 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). + +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 `x, y, z`. +This will be properly handled instead of breaking the application. +""" +function interactive_clicker end