Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new internal API for sampling #875

Merged
merged 32 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
07259af
new internal API for sampling
Tortar Sep 10, 2023
07f5166
Update space_interaction_API.jl
Tortar Sep 10, 2023
b4bc8e7
dev
Tortar Sep 11, 2023
b2f69f9
dev2
Tortar Sep 11, 2023
e317354
dev3
Tortar Sep 11, 2023
92d39e8
Update sample.jl
Tortar Sep 13, 2023
915f2bd
Update sample.jl
Tortar Sep 13, 2023
529e2fd
Update walk.jl
Tortar Sep 13, 2023
9532750
Update discrete.jl
Tortar Sep 13, 2023
b29835e
Update space_interaction_API.jl
Tortar Sep 13, 2023
9bd3da1
Update model_abstract.jl
Tortar Sep 13, 2023
79a1222
Update api.md
Tortar Sep 13, 2023
045752c
Update model_abstract.jl
Tortar Sep 13, 2023
a872152
Merge branch 'main' into sampling
Tortar Jan 4, 2024
5ae25a0
Update sample.jl
Tortar Jan 4, 2024
bc5cba1
Update space_interaction_API.jl
Tortar Jan 4, 2024
05a734e
Update sample.jl
Tortar Jan 4, 2024
993e4c4
update code
Tortar Jan 5, 2024
8f5859a
update code 2
Tortar Jan 5, 2024
eecc9db
update code 3
Tortar Jan 5, 2024
0df729b
Update Project.toml
Tortar Jan 7, 2024
67bbec8
Update Project.toml
Tortar Jan 7, 2024
d2a6d48
Update Agents.jl
Tortar Jan 7, 2024
a91acb9
fix code
Tortar Jan 14, 2024
abcc3ec
Merge branch 'main' into sampling
Tortar Jan 14, 2024
32e66cf
Update Agents.jl
Tortar Jan 14, 2024
af9b3a8
Update Agents.jl
Tortar Jan 14, 2024
e0a75d6
printing for debugging
Tortar Jan 14, 2024
cbcf392
remove change for AgentsExampleZoo
Tortar Jan 14, 2024
25aa71c
Update runtests.jl
Tortar Jan 14, 2024
8e905af
remove prints
Tortar Jan 15, 2024
9cfc01c
code review changes
Tortar Jan 15, 2024
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
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
IteratorSampling = "ef79a3d2-ae9f-5cd2-ab61-e13847810a6e"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
LazyArtifacts = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
LightOSM = "d1922b25-af4e-4ba3-84af-fe9bea896051"
Expand Down Expand Up @@ -46,6 +47,7 @@ DataStructures = "0.18"
Distributions = "0.25"
Downloads = "1"
Graphs = "1.4"
IteratorSampling = "0.2"
JLD2 = "0.4"
LazyArtifacts = "1.3.0"
LightOSM = "0.2.0"
Expand Down
7 changes: 4 additions & 3 deletions src/Agents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ module Agents
read(path, String)
end Agents

using Distributed
using DataFrames
using DataStructures
using Distributed
using Graphs
using DataFrames
using IteratorSampling
using MacroTools
import ProgressMeter
using Random
using StaticArrays: SVector
export SVector
import ProgressMeter
import LinearAlgebra

# Core structures of Agents.jl
Expand Down
8 changes: 6 additions & 2 deletions src/core/model_free_extensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ end
function fallback_random_agent(model, condition, alloc)
if alloc
iter_ids = allids(model)
return sampling_with_condition_agents_single(iter_ids, condition, model)
id = sampling_with_condition_single(iter_ids, condition, model, id -> model[id])
isnothing(id) && return nothing
return model[id]
else
iter_agents = allagents(model)
iter_filtered = Iterators.filter(agent -> condition(agent), iter_agents)
return resorvoir_sampling_single(iter_filtered, model)
agent = IteratorSampling.itsample(abmrng(model), iter_filtered)
isnothing(agent) && return nothing
return agent
end
end

