Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/src/visualizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
```
3 changes: 2 additions & 1 deletion ext/DynamicalSystemsVisualizations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
end
75 changes: 75 additions & 0 deletions ext/src/clicker.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
function DynamicalSystems.interactive_clicker(ds;
# DynamicalSystems kwargs:
tfinal = (1000.0, 10.0^4),
complete = (x, y) -> [x, y],
project = identity,
Comment on lines +4 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these keywords provided here? Shouldn't we expect a ProjectedDynamicalSystem directly instead of allowing the projection to happen within the function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but then I don't think we could use it directly with PoincareMap?

# Makie kwargs:
color = randomcolor,
labels = ("x", "y"),
scatterkwargs = ()
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you check here that dimension(dds) == 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because this function can be used with higher-dimensional dynamical systems, as long as the complete and project parameters are properly defined. Adding such a check would break the use of this in interactive_poincaresos.

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
62 changes: 8 additions & 54 deletions ext/src/poincareclick.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 43 additions & 2 deletions src/visualizations.jl
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
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
Loading