From b91e9c77f58cfed11c23740ad872ff0f2b37df5c Mon Sep 17 00:00:00 2001 From: Francis Felici Date: Sun, 26 Feb 2023 11:34:38 -0300 Subject: [PATCH] (NOT WORKING) Add heatmap for Continuous Spaces (#127) * create .devcontainer.json * (NOT WORKING) Add heatmap for Continuous Spaces * (NOT WORKING) Add heatmap for continous spaces * wip * make runnable flocking with spatial field * add if clause for continous heatmap; use correct extent * revert commit for lifting * only check the size of heatmap if gridspace * fix typo: there is no "property"... * add a plotting example that shows altering heatmap * visually distinctive colormap * delete wrongly commited .json file Please put these things in the .gitignore * bump patch version * add the news in the docstring and improve the docstring --------- Co-authored-by: Datseris --- Project.toml | 2 +- examples/agents/agents_flocking.jl | 89 ++++++++++++++++++++++++++++-- src/agents/abmplot.jl | 49 ++++++++++------ src/agents/lifting.jl | 13 +++-- 4 files changed, 125 insertions(+), 28 deletions(-) diff --git a/Project.toml b/Project.toml index 6ded719..982d4f4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "InteractiveDynamics" uuid = "ec714cd0-5f51-11eb-0b6e-452e7367ff84" repo = "https://github.com/JuliaDynamics/InteractiveDynamics.jl.git" -version = "0.22.0" +version = "0.22.1" [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" diff --git a/examples/agents/agents_flocking.jl b/examples/agents/agents_flocking.jl index e875184..df46cbb 100644 --- a/examples/agents/agents_flocking.jl +++ b/examples/agents/agents_flocking.jl @@ -1,10 +1,80 @@ using Agents using GLMakie using InteractiveDynamics +using LinearAlgebra: norm +using Random -model, flocking_agent_step!, flocking_model_step! = Models.flocking() -Bird = valtype(model.agents) +# Create flocking model +@agent Bird ContinuousAgent{2} begin + speed::Float64 + cohere_factor::Float64 + separation::Float64 + separate_factor::Float64 + match_factor::Float64 + visual_distance::Float64 +end + +function flocking(; + n_birds = 100, + speed = 1.0, + cohere_factor = 0.25, + separation = 4.0, + separate_factor = 0.25, + match_factor = 0.01, + visual_distance = 5.0, + extent = (100, 100), + spacing = visual_distance / 1.5, + spatial_field_size = (20, 20), +) + space2d = ContinuousSpace(extent; spacing) + properties = (spatial_field = rand(spatial_field_size...),) + model = ABM(Bird, space2d; properties, scheduler = Schedulers.Randomly()) + for _ in 1:n_birds + vel = Tuple(rand(model.rng, 2) * 2 .- 1) + add_agent!( + model, + vel, + speed, + cohere_factor, + separation, + separate_factor, + match_factor, + visual_distance, + ) + end + return model +end + +function flocking_agent_step!(bird, model) + neighbor_ids = nearby_ids(bird, model, bird.visual_distance) + N = 0 + match = separate = cohere = (0.0, 0.0) + for id in neighbor_ids + N += 1 + neighbor = model[id].pos + heading = neighbor .- bird.pos + cohere = cohere .+ heading + if euclidean_distance(bird.pos, neighbor, model) < bird.separation + separate = separate .- heading + end + match = match .+ model[id].vel + end + N = max(N, 1) + cohere = cohere ./ N .* bird.cohere_factor + separate = separate ./ N .* bird.separate_factor + match = match ./ N .* bird.match_factor + bird.vel = (bird.vel .+ cohere .+ separate .+ match) ./ 2 + bird.vel = bird.vel ./ norm(bird.vel) + move_agent!(bird, model, bird.speed) +end + +function flocking_model_step!(model) + Random.shuffle!(model.spatial_field) +end + +model = flocking() +# %% plot const bird_polygon = Polygon(Point2f[(-0.5, -0.5), (1, 0), (-0.5, 0.5)]) function bird_marker(b::Bird) φ = atan(b.vel[2], b.vel[1]) #+ π/2 + π @@ -19,7 +89,18 @@ fig fig, ax, abmobs = abmplot(model; axis = (; title = "Flocking"), agent_step! = flocking_agent_step!, - model_step! = flocking_model_step!, - am = bird_marker, + model_step! = flocking_model_step!, + am = bird_marker, ) fig + +# %% Test plot with a heatmap in continuous space +fig, ax, abmobs = abmplot(model; + axis = (; title = "Flocking"), + agent_step! = flocking_agent_step!, + model_step! = flocking_model_step!, + am = bird_marker, + heatarray = :spatial_field, + heatkwargs = (colormap = :grays,), +) +fig \ No newline at end of file diff --git a/src/agents/abmplot.jl b/src/agents/abmplot.jl index 1edf44a..66e358d 100644 --- a/src/agents/abmplot.jl +++ b/src/agents/abmplot.jl @@ -18,7 +18,7 @@ Requires `Agents`. See also [`abmvideo`](@ref) and [`abmexploration`](@ref). ### Agent related * `ac, as, am` : These three keywords decide the color, size, and marker, that each agent will be plotted as. They can each be either a constant or a *function*, - which takes as an input a single agent and outputs the corresponding value. If the model + which takes as an input a single agent and outputs the corresponding value. If the model uses a `GraphSpace`, `ac, as, am` functions instead take an *iterable of agents* in each position (i.e. node of the graph). @@ -43,27 +43,28 @@ Requires `Agents`. See also [`abmvideo`](@ref) and [`abmexploration`](@ref). * `scatterkwargs = ()` : Additional keyword arguments propagated to the `scatter!` call. ### Preplot related -* `heatarray = nothing` : A keyword that plots a heatmap over the space. +* `heatarray = nothing` : A keyword that plots a model property (that is a matrix) + as a heatmap over the space. Its values can be standard data accessors given to functions like `run!`, i.e. either a symbol (directly obtain model property) or a function of the model. - The returned data must be a matrix of the same size as the underlying space. + If the space is `AbstractGridSpace` then matrix must be the same size as the underlying + space. For `ContinuousSpace` any size works and will be plotted over the space extent. For example `heatarray = :temperature` is used in the Daisyworld example. But you could also define `f(model) = create_matrix_from_model...` and set `heatarray = f`. The heatmap will be updated automatically during model evolution in videos and interactive applications. - - It is strongly recommended to use `abmplot` instead of the `abmplot!` method if - you use `heatarray`, so that a colorbar can be placed naturally. * `heatkwargs = NamedTuple()` : Keywords given to `Makie.heatmap` function if `heatarray` is not nothing. * `add_colorbar = true` : Whether or not a Colorbar should be added to the right side of the - heatmap if `heatarray` is not nothing. + heatmap if `heatarray` is not nothing. It is strongly recommended to use `abmplot` + instead of the `abmplot!` method if you use `heatarray`, so that a colorbar can be + placed naturally. * `static_preplot!` : A function `f(ax, model)` that plots something after the heatmap but before the agents. -* `osmkwargs = NamedTuple()` : keywords directly passed to `OSMMakie.osmplot!` +* `osmkwargs = NamedTuple()` : keywords directly passed to `OSMMakie.osmplot!` if model space is `OpenStreetMapSpace`. -* `graphplotkwargs = NamedTuple()` : keywords directly passed to - [`GraphMakie.graphplot!`](https://graph.makie.org/stable/#GraphMakie.graphplot) +* `graphplotkwargs = NamedTuple()` : keywords directly passed to + [`GraphMakie.graphplot!`](https://graph.makie.org/stable/#GraphMakie.graphplot) if model space is `GraphSpace`. The stand-alone function `abmplot` also takes two optional `NamedTuple`s named `figure` and @@ -82,7 +83,7 @@ The stand-alone function `abmplot` also takes two optional `NamedTuple`s named ` 1. "run": starts/stops the continuous evolution of the model. 1. "reset model": resets the model to its initial state from right after starting the interactive application. - 1. Two sliders control the animation speed: "spu" decides how many model steps should be + 1. Two sliders control the animation speed: "spu" decides how many model steps should be done before the plot is updated, and "sleep" the `sleep()` time between updates. * `enable_inspection = add_controls`: If `true`, enables agent inspection on mouse hover. * `spu = 1:50`: The values of the "spu" slider. @@ -132,12 +133,13 @@ end abmplot(abmobs::ABMObservable; kwargs...) → fig, ax, abmobs abmplot!(ax::Axis/Axis3, abmobs::ABMObservable; kwargs...) → abmobs -Same functionality as `abmplot(model; kwargs...)`/`abmplot!(ax, model; kwargs...)` +Same functionality as `abmplot(model; kwargs...)`/`abmplot!(ax, model; kwargs...)` but allows to link an already existing `ABMObservable` to the created plots. """ -function abmplot(abmobs::ABMObservable; - figure = NamedTuple(), - axis = NamedTuple(), + +function abmplot(abmobs::ABMObservable; + figure = NamedTuple(), + axis = NamedTuple(), kwargs...) fig = Figure(; figure...) ax = fig[1,1][1,1] = agents_space_dimensionality(abmobs.model[]) == 3 ? @@ -218,6 +220,8 @@ const SUPPORTED_SPACES = Union{ Agents.GraphSpace, } + + function Makie.plot!(abmplot::_ABMPlot) model = abmplot.abmobs[].model[] if !(model.space isa SUPPORTED_SPACES) @@ -232,8 +236,8 @@ function Makie.plot!(abmplot::_ABMPlot) # Following attributes are all lifted from the recipe observables (specifically, # the model), see lifting.jl for source code. - pos, color, marker, markersize, heatobs = - lift_attributes(abmplot.abmobs[].model, abmplot.ac, abmplot.as, abmplot.am, + pos, color, marker, markersize, heatobs = + lift_attributes(abmplot.abmobs[].model, abmplot.ac, abmplot.as, abmplot.am, abmplot.offset, abmplot.heatarray, abmplot._used_poly) # OpenStreetMapSpace preplot @@ -247,7 +251,16 @@ function Makie.plot!(abmplot::_ABMPlot) # Heatmap if !isnothing(heatobs[]) - hmap = heatmap!(abmplot, heatobs; colormap = JULIADYNAMICS_CMAP, abmplot.heatkwargs...) + if !(model.space isa Agents.ContinuousSpace) + hmap = heatmap!(abmplot, heatobs; colormap = JULIADYNAMICS_CMAP, abmplot.heatkwargs...) + else # need special version for continuous space + nbinx, nbiny = size(heatobs[]) + extx, exty = Agents.abmspace(model).extent + coordx = range(0, extx; length = nbinx) + coordy = range(0, exty; length = nbiny) + hmap = heatmap!(abmplot, coordx, coordy, heatobs; colormap = JULIADYNAMICS_CMAP, abmplot.heatkwargs...) + end + if abmplot.add_colorbar[] Colorbar(fig[1, 1][1, 2], hmap, width = 20) # TODO: Set colorbar to be "glued" to axis diff --git a/src/agents/lifting.jl b/src/agents/lifting.jl index 5cacdab..aa87070 100644 --- a/src/agents/lifting.jl +++ b/src/agents/lifting.jl @@ -58,10 +58,10 @@ agents_space_dimensionality(::Agents.GraphSpace) = 2 ##### abmplot_colors(model::Agents.ABM{<:SUPPORTED_SPACES}, ac, ids) = to_color(ac) -abmplot_colors(model::Agents.ABM{<:SUPPORTED_SPACES}, ac::Function, ids) = +abmplot_colors(model::Agents.ABM{<:SUPPORTED_SPACES}, ac::Function, ids) = to_color.([ac(model[i]) for i in ids]) # in GraphSpace we iterate over a list of agents (not agent ids) at a graph node position -abmplot_colors(model::Agents.ABM{<:Agents.GraphSpace}, ac::Function, ids) = +abmplot_colors(model::Agents.ABM{<:Agents.GraphSpace}, ac::Function, ids) = to_color.(ac(model[id] for id in model.space.stored_ids[idx]) for idx in ids) ##### @@ -107,7 +107,7 @@ abmplot_markersizes(model::Agents.ABM{<:SUPPORTED_SPACES}, as::Function, ids) = [as(model[i]) for i in ids] abmplot_markersizes(model::Agents.ABM{<:Agents.GraphSpace}, as, ids) = as -abmplot_markersizes(model::Agents.ABM{<:Agents.GraphSpace}, as::Function, ids) = +abmplot_markersizes(model::Agents.ABM{<:Agents.GraphSpace}, as::Function, ids) = [as(model[id] for id in model.space.stored_ids[idx]) for idx in ids] @@ -124,8 +124,11 @@ function abmplot_heatobs(model, heatarray) # # TODO: use surface!(heatobs) here? matrix = Agents.get_data(model, heatarray, identity) - if !(matrix isa AbstractMatrix) || size(matrix) ≠ size(model.space) - error("The heat array property must yield a matrix of same size as the grid!") + # Check for correct size for discrete space + if Agents.abmspace(model) isa Agents.AbstractGridSpace + if !(matrix isa AbstractMatrix) || size(matrix) ≠ size(Agents.abmspace(model)) + error("The heat array property must yield a matrix of same size as the grid!") + end end matrix else