Expand Down
89 changes: 17 additions & 72 deletions src/core/space_interaction_API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ nearby_agents(a, model, r = 1; kwargs...) =

"""
random_nearby_id(agent, model::ABM, r = 1, f = nothing, alloc = false; kwargs...) → id
Return the `id` of a random agent near the position of the given `agent` using an optimized
algorithm from [Reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling#An_optimal_algorithm).
Return the `id` of a random agent near the position of the given `agent`.
Tortar marked this conversation as resolved.
Show resolved Hide resolved

Return `nothing` if no agents are nearby.

The value of the argument `r` and possible keywords operate identically to [`nearby_ids`](@ref).
Expand All @@ -317,18 +317,21 @@ is expensive since in this case the allocating version can be more performant.

For discrete spaces, use [`random_id_in_position`](@ref) instead to return a random id at a given
position.

This function, as all the other methods which sample from lazy iterators, uses an optimized
algorithm which doesn't require to collect all elements to just sample one of them.
"""
function random_nearby_id(a, model, r = 1, f = nothing, alloc = false; kwargs...)
iter = nearby_ids(a, model, r; kwargs...)
if isnothing(f)
return resorvoir_sampling_single(iter, model)
return IteratorSampling.itsample(abmrng(model), iter)
else
if alloc
return sampling_with_condition_single(iter, f, model)
else
iter_filtered = Iterators.filter(id -> f(id), iter)
return resorvoir_sampling_single(iter_filtered, model)
end
return IteratorSampling.itsample(abmrng(model), iter_filtered)
end
end
end

Expand All @@ -348,21 +351,19 @@ For discrete spaces, use [`random_agent_in_position`](@ref) instead to return a
position.
"""
function random_nearby_agent(a, model, r = 1, f = nothing, alloc = false; kwargs...)
iter_ids = nearby_ids(a, model, r; kwargs...)
if isnothing(f)
id = random_nearby_id(a, model, r; kwargs...)
isnothing(id) && return nothing
return model[id]
id = IteratorSampling.itsample(abmrng(model), iter_ids)
else
iter_ids = nearby_ids(a, model, r; kwargs...)
if alloc
return sampling_with_condition_agents_single(iter_ids, f, model)
id = sampling_with_condition_single(iter_ids, f, model, id -> model[id])
else
iter_filtered = Iterators.filter(id -> f(model[id]), iter_ids)
id = resorvoir_sampling_single(iter_filtered, model)
isnothing(id) && return nothing
return model[id]
id = IteratorSampling.itsample(abmrng(model), iter_filtered)
end
end
isnothing(id) && return nothing
return model[id]
end

"""
Expand All @@ -380,69 +381,13 @@ is expensive since in this case the allocating version can be more performant.
function random_nearby_position(pos, model, r=1, f = nothing, alloc = false; kwargs...)
iter = nearby_positions(pos, model, r; kwargs...)
if isnothing(f)
return resorvoir_sampling_single(iter, model)
return IteratorSampling.itsample(abmrng(model), iter)
else
if alloc
return sampling_with_condition_single(iter, f, model)
else
iter_filtered = Iterators.filter(pos -> f(pos), iter)
return resorvoir_sampling_single(iter_filtered, model)
return IteratorSampling.itsample(abmrng(model), iter_filtered)
end
end
end

#######################################################################################
# %% sampling functions
#######################################################################################

function sampling_with_condition_single(iter, condition, model)
population = collect(iter)
n = length(population)
rng = abmrng(model)
@inbounds while n != 0
index_id = rand(rng, 1:n)
el = population[index_id]
condition(el) && return el
population[index_id], population[n] = population[n], population[index_id]
n -= 1
end
return nothing
end

# almost a copy of sampling_with_condition_single, but it's better to call this one
# when selecting an agent since collecting ids is less costly than collecting agents
function sampling_with_condition_agents_single(iter, condition, model)
population = collect(iter)
n = length(population)
rng = abmrng(model)
@inbounds while n != 0
index_id = rand(rng, 1:n)
el = population[index_id]
condition(model[el]) && return model[el]
population[index_id], population[n] = population[n], population[index_id]
n -= 1
end
return nothing
end

# Reservoir sampling function (https://en.wikipedia.org/wiki/Reservoir_sampling)
function resorvoir_sampling_single(iter, model)
res = iterate(iter)
isnothing(res) && return nothing
rng = abmrng(model)
el, state = res
w = rand(rng)
while true
skip_counter = ceil(Int, randexp(rng)/log(1-w))
while skip_counter != 0
skip_res = iterate(iter, state)
isnothing(skip_res) && return el
state = skip_res[2]
skip_counter += 1
end
res = iterate(iter, state)
isnothing(res) && return el
el, state = res
w *= rand(rng)
end
end
end
18 changes: 18 additions & 0 deletions src/simulations/sample.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,21 @@ end
function choose_arg(x, kwargs_nt, agent)
return deepcopy(getfield(hasproperty(kwargs_nt, x) ? kwargs_nt : agent, x))
end

#######################################################################################
# %% sampling functions
#######################################################################################

function sampling_with_condition_single(iter, condition, model, transform=identity)
population = collect(iter)
n = length(population)
rng = abmrng(model)
@inbounds while n != 0
index_id = rand(rng, 1:n)
el = population[index_id]
condition(transform(el)) && return el
population[index_id], population[n] = population[n], population[index_id]
n -= 1
end
return nothing
end
24 changes: 10 additions & 14 deletions src/spaces/discrete.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function random_empty(model::ABM{<:DiscreteSpace}, cutoff = 0.998)
end
else
empty = empty_positions(model)
return resorvoir_sampling_single(empty, model)
return IteratorSampling.itsample(abmrng(model), empty)
end
end

Expand Down Expand Up @@ -145,13 +145,15 @@ function random_id_in_position(pos, model)
isempty(ids) && return nothing
return rand(abmrng(model), ids)
end
function random_id_in_position(pos, model, f, alloc = false)
function random_id_in_position(pos, model, f, alloc = false, transform = identity)
iter_ids = ids_in_position(pos, model)
if alloc
return sampling_with_condition_single(iter_ids, f, model)
return sampling_with_condition_single(iter_ids, f, model, transform)
else
iter_filtered = Iterators.filter(id -> f(id), iter_ids)
return resorvoir_sampling_single(iter_filtered, model)
iter_filtered = Iterators.filter(id -> f(transform(id)), iter_ids)
id = IteratorSampling.itsample(abmrng(model), iter_filtered)
isnothing(id) && return nothing
return id
end
end

Expand All @@ -172,15 +174,9 @@ function random_agent_in_position(pos, model)
return model[id]
end
function random_agent_in_position(pos, model, f, alloc = false)
iter_ids = ids_in_position(pos, model)
if alloc
return sampling_with_condition_agents_single(iter_ids, f, model)
else
iter_filtered = Iterators.filter(id -> f(model[id]), iter_ids)
id = resorvoir_sampling_single(iter_filtered, model)
isnothing(id) && return nothing
return model[id]
end
id = random_id_in_position(pos, model, f, alloc, id -> model[id])
isnothing(id) && return nothing
return model[id]
end

#######################################################################################
Expand Down
4 changes: 2 additions & 2 deletions src/spaces/walk.jl
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ function random_empty_pos_in_offsets(offsets, agent, model)
n_attempts -= 1
end
targets = Iterators.map(β -> normalize_position(agent.pos .+ β, model), offsets)
check_empty = pos -> isempty(pos, model)
return sampling_with_condition_single(targets, check_empty, model)
empty_targets = Iterators.filter(pos -> isempty(pos, model), targets)
return IteratorSampling.itsample(abmrng(model), empty_targets)
end

"""
Expand Down
Loading