Repository: JuliaDynamics/Agents.jl
Pull request #1153: Support StructVector as a container (Issue 1146)
Author: login: bergio13
Created: 2025-04-07T15:53:44Z | Merged: 2025-04-28T18:13:05Z

This is a draft for the fix of issue 1146, the code is adapted from [here](https://github.com/JuliaDynamics/Agents.jl/issues/1146#issuecomment-2780752740). The wrapper implementation adds some complexities which I was not able to resolve yet in a clean and efficient way and should be adressed before making the pr. I tested my implementation on the flocking example by changing the type of container to `StructVector` and it works if we apply some small tweaks to the code in particular:
- in `continuous.jl ` we change `agent.pos = pos` to `model[agent.id].pos = pos`
```
function move_agent!(agent::AbstractAgent, pos::ContinuousPos, model::ABM{<:ContinuousSpace})
    space_size = spacesize(model)
    D = length(space_size)
    all(i -> 0 <= pos[i] <= space_size[i], 1:D) || error("position is outside space extent!")
    oldcell = pos2cell(agent, model)
    newcell = pos2cell(pos, model)
    if oldcell ≠ newcell
        remove_agent_from_space!(agent, model, oldcell)
        add_agent_to_space!(agent, model, newcell)
    end
    model[agent.id].pos = pos
    return agent
end
```
- in `flock.jl` (remove the bird marker for plotting or change type of the input agent)
```
function agent_step!(bird, model)
    ## Obtain the ids of neighbors within the bird's visual distance
    neighbor_agents = nearby_agents(bird.pos, model, bird.visual_distance)
    N = 0
    match = separate = cohere = SVector{2}(0.0, 0.0)
    ## Calculate behaviour properties based on neighbors
    for neighbor in neighbor_agents
        N += 1
        heading = get_direction(bird.pos, neighbor.pos, model)

        ## `cohere` computes the average position of neighboring birds
        cohere += heading
        ## `match` computes the average trajectory of neighboring birds
        match += neighbor.vel
        if sum(heading .^ 2) < bird.separation^2
            ## `separate` repels the bird away from neighboring birds
            separate -= heading
        end
    end

    ## Normalise results based on model input and neighbor count
    cohere *= bird.cohere_factor
    separate *= bird.separate_factor
    match *= bird.match_factor
    ## Compute velocity based on rules defined above
    bird.vel += (cohere + separate + match) / max(N, 1)
    bird.vel /= norm(bird.vel)
    ## Move bird according to new velocity and speed
    move_agent!(Agents.agent_container(model)[bird.id], model, bird.speed)
end
```
I will happily listen to advices on how to integrate this in the code in a smooth way.

Moreover, it is still necessary to adress the issue mentioned in this [comment](https://github.com/JuliaDynamics/Agents.jl/issues/1146#issuecomment-2781363742).


Diff:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cdbcbfaaf6..e6db730eb9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,7 +58,7 @@ jobs:
         version:
           - '1'
         os:
-          - ubuntu-20.04
+          - ubuntu-latest
         arch:
           - x64
     steps:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fee9c98ebd..d285e94b8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# v6.3
+
+**Experimental**: The `StructVector` container type, that supports a Struct-of-Arrays (SoA) layout, has been added. It currently only works with single agent types.
+
 # v6.2
 
 The `EventQueueABM` which was previously considered experimental is now considered to be stable.
diff --git a/Project.toml b/Project.toml
index 642621880c..82577152b9 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "Agents"
 uuid = "46ada45e-f475-11e8-01d0-f70cc89e6671"
 authors = ["George Datseris", "Tim DuBois", "Aayush Sabharwal", "Ali Vahdati", "Adriano Meligrana"]
-version = "6.2.10"
+version = "6.3.0"
 
 [deps]
 CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
@@ -26,6 +26,7 @@ Scratch = "6c6a2e73-6563-6170-7368-637461726353"
 StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
 StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
 StreamSampling = "ff63dad9-3335-55d8-95ec-f8139d39e468"
+StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
 
 [weakdeps]
 Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"
@@ -66,4 +67,5 @@ Scratch = "1"
 StaticArrays = "1"
 StatsBase = "0.32, 0.33, 0.34"
 StreamSampling = "0.6"
+StructArrays = "0.5, 0.6, 0.7"
 julia = "1.9"
diff --git a/src/Agents.jl b/src/Agents.jl
index 2530bca78e..fb458866f0 100644
--- a/src/Agents.jl
+++ b/src/Agents.jl
@@ -22,11 +22,14 @@ export SVector
 import LinearAlgebra
 import StreamSampling
 import StreamSampling: itsample
+using StructArrays: StructVector
+export StructVector
 
 # Core structures of Agents.jl
 include("core/agents.jl")
 include("core/model_abstract.jl")
 include("core/model_free_extensions.jl")
+include("core/agent_containers.jl")
 include("core/model_standard.jl")
 include("core/model_event_queue.jl")
 include("core/model_validation.jl")
diff --git a/src/core/agent_containers.jl b/src/core/agent_containers.jl
new file mode 100644
index 0000000000..7427fe3ebb
--- /dev/null
+++ b/src/core/agent_containers.jl
@@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
+
+"""
+    struct AgentWrapperSoA{C} <: AbstractAgent
+
+Wrapper type for agents in a StructVector container.
+"""
+struct AgentWrapperSoA{A, C} <: AbstractAgent
+    soa::C
+    id::Int
+end
+const SoAType = AgentWrapperSoA
+
+function AgentWrapperSoA{A}(soa::C, id::Int) where {A<:AbstractAgent, C}
+    return AgentWrapperSoA{A,C}(soa, id)
+end
+
+function Base.getproperty(agent::AgentWrapperSoA, name::Symbol)
+    return getproperty(getfield(agent, :soa), name)[getfield(agent, :id)]
+end
+
+function Base.setproperty!(agent::AgentWrapperSoA, name::Symbol, x)
+    getproperty(getfield(agent, :soa), name)[getfield(agent, :id)] = x
+    return agent
+end
+
+"""
+    agent_container_type(container::Type, A)
+
+Returns the container type for storing agents of type `A` in the specified container type `container`.
+"""
+agent_container_type(::Type{T}, A) where {T<:AbstractDict} = T{Int,A}
+agent_container_type(::Type{T}, A) where {T<:AbstractVector} = T{A}
+agent_container_type(container, A) = throw(
+    ArgumentError("Unrecognised container $container, please specify `Dict`, `Vector` or `StructVector`.")
+)
+
+function construct_agent_container(container::Type, A)
+    C = agent_container_type(container, A)
+    if C <: StructVector
+        init = NamedTuple(name => T[] for (name, T) in zip(fieldnames(A), fieldtypes(A)))
+        return C(init)
+    else
+        return C()
+    end
+end
diff --git a/src/core/model_abstract.jl b/src/core/model_abstract.jl
index b2fdc2fa7f..621ae9d6ab 100644
--- a/src/core/model_abstract.jl
+++ b/src/core/model_abstract.jl
@@ -172,17 +172,17 @@ Return a valid `id` for creating a new agent with it.
 nextid(model::ABM) = notimplemented(model)
 
 """
-    add_agent_to_model!(agent, model)
+    add_agent_to_container!(agent, model)
 Add the agent to the model's internal container, if the addition is valid
 given the agent's ID and those already in the model. Otherwise error.
 """
-add_agent_to_model!(agent, model) = notimplemented(model)
+add_agent_to_container!(agent, model) = add_agent_to_container!(agent, agent_container(model))
 
 """
-    remove_agent_from_model!(agent, model)
+    remove_agent_from_container!(agent, model)
 Remove the agent from the model's internal container.
 """
-remove_agent_from_model!(agent, model) = notimplemented(model)
+remove_agent_from_container!(agent, model) = notimplemented(model)
 
 function Base.setindex!(m::ABM, args...; kwargs...)
     error("`setindex!` or `model[id] = agent` are invalid. Use `add_agent!` instead.")
diff --git a/src/core/model_accessing_API.jl b/src/core/model_accessing_API.jl
index d048810965..45b655a59c 100644
--- a/src/core/model_accessing_API.jl
+++ b/src/core/model_accessing_API.jl
@@ -2,12 +2,14 @@ const DictABM = Union{StandardABM{S,A,<:AbstractDict{<:Integer,A}} where {S,A},
                       EventQueueABM{S,A,<:AbstractDict{<:Integer,A}} where {S,A}}
 const VecABM = Union{StandardABM{S,A,<:AbstractVector{A}} where {S,A},
                      EventQueueABM{S,A,<:AbstractVector{A}} where {S,A}}
+const StructVecABM = Union{StandardABM{S,A,<:StructVector{A}} where {S,A},
+                         EventQueueABM{S,A,<:StructVector{A}} where {S,A}}
 
 nextid(model::DictABM) = getfield(model, :maxid)[] + 1
-nextid(model::VecABM) = nagents(model) + 1
-hasid(model::VecABM, id::Int) = id ≤ nagents(model)
+nextid(model::Union{VecABM, StructVecABM}) = nagents(model) + 1
+hasid(model::Union{VecABM, StructVecABM}, id::Int) = id ≤ nagents(model)
 
-function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
+function add_agent_to_container!(agent::AbstractAgent, model::DictABM)
     if haskey(agent_container(model), agent.id)
         error(lazy"Can't add agent to model. There is already an agent with id=$(agent.id)")
     else
@@ -16,26 +18,29 @@ function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
     maxid = getfield(model, :maxid)
     new_id = agent.id
     if maxid[] < new_id; maxid[] = new_id; end
-    extra_actions_after_add!(agent, model)
     return
 end
 
-function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
+function add_agent_to_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
     push!(agent_container(model), agent)
-    extra_actions_after_add!(agent, model)
     return
 end
 
-# This is extended for event based models (in their file)
+# This is extended for event based models
 extra_actions_after_add!(agent, model::StandardABM) = nothing
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:Union{AbstractDict, AbstractVector}} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(agent, model)
+end
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:StructVector} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(model[agent.id], model)
+end
 
-function remove_agent_from_model!(agent::AbstractAgent, model::DictABM)
+function remove_agent_from_container!(agent::AbstractAgent, model::DictABM)
     delete!(agent_container(model), agent.id)
     return
 end
-
-function remove_agent_from_model!(agent::AbstractAgent, model::VecABM)
+function remove_agent_from_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
     error("Cannot remove agents in a `StandardABM` with a vector container.")
 end
 
diff --git a/src/core/model_event_queue.jl b/src/core/model_event_queue.jl
index ea345dd12d..2992220e49 100644
--- a/src/core/model_event_queue.jl
+++ b/src/core/model_event_queue.jl
@@ -144,7 +144,7 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
@@ -152,9 +152,17 @@ function EventQueueABM(
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to 
+            remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
+        end
+    end
+    agents = construct_agent_container(container, A)
     events = SizedVector{length(events), Union{typeof.(events)...}}(events...)
 
     # the queue stores pairs of (agent ID, event index) mapping them to their trigger time
@@ -205,7 +213,7 @@ function EventQueueABM(
         events, type_func, idx_events_each_type, propensities_each_type,
         idx_func_propensities_each_type, queue
     ))
-    return EventQueueABM{S,A,C,P,E,R,TF,ET,PT,FPT,Q}(
+    return EventQueueABM{S,A,typeof(agents),P,E,R,TF,ET,PT,FPT,Q}(
 
         agents, space, properties, rng, Ref(0), Ref(0.0),
 
@@ -215,18 +223,12 @@ function EventQueueABM(
     )
 end
 
+function Base.getindex(model::EventQueueABM{S, A, C}, id::Int) where {S,A,C<:StructVector}
+    return AgentWrapperSoA{A}(agent_container(model), id)
+end
 ###########################################################################################
 # %% Adding events to the queue
 ###########################################################################################
-# This function ensures that once an agent is added into the model,
-# an event is created and added for it. It is called internally
-# by `add_agent_to_model!`.
-function extra_actions_after_add!(agent, model::EventQueueABM)
-    if getfield(model, :autogenerate_on_add)
-        add_event!(agent, model)
-    end
-end
-
 """
     add_event!(agent, model)
 
diff --git a/src/core/model_free_extensions.jl b/src/core/model_free_extensions.jl
index 86f8188342..ad986b9b0f 100644
--- a/src/core/model_free_extensions.jl
+++ b/src/core/model_free_extensions.jl
@@ -95,6 +95,6 @@ end
 
 function remove_all_from_model!(model::ABM)
     for a in allagents(model)
-        remove_agent_from_model!(a, model)
+        remove_agent_from_container!(a, model)
     end
 end
diff --git a/src/core/model_standard.jl b/src/core/model_standard.jl
index 51d614b47b..1bb666dd13 100644
--- a/src/core/model_standard.jl
+++ b/src/core/model_standard.jl
@@ -2,8 +2,6 @@ export StandardABM, UnremovableABM
 export abmscheduler
 export dummystep
 
-ContainerType{A} = Union{AbstractDict{Int,A}, AbstractVector{A}}
-
 struct StandardABM{
     S<:SpaceType,
     A<:AbstractAgent,
@@ -77,6 +75,10 @@ The evolution rules are functions given to the keywords `agent_step!`, `model_st
   Use `Vector` if no agents are removed during the simulation.
   This allows storing agents more efficiently, yielding faster retrieval and
   iteration over agents. Use `Dict` if agents are expected to be removed during the simulation.
+  **Experimental**: Use `StructVector` from StructArrays.jl to enable struct-of-arrays (SoA) storage 
+  for performance benefits in some applications with large numbers of agents and fixed fields.  
+  The agent type should be wrapped in `SoAType` (e.g., `SoAType{MyAgent}`). 
+  `StructVector` currently only works for single agent models.
 - `properties = nothing`: additional model-level properties that the user may
   include in the model. `properties` can be an arbitrary container of data,
   however it is most typically a `Dict` with `Symbol` keys, or a composite type (`struct`).
@@ -129,7 +131,7 @@ end
 ```
 """
 function StandardABM(
-    ::Type{A},
+    A::Type,
     space::S = nothing;
     agent_step!::G = dummystep,
     model_step!::K = dummystep,
@@ -140,7 +142,7 @@ function StandardABM(
     agents_first::Bool = true,
     warn = true,
     warn_deprecation = true
-) where {A<:AbstractAgent,S<:SpaceType,G,K,F,P,R<:AbstractRNG}
+) where {S<:SpaceType,G,K,F,P,R<:AbstractRNG}
     if warn_deprecation && agent_step! == dummystep && model_step! == dummystep
         @warn """
         From version 6.0 it is necessary to pass at least one of agent_step! or model_step!
@@ -152,12 +154,22 @@ function StandardABM(
         ABMObservable.
         """ maxlog=1
     end
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor 
+            to remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
+        end
+    end
+
     !(is_sumtype(A)) && agent_validator(A, space, warn)
-    C = construct_agent_container(container, A)
-    agents = C()
+    agents = construct_agent_container(container, A)
+    
     agents_types = union_types(A)
     T = typeof(agents_types)
-    return StandardABM{S,A,C,T,G,K,F,P,R}(agents, agent_step!, model_step!, space, scheduler,
+    return StandardABM{S,A,typeof(agents),T,G,K,F,P,R}(agents, agent_step!, model_step!, space, scheduler,
                                         properties, rng, agents_types, agents_first, Ref(0), Ref(0))
 end
 
@@ -165,12 +177,6 @@ function StandardABM(agent::AbstractAgent, args::Vararg{Any, N}; kwargs...) wher
     return StandardABM(typeof(agent), args...; kwargs...)
 end
 
-construct_agent_container(::Type{T}, A) where {T<:AbstractDict} = T{Int,A}
-construct_agent_container(::Type{T}, A) where {T<:AbstractVector} = T{A}
-construct_agent_container(container, A) = throw(
-    "Unrecognised container $container, please specify either `Dict` or `Vector`."
-)
-
 function remove_all_from_model!(model::StandardABM)
     empty!(agent_container(model))
 end
@@ -181,6 +187,9 @@ function remove_all_from_space!(model)
     end
 end
 
+function Base.getindex(model::StandardABM{S,A,C}, id::Int) where {S,A,C<:StructVector}
+    return AgentWrapperSoA{A}(agent_container(model), id)
+end
 
 """
     dummystep(model)
@@ -199,7 +208,13 @@ dummystep(agent, model) = nothing
 #######################################################################################
 function Base.show(io::IO, abm::StandardABM{S,A,C}) where {S,A,C}
   n = isconcretetype(A) ? nameof(A) : string(A)
-  typecontainer = C <: AbstractDict ? "Dict" : "Vector"
+  if C <: AbstractDict
+    typecontainer = "Dict"
+  elseif C <: StructVector
+    typecontainer = "StructVector"
+  elseif C <: AbstractVector
+    typecontainer = "Vector"
+  end
   s = "StandardABM with $(nagents(abm)) agents of type $(n)"
   s *= "\n agents container: $(typecontainer)"
   if abmspace(abm) === nothing
diff --git a/src/core/space_interaction_API.jl b/src/core/space_interaction_API.jl
index a820d99809..49c35035c9 100644
--- a/src/core/space_interaction_API.jl
+++ b/src/core/space_interaction_API.jl
@@ -141,7 +141,7 @@ end
 Remove an agent from the model.
 """
 function remove_agent!(a::AbstractAgent, model::ABM)
-    remove_agent_from_model!(a, model)
+    remove_agent_from_container!(a, model)
     remove_agent_from_space!(a, model)
 end
 remove_agent!(id::Integer, model::ABM) = remove_agent!(model[id], model)
@@ -197,7 +197,8 @@ end
 Add the agent to the `model` at the agent's own position.
 """
 function add_agent_own_pos!(agent::AbstractAgent, model::ABM)
-    add_agent_to_model!(agent, model)
+    add_agent_to_container!(agent, model)
+    extra_actions_after_add!(agent, model)
     add_agent_to_space!(agent, model)
     return agent
 end
diff --git a/src/submodules/pathfinding/astar.jl b/src/submodules/pathfinding/astar.jl
index 3f9c81f20a..0411b3dfb3 100644
--- a/src/submodules/pathfinding/astar.jl
+++ b/src/submodules/pathfinding/astar.jl
@@ -247,6 +247,6 @@ from `pathfinder`.
 """
 function Agents.remove_agent!(agent::AbstractAgent, model::ABM, pathfinder::AStar)
     delete!(pathfinder.agent_paths, agent.id)
-    Agents.remove_agent_from_model!(agent, model)
+    Agents.remove_agent_from_container!(agent, model)
     Agents.remove_agent_from_space!(agent, model)
 end
diff --git a/test/model_access_tests.jl b/test/model_access_tests.jl
index 247be18544..24a1bdce8c 100644
--- a/test/model_access_tests.jl
+++ b/test/model_access_tests.jl
@@ -3,17 +3,24 @@ using Agents, Test
 @testset "Model Access" begin
 @testset "Accessing model" begin
     @testset "ModelType=$(ModelType)" for ModelType in (StandardABM, EventQueueABM)
-        @testset "ContainerType=$(ContainerType)" for ContainerType in (Dict, Vector)
+        @testset "ContainerType=$(ContainerType)" for ContainerType in (Dict, Vector, StructVector)
 
         extra_args = ifelse(ModelType != EventQueueABM, (), ((),))
         extra_kwargs = ifelse(ModelType != EventQueueABM, (warn_deprecation = false, ), ((autogenerate_on_add=false),))
-        model = ModelType(NoSpaceAgent, extra_args...;
+        # For StructVector container, we need to wrap the agent type with SoAType
+        agent_type = ContainerType == StructVector ? SoAType{NoSpaceAgent} : NoSpaceAgent
+        model = ModelType(agent_type, extra_args...;
             properties = Dict(:a => 2, :b => "test"),
             container = ContainerType, extra_kwargs...
         )
+        # Add agents differently based on container type
         for i in 1:5
-            add_agent!(model)
-        end
+            if ContainerType == StructVector
+                add_agent!(NoSpaceAgent, model)
+            else
+                add_agent!(model)
+            end
+        end        
         if ModelType == StandardABM
             @test abmscheduler(model) == Schedulers.fastest
         end
@@ -21,7 +28,12 @@ using Agents, Test
         @test abmtime(model) == 0
         @test abmproperties(model) == Dict(:a => 2, :b => "test")
         a = model[1]
-        @test a isa NoSpaceAgent
+        # Test based on container type
+        if ContainerType == StructVector
+            @test a isa SoAType
+        else
+            @test a isa NoSpaceAgent
+        end
         @test a.id == 1
         @test model.a == 2
         @test model.b == "test"
@@ -32,8 +44,14 @@ using Agents, Test
         @test model.b == "Changed"
         @test_throws ErrorException model.c = 5
 
-        newa = add_agent!(NoSpaceAgent, model)
-        @test model[6] == newa
+        # Add a new agent
+        if ContainerType == StructVector
+            newa = add_agent!(NoSpaceAgent, model)
+            @test model[6].id == newa.id
+        else
+            newa = add_agent!(agent_type, model)
+            @test model[6] == newa
+        end        
         # setindex must errors
         @test_throws ErrorException model[7] = newa
 
@@ -43,7 +61,7 @@ using Agents, Test
             par3::String
         end
         properties = Properties(1,1.0,"Test")
-        model = ModelType(NoSpaceAgent, extra_args...; properties, container = ContainerType, extra_kwargs...)
+        model = ModelType(agent_type, extra_args...; properties, container = ContainerType, extra_kwargs...)
         @test abmproperties(model) isa Properties
         @test model.par1 == 1
         @test model.par2 == 1.0
@@ -57,7 +75,7 @@ using Agents, Test
         @test model.par3 == "Changed"
         @test_throws ErrorException model.par4 = 5
 
-        model = ModelType(NoSpaceAgent, extra_args...; container = ContainerType, extra_kwargs...)
+        model = ModelType(agent_type, extra_args...; container = ContainerType, extra_kwargs...)
         @test_throws ErrorException model.a = 5
         end
     end
@@ -72,11 +90,14 @@ end
     prop2 = TestContainer(1, 0.5)
     model1 = StandardABM(NoSpaceAgent; properties = prop1, warn_deprecation = false)
     model2 = StandardABM(NoSpaceAgent; properties = prop2, warn_deprecation = false)
+    model3 = StandardABM(SoAType{NoSpaceAgent}; properties = prop1, container = StructVector, warn_deprecation = false)
+    add_agent!(NoSpaceAgent, model3)
 
     test1(model) = model.a
     test2(model) = model.weight
     @test_nowarn @inferred test1(model1)
     @test_nowarn @inferred test2(model2)
+    @test_nowarn @inferred test1(model3)
 end
 
 @testset "Core methods" begin
@@ -85,6 +106,11 @@ end
     @test Agents.spacetype(model) <: GridSpace
     @test size(abmspace(model)) == (5,5)
     @test all(isempty(p, model) for p in positions(model))
+    model_soa = StandardABM(SoAType{GridAgent{2}}, GridSpace((5,5)), container = StructVector, warn_deprecation = false)
+    @test Agents.agenttype(model_soa) == GridAgent{2}
+    @test Agents.spacetype(model_soa) <: GridSpace
+    @test size(abmspace(model_soa)) == (5,5)
+    @test all(isempty(p, model_soa) for p in positions(model_soa))
 end
 
 @testset "Display" begin
@@ -98,6 +124,45 @@ end
     @test sprint(show, Agents.abmspace(model)) == "periodic continuous space with [1.0, 1.0] extent and spacing=0.05"
     model = StandardABM(GraphAgent, GraphSpace(path_graph(5)), warn_deprecation = false)
     @test sprint(show, Agents.abmspace(model)) == "GraphSpace with 5 positions and 4 edges"
+    model_soa = StandardABM(SoAType{NoSpaceAgent}, container = StructVector, warn_deprecation = false)
+    @test occursin("no spatial structure", sprint(show, model_soa))
+    @test occursin("agents container: StructVector", sprint(show, model_soa))
 end
 
 end
+
+@testset "SoAType Specific Tests" begin
+    @testset "SoAType wrapper functionality" begin
+        model = StandardABM(SoAType{NoSpaceAgent}, container = StructVector, warn_deprecation = false)
+        for i in 1:3
+            add_agent!(NoSpaceAgent, model)
+        end
+        # Test agent retrieval and wrapper behavior
+        agent = model[1]
+        @test agent isa SoAType
+        @test getfield(agent, :id) == 1 
+        # Test property access and modification
+        agent.id = 100 
+        @test model[1].id == 100
+        # Test get/set property through the wrapper
+        old_id = agent.id
+        agent.id = 200
+        @test agent.id == 200
+        @test agent.id != old_id
+        # Test the underlying StructVector
+        soa = getfield(agent, :soa)
+        @test soa isa StructVector
+        @test soa.id[1] == 200 
+    end
+    @testset "add_agent! with StructVector" begin
+        model = StandardABM(SoAType{NoSpaceAgent}, container = StructVector, warn_deprecation = false)
+        # Add an agent and verify it's accessible
+        agent = add_agent!(NoSpaceAgent, model)
+        @test agent isa NoSpaceAgent
+        @test agent.id == 1
+        # Test that they represent the same agent but aren't the same type
+        wrapped_agent = model[1]
+        @test wrapped_agent.id == agent.id
+        @test typeof(wrapped_agent) != typeof(agent)
+    end
+end
\ No newline at end of file


Reviews:
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T16:34:52Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T16:37:06Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T16:40:24Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T16:54:08Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T16:56:15Z
user: login: bergio13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T17:08:36Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T20:22:47Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T20:24:58Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T20:29:57Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T20:32:04Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T21:27:39Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T21:02:38Z
user: login: Datseris
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T21:08:19Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T21:09:24Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T13:23:28Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T14:37:01Z
user: login: ameligrana
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T13:25:13Z
user: login: Datseris
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T14:03:35Z
user: login: ameligrana
body: To me the PR seems in good shape, thanks @bergio13 for the work!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-26T20:45:30Z
user: login: Datseris
body: Merging this PR was unecessarily rushed. Besides the code changes that still need to happen, this PR didn't change any of the docs. It added the extra subsection in the docstring of the standard ABM. 

But I think we can do better. Instead, in the main docs, right after the type of the ABMs ,we should have a new subsection about the type of the containers. There we should list the 3 containers. Then the ABM docs should just say "any of the ABM containers" and cite this subsection.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T19:51:54Z
user: login: bergio13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T16:49:55Z
user: login: bergio13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T15:05:24Z
user: login: bergio13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T15:51:06Z
user: login: bergio13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T08:01:57Z

Review comments:
diff_hunk: @@ -2,10 +2,14 @@ const DictABM = Union{StandardABM{S,A,<:AbstractDict{<:Integer,A}} where {S,A},
                       EventQueueABM{S,A,<:AbstractDict{<:Integer,A}} where {S,A}}
 const VecABM = Union{StandardABM{S,A,<:AbstractVector{A}} where {S,A},
                      EventQueueABM{S,A,<:AbstractVector{A}} where {S,A}}
+const StructABM = Union{StandardABM{S,A,<:StructVector{A}} where {S,A},
+                         EventQueueABM{S,A,<:StructVector{A}} where {S,A}}
 
 nextid(model::DictABM) = getfield(model, :maxid)[] + 1
 nextid(model::VecABM) = nagents(model) + 1
 hasid(model::VecABM, id::Int) = id ≤ nagents(model)
+nextid(model::StructABM) = nagents(model) + 1
+hasid(model::StructABM, id::Int) = id ≤ nagents(model)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: ```suggestion
nextid(model::Union{VecABM, StructABM}) = nagents(model) + 1
hasid(model::Union{VecABM, StructABM}, id::Int) = id ≤ nagents(model)
```
created_at: 2025-04-07T16:34:52Z
author_association: MEMBER
diff_hunk: @@ -39,5 +51,9 @@ function remove_agent_from_model!(agent::AbstractAgent, model::VecABM)
     error("Cannot remove agents in a `StandardABM` with a vector container.")
 end
 
+function remove_agent_from_model!(agent::AbstractAgent, model::StructABM)
+    error("Cannot remove agents in a `StandardABM` with a vector container.")
+end
+
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: ```suggestion
```

same as before, just use `Union`
created_at: 2025-04-07T16:37:06Z
author_association: MEMBER
diff_hunk: @@ -27,6 +31,14 @@ function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
     return
 end
 
+function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
+    agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
+    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
+    push!(agent_container(model), tuple_agent)
+    extra_actions_after_add!(tuple_agent, model)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: ```suggestion
    extra_actions_after_add!(model[agent.id], model)
```

this should be like this (at least is already a bit better) because you pass the wrapper this way
created_at: 2025-04-07T16:40:23Z
author_association: MEMBER
diff_hunk: @@ -181,6 +188,25 @@ function remove_all_from_space!(model)
     end
 end
 
+struct AgentWrapperSoA{C}
path: src/core/model_standard.jl
user: login: ameligrana
body: ```suggestion
struct AgentWrapperSoA{C} <: AbstractAgent
```

with this Flocking works without any modification
created_at: 2025-04-07T16:54:08Z
author_association: MEMBER
diff_hunk: @@ -39,5 +51,9 @@ function remove_agent_from_model!(agent::AbstractAgent, model::VecABM)
     error("Cannot remove agents in a `StandardABM` with a vector container.")
 end
 
+function remove_agent_from_model!(agent::AbstractAgent, model::StructABM)
+    error("Cannot remove agents in a `StandardABM` with a vector container.")
+end
+
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: I don't see anymore remove_agent_from_model! for `StructABM`, you should add `Union{VecABM, StructABM}` in the signature
created_at: 2025-04-07T16:56:14Z
author_association: MEMBER
diff_hunk: @@ -39,5 +51,9 @@ function remove_agent_from_model!(agent::AbstractAgent, model::VecABM)
     error("Cannot remove agents in a `StandardABM` with a vector container.")
 end
 
+function remove_agent_from_model!(agent::AbstractAgent, model::StructABM)
+    error("Cannot remove agents in a `StandardABM` with a vector container.")
+end
+
path: src/core/model_accessing_API.jl
user: login: bergio13
body: you should see it now
created_at: 2025-04-07T17:08:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +29,14 @@ function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
     return
 end
 
+function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
+    agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
+    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
+    push!(agent_container(model), tuple_agent)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: 

I have the feeling that pushing is slow this way, worth benchmarking
created_at: 2025-04-07T20:22:47Z
author_association: MEMBER
diff_hunk: @@ -27,6 +29,14 @@ function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
     return
 end
 
+function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
+    agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
+    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
+    push!(agent_container(model), tuple_agent)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: If this is so, to solve this, one should go down the calling path, and have a specialized version of `add_agent!` which doesn't construct the agent (if this is not already passed, and instead only its arguments are passed)
created_at: 2025-04-07T20:24:58Z
author_association: MEMBER
diff_hunk: @@ -27,6 +29,14 @@ function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
     return
 end
 
+function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
+    agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
+    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
+    push!(agent_container(model), tuple_agent)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: but this last thing is clearly advantageous in any case
created_at: 2025-04-07T20:29:57Z
author_association: MEMBER
diff_hunk: @@ -2,10 +2,12 @@ const DictABM = Union{StandardABM{S,A,<:AbstractDict{<:Integer,A}} where {S,A},
                       EventQueueABM{S,A,<:AbstractDict{<:Integer,A}} where {S,A}}
 const VecABM = Union{StandardABM{S,A,<:AbstractVector{A}} where {S,A},
                      EventQueueABM{S,A,<:AbstractVector{A}} where {S,A}}
+const StructABM = Union{StandardABM{S,A,<:StructVector{A}} where {S,A},
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: ```suggestion
const StructVecABM = Union{StandardABM{S,A,<:StructVector{A}} where {S,A},
```
seems slightly better
created_at: 2025-04-07T20:32:04Z
author_association: MEMBER
diff_hunk: @@ -154,7 +154,13 @@ function StandardABM(
     end
     !(is_sumtype(A)) && agent_validator(A, space, warn)
     C = construct_agent_container(container, A)
-    agents = C()
+    if C <: StructVector
+        fields_with_types = zip(fieldnames(A), fieldtypes(A))
+	tuple_init = NamedTuple(x => T[] for (x, T) in fields_with_types)
+	agents = C(tuple_init)
+    else
+        agents = C()
+    end
     agents_types = union_types(A)
     T = typeof(agents_types)
     return StandardABM{S,A,C,T,G,K,F,P,R}(agents, agent_step!, model_step!, space, scheduler,
path: src/core/model_standard.jl
user: login: ameligrana
body: ```suggestion
    return StandardABM{S,A,typeof(agents),T,G,K,F,P,R}(agents, agent_step!, model_step!, space, scheduler,
```

otherwise the type is abstract
created_at: 2025-04-07T21:27:39Z
author_association: MEMBER
diff_hunk: @@ -27,6 +29,14 @@ function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
     return
 end
 
+function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
+    agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
+    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
+    push!(agent_container(model), tuple_agent)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: ```suggestion
    push!(agent_container(model), agent)
```

this reduces indirection
created_at: 2025-04-12T21:02:37Z
author_association: MEMBER
diff_hunk: @@ -31,8 +31,7 @@ end
 
 function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
-    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
-    push!(agent_container(model), tuple_agent)
+    push!(agent_container(model), agent)
path: src/core/model_accessing_API.jl
user: login: Datseris
body: Rather, this should be a dedicated function `add_agent_to_container!`. And the function `add_agent_to_model!` shouldn't be named at all this way. We have `add_agent!`. This is the function that means "add agent to model", which entails more lower level processes: add to space, add to container, update next id, etc. etc. 

So all `add_agent!` functions propagate to the lowest `add_agent!(agent::AA, model::ABM)`. From there we call the internal functions `add_agent_to_container!`, to space, and the extra actions.
created_at: 2025-04-12T21:08:19Z
author_association: MEMBER
diff_hunk: @@ -31,8 +31,7 @@ end
 
 function add_agent_to_model!(agent::AbstractAgent, model::StructABM)
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
-    tuple_agent = NamedTuple(x => getfield(agent, x) for x in fieldnames(typeof(agent)))
-    push!(agent_container(model), tuple_agent)
+    push!(agent_container(model), agent)
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: yes, right, my change was from a performance standpoint
created_at: 2025-04-12T21:09:24Z
author_association: MEMBER
diff_hunk: @@ -144,17 +144,22 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
         rng::R = Random.default_rng(),
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to remove this warning." maxlog=1
+        end
+        A = A.body.parameters[1]
+    end
path: src/core/model_event_queue.jl
user: login: ameligrana
body: ```suggestion
    if container == StructVector
        if !(A <: SoAType)
            @warn "The agent type passed to the model constructor is of type $A but a model with a StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to remove this warning." maxlog=1
        else
            A = A.body.parameters[1]
        end
    end
```
created_at: 2025-04-25T13:23:28Z
author_association: MEMBER
diff_hunk: @@ -144,17 +144,22 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
         rng::R = Random.default_rng(),
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to remove this warning." maxlog=1
+        end
+        A = A.body.parameters[1]
+    end
path: src/core/model_event_queue.jl
user: login: ameligrana
body: same in model standard
created_at: 2025-04-25T14:37:00Z
author_association: MEMBER
diff_hunk: @@ -16,26 +18,29 @@ function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
     maxid = getfield(model, :maxid)
     new_id = agent.id
     if maxid[] < new_id; maxid[] = new_id; end
-    extra_actions_after_add!(agent, model)
     return
 end
 
-function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
+function add_agent_to_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
     push!(agent_container(model), agent)
-    extra_actions_after_add!(agent, model)
     return
 end
 
-# This is extended for event based models (in their file)
+# This is extended for event based models
 extra_actions_after_add!(agent, model::StandardABM) = nothing
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:Union{AbstractDict, AbstractVector}} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(agent, model)
+end
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:StructVector} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(model[agent.id], model)
+end
 
 function remove_agent_from_model!(agent::AbstractAgent, model::DictABM)
     delete!(agent_container(model), agent.id)
     return
 end
-
-function remove_agent_from_model!(agent::AbstractAgent, model::VecABM)
+function remove_agent_from_model!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
path: src/core/model_accessing_API.jl
user: login: ameligrana
body: ```suggestion
function remove_agent_from_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
```

for consistency this should be renamed
created_at: 2025-04-26T13:25:13Z
author_association: MEMBER
diff_hunk: @@ -172,11 +172,11 @@ Return a valid `id` for creating a new agent with it.
 nextid(model::ABM) = notimplemented(model)
 
 """
-    add_agent_to_model!(agent, model)
+    add_agent_to_container!(agent, model)
 Add the agent to the model's internal container, if the addition is valid
 given the agent's ID and those already in the model. Otherwise error.
 """
-add_agent_to_model!(agent, model) = notimplemented(model)
+add_agent_to_container!(agent, model) = notimplemented(model)
path: src/core/model_abstract.jl
user: login: Datseris
body: I think this function should dispatch to `add_agent_to_container!(agent, agent_container(model))`. 

I am not convinced we need a `notimplemented` function here. The only way this function is called is anyways in the high level.
created_at: 2025-04-26T14:03:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
path: src/core/agent_containers.jl
user: login: Datseris
body: Why do we need this? We have done something similar with the `ValidPos` type and it lead to problems. FOr example someone couldn't implement a new space without making a PR here. We need to remove this abstract type, it doesn't appear to serve a purpose.
created_at: 2025-04-28T18:51:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
+
+"""
+    struct AgentWrapperSoA{C} <: AbstractAgent
path: src/core/agent_containers.jl
user: login: Datseris
body: Remove `struct`, remove `{C}`. Where is the usage of this type (how to use it) documented?
created_at: 2025-04-28T18:52:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
+
+"""
+    struct AgentWrapperSoA{C} <: AbstractAgent
+
+Wrapper type for agents in a StructVector container.
+"""
+struct AgentWrapperSoA{A, C} <: AbstractAgent
+    soa::C
+    id::Int
+end
+const SoAType = AgentWrapperSoA
+
+function AgentWrapperSoA{A}(soa::C, id::Int) where {A<:AbstractAgent, C}
+    return AgentWrapperSoA{A,C}(soa, id)
+end
+
+function Base.getproperty(agent::AgentWrapperSoA, name::Symbol)
+    return getproperty(getfield(agent, :soa), name)[getfield(agent, :id)]
+end
+
+function Base.setproperty!(agent::AgentWrapperSoA, name::Symbol, x)
+    getproperty(getfield(agent, :soa), name)[getfield(agent, :id)] = x
+    return agent
+end
+
+"""
+    agent_container_type(container::Type, A)
+
+Returns the container type for storing agents of type `A` in the specified container type `container`.
path: src/core/agent_containers.jl
user: login: Datseris
body: Returns -> Return
created_at: 2025-04-28T18:53:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
+
+"""
+    struct AgentWrapperSoA{C} <: AbstractAgent
+
+Wrapper type for agents in a StructVector container.
+"""
+struct AgentWrapperSoA{A, C} <: AbstractAgent
+    soa::C
+    id::Int
+end
+const SoAType = AgentWrapperSoA
+
+function AgentWrapperSoA{A}(soa::C, id::Int) where {A<:AbstractAgent, C}
+    return AgentWrapperSoA{A,C}(soa, id)
+end
+
+function Base.getproperty(agent::AgentWrapperSoA, name::Symbol)
+    return getproperty(getfield(agent, :soa), name)[getfield(agent, :id)]
+end
+
+function Base.setproperty!(agent::AgentWrapperSoA, name::Symbol, x)
+    getproperty(getfield(agent, :soa), name)[getfield(agent, :id)] = x
+    return agent
+end
+
+"""
+    agent_container_type(container::Type, A)
+
+Returns the container type for storing agents of type `A` in the specified container type `container`.
+"""
+agent_container_type(::Type{T}, A) where {T<:AbstractDict} = T{Int,A}
+agent_container_type(::Type{T}, A) where {T<:AbstractVector} = T{A}
+agent_container_type(container, A) = throw(
+    ArgumentError("Unrecognised container $container, please specify `Dict`, `Vector` or `StructVector`.")
+)
+
path: src/core/agent_containers.jl
user: login: Datseris
body: Not very future proof. Remove the second sentence that talks about a specific container.
created_at: 2025-04-28T18:53:50Z
author_association: MEMBER
diff_hunk: @@ -16,26 +18,29 @@ function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
     maxid = getfield(model, :maxid)
     new_id = agent.id
     if maxid[] < new_id; maxid[] = new_id; end
-    extra_actions_after_add!(agent, model)
     return
 end
 
-function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
+function add_agent_to_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
path: src/core/model_accessing_API.jl
user: login: Datseris
body: I illustrated a different interface here.

The whole purpose of adding these new interface functions such as `add_agent_to_container!` is that they **should not care about the model!**. Similarly with how `add_agent_to_space!` does not care about the model type. These functions should directly and immediatelly dispatch to the container itself. The container itself then needs to implement this function. 

The way things are right now you need to create this function twice: once for the discrete time and once for the event queue ABM.
created_at: 2025-04-28T18:57:01Z
author_association: MEMBER
diff_hunk: @@ -16,26 +18,29 @@ function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
     maxid = getfield(model, :maxid)
     new_id = agent.id
     if maxid[] < new_id; maxid[] = new_id; end
-    extra_actions_after_add!(agent, model)
     return
 end
 
-function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
+function add_agent_to_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
     push!(agent_container(model), agent)
-    extra_actions_after_add!(agent, model)
     return
 end
 
-# This is extended for event based models (in their file)
+# This is extended for event based models
 extra_actions_after_add!(agent, model::StandardABM) = nothing
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:Union{AbstractDict, AbstractVector}} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(agent, model)
+end
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:StructVector} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(model[agent.id], model)
path: src/core/model_accessing_API.jl
user: login: Datseris
body: Have we even tested that this works with Event Queue?
created_at: 2025-04-28T19:43:40Z
author_association: MEMBER
diff_hunk: @@ -16,26 +18,29 @@ function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
     maxid = getfield(model, :maxid)
     new_id = agent.id
     if maxid[] < new_id; maxid[] = new_id; end
-    extra_actions_after_add!(agent, model)
     return
 end
 
-function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
+function add_agent_to_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
     push!(agent_container(model), agent)
-    extra_actions_after_add!(agent, model)
     return
 end
 
-# This is extended for event based models (in their file)
+# This is extended for event based models
 extra_actions_after_add!(agent, model::StandardABM) = nothing
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:Union{AbstractDict, AbstractVector}} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(agent, model)
+end
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:StructVector} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(model[agent.id], model)
path: src/core/model_accessing_API.jl
user: login: Datseris
body: Nope, there aren't any tests in the event queue file.
created_at: 2025-04-28T19:43:52Z
author_association: MEMBER
diff_hunk: @@ -144,17 +144,25 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
         rng::R = Random.default_rng(),
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to 
+            remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
path: src/core/model_event_queue.jl
user: login: Datseris
body: What is this? `A.body.parameters`? Doesn't sound very stable. Isn't there a programmatic, public-API way to obtain what you want to obtain?
created_at: 2025-04-28T19:45:08Z
author_association: MEMBER
diff_hunk: @@ -144,17 +144,25 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
         rng::R = Random.default_rng(),
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to 
+            remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
+        end
+    end
+    agents = construct_agent_container(container, A)
path: src/core/model_event_queue.jl
user: login: Datseris
body: is this one of the Interface function for the container API? If so we should put it in a subsection in the developer's docs.
created_at: 2025-04-28T19:46:32Z
author_association: MEMBER
diff_hunk: @@ -144,17 +144,25 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
         rng::R = Random.default_rng(),
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to 
+            remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
+        end
+    end
+    agents = construct_agent_container(container, A)
path: src/core/model_event_queue.jl
user: login: Datseris
body: Oh, wow. This PR has introduced NO documentation changes. Yet it was merged and ready to be tagged as a new minor version...?
created_at: 2025-04-28T19:46:53Z
author_association: MEMBER
diff_hunk: @@ -215,18 +223,12 @@ function EventQueueABM(
     )
 end
 
+function Base.getindex(model::EventQueueABM{S, A, C}, id::Int) where {S,A,C<:StructVector}
+    return AgentWrapperSoA{A}(agent_container(model), id)
+end
path: src/core/model_event_queue.jl
user: login: Datseris
body: Why do we have this function? `getindex` should be defined only once at the abstract intertface level. It should call the `agent_container` function. A new internal function `retrieve_agent(container, id)` should be defined. This way `getindex` doesn't have to be implemented twice for the different model types.

Again, we have to get the interfaces good with this PR because in the future there will be more model types beyond event que and discrete time.
created_at: 2025-04-28T19:48:35Z
author_association: MEMBER
diff_hunk: @@ -152,25 +154,29 @@ function StandardABM(
         ABMObservable.
         """ maxlog=1
     end
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor 
+            to remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
path: src/core/model_standard.jl
user: login: Datseris
body: Same comments as above.
created_at: 2025-04-28T19:49:19Z
author_association: MEMBER
diff_hunk: @@ -181,6 +187,9 @@ function remove_all_from_space!(model)
     end
 end
 
+function Base.getindex(model::StandardABM{S,A,C}, id::Int) where {S,A,C<:StructVector}
+    return AgentWrapperSoA{A}(agent_container(model), id)
path: src/core/model_standard.jl
user: login: Datseris
body: same comments as above.
created_at: 2025-04-28T19:49:32Z
author_association: MEMBER
diff_hunk: @@ -16,26 +18,29 @@ function add_agent_to_model!(agent::AbstractAgent, model::DictABM)
     maxid = getfield(model, :maxid)
     new_id = agent.id
     if maxid[] < new_id; maxid[] = new_id; end
-    extra_actions_after_add!(agent, model)
     return
 end
 
-function add_agent_to_model!(agent::AbstractAgent, model::VecABM)
+function add_agent_to_container!(agent::AbstractAgent, model::Union{VecABM, StructVecABM})
     agent.id != nagents(model) + 1 && error(lazy"Cannot add agent of ID $(agent.id) in a vector ABM of $(nagents(model)) agents. Expected ID == $(nagents(model)+1).")
     push!(agent_container(model), agent)
-    extra_actions_after_add!(agent, model)
     return
 end
 
-# This is extended for event based models (in their file)
+# This is extended for event based models
 extra_actions_after_add!(agent, model::StandardABM) = nothing
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:Union{AbstractDict, AbstractVector}} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(agent, model)
+end
+function extra_actions_after_add!(agent, model::EventQueueABM{S,A,<:StructVector} where {S,A})
+    getfield(model, :autogenerate_on_add) && add_event!(model[agent.id], model)
path: src/core/model_accessing_API.jl
user: login: bergio13
body: I had tested it on the rock-paper-scissor example if i remember correctly, but I forgot to add proper tests, I will soon make up for it !
created_at: 2025-04-30T16:49:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
+
+"""
+    struct AgentWrapperSoA{C} <: AbstractAgent
path: src/core/agent_containers.jl
user: login: bergio13
body: the usage of this is not documented as below we define SoAType which is a type alias for it and then we document it as follows:

The agent type should be wrapped in `SoAType` (e.g., `SoAType{MyAgent}`).
created_at: 2025-05-27T15:05:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,17 +144,25 @@ can be evolved in time.
     Pull Requests that implement this are welcomed!
 """
 function EventQueueABM(
-        ::Type{A}, events,
+        A::Type, events,
         space::S = nothing;
         container::Type = Dict,
         properties::P = nothing,
         rng::R = Random.default_rng(),
         warn = true,
         autogenerate_on_add = true,
         autogenerate_after_action = true,
-    ) where {A<:AbstractAgent,S<:SpaceType,P,R<:AbstractRNG}
-    C = construct_agent_container(container, A)
-    agents = C()
+    ) where {S<:SpaceType,P,R<:AbstractRNG}
+    if container == StructVector
+        if !(A <: SoAType)
+            @warn "The agent type passed to the model constructor is of type $A but a model with a 
+            StructVector container will have agents of type SoAType{$A}. Pass this to the constructor to 
+            remove this warning." maxlog=1
+        else
+            A = A.body.parameters[1]
path: src/core/model_event_queue.jl
user: login: bergio13
body: unfortunately none that i know of, the other option that i know is:
`Base.unwrap_unionall(A).parameters[1]`
but that could introduce other issues 
created_at: 2025-05-27T15:51:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+export SoAType
+
+"""
+    ContainerType{A}
+
+Union type representing the possible container types for storing agents.
+"""
+ContainerType{A} = Union{AbstractDict{Int,A},AbstractVector{A}}
path: src/core/agent_containers.jl
user: login: bergio13
body: Just to be clear, you would just like to have Union{AbstractDict{Int,A},AbstractVector{A}} wherever now there is ContainerType{A} ?
created_at: 2025-05-28T08:01:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov-commenter
created_at: 2025-04-07T17:32:35Z
body: ## [Codecov](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) Report
Attention: Patch coverage is `83.92857%` with `9 lines` in your changes missing coverage. Please review.
> Project coverage is 85.09%. Comparing base [(`8b5b456`)](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/commit/8b5b4569638cc30f1bd974a5f6c7d83d86eab9a3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) to head [(`ba6db81`)](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/commit/ba6db81faeb55d63970f41c0988cc358aaf50d91?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics).
> Report is 189 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) | Patch % | Lines |
|---|---|---|
| [src/core/model\_standard.jl](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&filepath=src%2Fcore%2Fmodel_standard.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics#diff-c3JjL2NvcmUvbW9kZWxfc3RhbmRhcmQuamw=) | 78.57% | [3 Missing :warning: ](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) |
| [src/core/model\_abstract.jl](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&filepath=src%2Fcore%2Fmodel_abstract.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics#diff-c3JjL2NvcmUvbW9kZWxfYWJzdHJhY3Quamw=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) |
| [src/core/agent\_containers.jl](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&filepath=src%2Fcore%2Fagent_containers.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics#diff-c3JjL2NvcmUvYWdlbnRfY29udGFpbmVycy5qbA==) | 94.11% | [1 Missing :warning: ](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) |
| [src/core/model\_accessing\_API.jl](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&filepath=src%2Fcore%2Fmodel_accessing_API.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics#diff-c3JjL2NvcmUvbW9kZWxfYWNjZXNzaW5nX0FQSS5qbA==) | 90.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) |
| [src/core/model\_event\_queue.jl](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&filepath=src%2Fcore%2Fmodel_event_queue.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics#diff-c3JjL2NvcmUvbW9kZWxfZXZlbnRfcXVldWUuamw=) | 87.50% | [1 Missing :warning: ](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) |
| [src/core/model\_free\_extensions.jl](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&filepath=src%2Fcore%2Fmodel_free_extensions.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics#diff-c3JjL2NvcmUvbW9kZWxfZnJlZV9leHRlbnNpb25zLmps) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main    #1153       +/-   ##
===========================================
+ Coverage   70.12%   85.09%   +14.97%     
===========================================
  Files          42       38        -4     
  Lines        2718     2563      -155     
===========================================
+ Hits         1906     2181      +275     
+ Misses        812      382      -430     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/JuliaDynamics/Agents.jl/pull/1153?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=JuliaDynamics).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: ameligrana
created_at: 2025-04-07T20:00:14Z
body: There is still an inconsistency in all of this which causes problems. The flocking stepping problem is solved, but the marker problem in Flocking reveals this other problem, the marker is defined as so:

```julia
function bird_marker(b::Bird)
    φ = atan(b.vel[2], b.vel[1]) #+ π/2 + π
    rotate_polygon(bird_polygon, φ)
end
```

the problem with this implementation is that b is not `Bird` but is `AgentWrapperSoA`. This is easy to solve in this case, just use `AbstractAgent` in the signature, but this is in general problematic because the user can't just switch between containers because the signature are actually wrong with `StructVector`. So, this should be fixed.

To do so, maybe we can have a different macro variant for the `StructVector` for `@agent`. It would actually be cool to have e.g.

```julia
@agent array Bird(ContinuousAgent{2, Float64}) begin
    speed::Float64
    cohere_factor::Float64
    separation::Float64
    separate_factor::Float64
    match_factor::Float64
    visual_distance::Float64
end
```

and reject things created with the `struct` version in model creation.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T20:03:23Z
body: or maybe even simpler as

```julia
@agent :SoA struct Bird(ContinuousAgent{2, Float64})
    speed::Float64
    cohere_factor::Float64
    separation::Float64
    separate_factor::Float64
    match_factor::Float64
    visual_distance::Float64
end
```
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T20:08:37Z
body: another solution would be to reject the type when `StructVector` is used and instead accept something like `SoA(Bird)` where `SoA(::Type{A}) = AgentWrapperSoA{...}`, this has the disadvantage that requires to change all signatures by using `SoA(Bird)` instead of `Bird` .
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T20:32:15Z
body: Why are we doing this as an additional container? I think this unecessarily complicates things because it requires us to adjust existing code to accommodate for something the code was never built for. 

Why are we not creating a brand new model type? To my understanding this type of functionality is only useful for rather special time evolution rules, where one broadcast the same operations across agents, rather than having per-agent instructions. I guess the simple `nearby_agents` iteration becomes significantly slower with this new container type. On the other hand, broadcasting across agents is fast.

So why are we not think about just implementing a `StructVectorABM` which can only allow for a struct-of-arrays container?
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T20:37:00Z
body: > in `continuous.jl ` we change `agent.pos = pos` to `model[agent.id].pos = pos`

There is no reason to do this. You just have to overload `setproperty!` for the new agent wrapper type. `agent.pos = x` must work also for the user-facing API.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T20:39:23Z
body: > the problem with this implementation is that b is not `Bird` but is `AgentWrapperSoA`. This is easy to solve in this case, just use `AbstractAgent` in the signature, but this is in general problematic because the user can't just switch between containers because the signature are actually wrong with `StructVector`. So, this should be fixed.

Yes, this is really unfortunate. The way I see it, there is no real way to allow users to use exactly the same code for StandardABM and StructVectorABM. I don't have any ideas how to change this. I don't like making `@agent` even more complicated. It is already as complicated as it can get. Perhaps we can simply instruct the users to not use type declaration in their functions if they want to switch to also use StructVectorABM? The downside is of course that they won't be able to use multi-agent models this way.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T20:39:53Z
body: Does BeforeIT.jl allow for multi-agent models? How do they handle it? I just realized now that actually I have no idea how multi-agent models would be handled at all with this struct of array approach.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T20:41:27Z
body: > I don't like making `@agent` even more complicated. It is already as complicated as it can get. Perhaps we can simply instruct the users to not use type declaration in their functions if they want to switch to also use StructVectorABM? The downside is of course that they won't be able to use multi-agent models this way.

Or we make a new macro `@svagent` that does all the internal magic for StructVectorABM agents...?
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T20:44:38Z
body: Using Struct-of-Arrrays with the Event queue model doesn't make sense. It fundamentally opposes the model's nature that operates exclusively on a per-agent approach. Do you see much use to allow SoA with event queue? Just because we can doesn't mean we should... :D 
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T21:18:47Z
body: you are right on the `EventQueueABM`, it actually doesn't make sense.

But actually it makes sense to have it as a container because it's actually not slower than a `Vector` container, I benchmarked flocking and it is the same, actually it is even slightly faster than a `Vector` in quick&dirty benchmarks, my question is then, instead, why don't we use a `StructVector` instead of a `Vector`? I'm not sure at the moment if there are cases where a `Vector` would still be advantegeous, and to be conservative I would add a `StructVector` as an option instead, and also because unlike a `Vector` it requires this tweak to the macro.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T21:24:39Z
body: ah and also `Vector` is very easy to multiagent, while a multi-StructVector would need something along the lines of https://github.com/JuliaDynamics/Agents.jl/issues/841 which is not trivial. 
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T21:34:11Z
body: at the same time, I think that having it as a new model is okay, I don't see disadvantages. The fact that #841 is needed for multi agent `VectorizedABM` (do you like this name?) is a point in favour of having a different type. 
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T21:38:03Z
body: so this needs to be totally overhauled with a new type, which I propose it could be called `VectorizedABM`. The fact that Flocking, even though it didn't have any vectorized operation is even slightly faster is cool though.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T21:41:05Z
body: in BeforeIT.jl we use #841 multiagent style, we don't have problems of type instability because agents of a certain type always step before agents of another type (actually I don't think type instability would be problematic in any case if #841 is done right)
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T21:43:25Z
body: I want just to note another cool aspect of all of this: we are actually doing what https://github.com/projectmesa/mesa-frames does in 100 locs while they have more than 10000 locs  :D
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T22:03:54Z
body: Actually rethinking a bit about the fact that `EventQueueABM` is not sensible with a `StructVector`, I'm actually not sure if this is so, in a sense a `StructVector` could be handy whenever you want to do vectorized operations inside an event of a single agent, even though this is rare, this is actually sensible to have vectorized because that could be the most expensive operation inside all the events
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T22:04:55Z
body: > benchmarked flocking and it is the same, actually it is even slightly faster than a `Vector` in quick&dirty benchmarks, my question is then, instead, why don't we use a `StructVector` instead of a `Vector`?

Where are these benchmarks? I find this hard to believe given the extensive testing that we have done for Struct of Arrays in the past...
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T22:06:31Z
body: so all in all, I'm actually opposed to have a new type, given that effectively we are not changing the rules of the game as in `EventQueueABM`, but we are just changing container
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T22:08:25Z
body: > The fact that #841 is needed for multi agent `VectorizedABM` (do you like this name?) is a point in favour of having a different type

I don't understand where the reference to #841 is coming from. Actually, why is #841 even open? This issue should be closed because your recent additions to `@multiagent` removed the dynamic dispatch.

The core issue of #841 remains, and will remain forever: as long as we are obtaining agents by ID, this fundamental type instability will remain. One needs to add an additional interface so that agents are obtained by type, instead of by ID. Then you would loose the power to obtain agents by ID but you could still iterate over all agents of a particular type. That's all fine. It just needs a different function, and hence, I would argue, a new model type. It makes it much easier to have this SoA model where you obtain agents by type instead of by ID.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T22:08:51Z
body: > Where are these benchmarks? I find this hard to believe given the extensive testing that we have done for Struct of Arrays in the past...

I cook up a benchmark in a second and show this :D
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T22:10:18Z
body: > I want just to note another cool aspect of all of this: we are actually doing what https://github.com/projectmesa/mesa-frames does in 100 locs while they have more than 10000 locs :D

Yeah, I am not surprised...
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T22:11:27Z
body: > > I want just to note another cool aspect of all of this: we are actually doing what https://github.com/projectmesa/mesa-frames does in 100 locs while they have more than 10000 locs :D
> 
> Yeah, I am not surprised...

We should write a blog post about this or something when this PR is nice and done. The Julia community will appreciate it.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T22:20:02Z
body: benchmark with the code in this pr:

```julia
using Agents, LinearAlgebra

@agent struct Bird(ContinuousAgent{2, Float64})
    const speed::Float64
    const cohere_factor::Float64
    const separation::Float64
    const separate_factor::Float64
    const match_factor::Float64
    const visual_distance::Float64
end

function flocking_model(rng, extent, n_birds, visual_distance, C;
        speed = 1.0, cohere_factor = 0.03, separation = 1.0, separate_factor = 0.015,
        match_factor = 0.05, spacing = visual_distance / 1.5,)
    space2d = ContinuousSpace(extent; spacing)
    T = C == StructVector ? SoAType{Bird} : Bird
    model = StandardABM(T, space2d; agent_step!, rng, container = C,
        scheduler = Schedulers.Randomly())
    for n in 1:n_birds
        vel = SVector{2}(rand(abmrng(model)) * 2 - 1 for _ in 1:2)
        add_agent!(model, vel, speed, cohere_factor, separation,
            separate_factor, match_factor, visual_distance)
    end
    return model
end

function agent_step!(bird, model)
    neighbor_agents = nearby_agents(bird, model, bird.visual_distance)
    N = 0
    match = separate = cohere = SVector{2}(0.0, 0.0)
    for neighbor in neighbor_agents
        N += 1
        heading = get_direction(bird.pos, neighbor.pos, model)
        cohere += heading
        match += neighbor.vel
        if sum(heading .^ 2) < bird.separation^2
            separate -= heading
        end
    end
    cohere *= bird.cohere_factor
    separate *= bird.separate_factor
    match *= bird.match_factor
    bird.vel += (cohere + separate + match) / max(N, 1)
    bird.vel /= norm(bird.vel)
    move_agent!(bird, model, bird.speed)
end

using BenchmarkTools, Random

rng = Xoshiro(42)
model = flocking_model(rng, (100, 100), 5000, 3.0, Vector)
@benchmark step!($model)

rng = Xoshiro(42)
model = flocking_model(rng, (100, 100), 5000, 3.0, StructVector)
@benchmark step!($model)
```

we have

```julia
julia> rng = Xoshiro(42);

julia> model = flocking_model(rng, (100, 100), 5000, 3.0, Vector);

julia> @benchmark step!($model)
BenchmarkTools.Trial: 168 samples with 1 evaluation per sample.
 Range (min … max):  22.428 ms … 45.217 ms  ┊ GC (min … max):  0.00% … 25.77%
 Time  (median):     29.986 ms              ┊ GC (median):    13.59%
 Time  (mean ± σ):   29.815 ms ±  3.007 ms  ┊ GC (mean ± σ):  13.16% ±  4.63%

                     █▄▃▃  ▃   ▆▅▂ ▄ ▂                         
  ▄▁▅▁▅▃▄▃▁▁▁▁▁▁▁▁▁▃▆█████▇█▇▆▆███▆████▃▇▄▄▃▁▁▁▁▃▃▄▃▃▁▁▁▁▃▁▁▃ ▃
  22.4 ms         Histogram: frequency by time          38 ms <

 Memory estimate: 36.14 MiB, allocs estimate: 1183942.

julia> rng = Xoshiro(42);

julia> model = flocking_model(rng, (100, 100), 5000, 3.0, StructVector);

julia> @benchmark step!($model)
BenchmarkTools.Trial: 192 samples with 1 evaluation per sample.
 Range (min … max):  19.608 ms … 36.707 ms  ┊ GC (min … max):  0.00% … 0.00%
 Time  (median):     25.666 ms              ┊ GC (median):    14.69%
 Time  (mean ± σ):   26.063 ms ±  2.762 ms  ┊ GC (mean ± σ):  14.82% ± 5.18%

                  ▃   ▃▆▂▂       █                             
  ▄▅▄▁▁▁▁▁▁▁▃▁▁▁▄▅█▅▆▆█████▄▇█▆▆███▄▆▇▄▃▁▁▁▃▄▁▃▃▄▃▁▄▁▄▁▁▁▁▃▁▄ ▃
  19.6 ms         Histogram: frequency by time        34.1 ms <

 Memory estimate: 38.72 MiB, allocs estimate: 1268139.
```
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-07T22:24:10Z
body: in general, we have some pros and cons for a new type from my perspective, I'm not sure what's better actually
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T22:58:48Z
body: can you run the same with the agent fields being non-constants?
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-07T22:59:45Z
body: In general, we should also be benchmarking this with a model that has (a) agent field changing and (b) agents being added and removed. I think the wolf sheep model is a good test case for this.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-08T06:30:24Z
body: that is impossible because we don't support either multiagent or removal of agents, the latter of which is probably impossible to support efficiently in any case
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-08T07:53:03Z
body: > that is impossible because we don't support either multiagent

For the removal part, okay, but if this cannot support multiagent, then it cannot be a generic container either right? ALl the more reason to make this a separate model.

I would still argue, it is better for start to make this a separate model type, and once it has seen a bit more testing, see if it can generalize as a container also for `StandardABM`. There is no reason to be greedy and try to achieve everything in one go.

We can label this as an expertimental feature on release. if all goes well and it can become part of `StandardABM` we simply deprecate the experiemtnal function.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-08T14:30:55Z
body: ok, to be it is fine, it is sensible to track it as experimental and make it a type while we learn about its working. The only concern I have is that I see use cases for event based ABMs, e.g. they can be faster for single-agent event-based ABMs and allow some better vectorization, so maybe we could have two types: a `VectorizedABM` and a `VectorizedEventABM`? It is a bit cumbersome but this is the way to go if we want to add types instead of a container
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-08T15:01:10Z
body: @Tortar something is off. I am looking at your benchmark and comment here: https://github.com/JuliaDynamics/Agents.jl/pull/1153#issuecomment-2784759416 . How can it be that the StructVector case is faster? There is not a single operation that is done across agents. Sure, the actual dynamics of the model are the same for every agent, but there is no code instruction to broadcast the operations. There is no `@~` or something that takes advatange the struct vector datastructure. So how can it be faster? Are you sure things are correct in your comment https://github.com/JuliaDynamics/Agents.jl/pull/1153#issuecomment-2784759416 ?
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-08T15:13:26Z
body: it should'nt be off, if you want to be sure you just need to run the benchmarks and see if you can reproduce, on my computer those are the results when using this branch. The fact that there aren't any vectorized operation and that this is even a bit faster breaks my mental model too, but this is the truth (at least in this case).
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-08T15:24:25Z
body: The difference in flocking becomes even bigger and bigger more you add agents, e.g. same benchmark as above but with 10^5 agents, one step with the StructVector requires 1.5 seconds, while with a Vector it requires 2.6 seconds
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T08:10:39Z
body: I can confirm that the struct-vector example is faster, which confuses me greatly. It shouldn't be...? In any case, this means that we should prioritize merging this PR because this is a big positive change. Here are the important things to do:

1. Organize source code better. There should a dedicated file `core/agent_containers.jl` that has all the business of agent containers.
2. To simplify, the `construct_agent_container` function should give both the type `C` as well as the `agents` empty container. Then these `if` clauses are not necessary anymore. 
3. Orthogonalize the agent container from the model type. Just like we have orthogonalized the space from the model type, we now need to make the same for the agent container. It really shouldn't matter how the agents are stored and how the temporal dynamics play out! These are orthogonal. So just like we have a `add_agent_to_space!` function we should have an `add_agent_to_container!` function, and similar for remove. This allows us to separate the high level `add_agent!` from the "internal" or _operation performing_ functions, and also brings well needed clarity to the source code: we will now know exactly where each operation occurs at.
4. Implement the agent-wide broadcasting macro, or at least document how to do it directly using StructVecrtors.jl
5. Add tests, documentation, changelog entry, and bump version. 

I'll then do a more detailed line-by-line review.

---

I don't know why this is such a surprise. Either newer Julia versions have improved this data layout, and the compiler code generation from it drastically. Or, the original SoAs implementation in #492 was so different to the one here that it simplu couldn't match the performance.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T08:10:52Z
body: cc @bergio13  
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T08:17:02Z
body: We also need to implement removal. @Tortar you said that agent removal and addition doens't work here, but why? The struct-vector object simply has fields that are normal `Vector`s; We can simply `pop!` all such fields at a given `id` location. We just need to first find the `id` from the `.id` field of the struct vector, because as you remove agents it is no longer true that agent id = location in the inner vectors. 

Am I missing something on why addintion and removal of agents shouldn't work with StructVector? I am not considering performance at all here.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-12T11:57:00Z
body: This can work for removal, but the model will have abysmal performance after the removal for indexing so I don't think this is worth doing. To make it a bit better, we can instead not `pop!` it but using `removeat!`, so that you will still preserve the invariant of ordering, then you can at least use binary search for indexing, removal will be o(n) though. And this will add probably a non-zero cost for any model, because you still need to understand if `agent id = location in the inner vectors` is true even for the no-removal case, though the branch is perfectly predictable in this case so maybe the cost will be infinitesimal. In any case, this doesn't seem worth doing to me, because it has this kind of trade-offs which can only be undestood by benchmarking and the case of removal is probably better with a `Dict` in any case, so I think this PR should not try to chase something that is not clear if it is worth or not. Maybe a new PR could try it, we could open an issue on this to track it (All of this applies the same for a `Vector` by the way).

Also, what I'm not understanding from your words is if you think that `StructVector` should be a container in the end, or inside a new model type?
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-12T11:58:06Z
body: If we go with a container, we need also a new macro, or a specialization of the `@agent` macro.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T12:30:42Z
body: > If we go with a container, we need also a new macro, or a specialization of the `@agent` macro.

What do you mean? There is no change at least in the flocking example we are running with `@agent`...?
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-12T13:14:30Z
body: yes, but the problem is that the `StructVector` implementation passes around the wrapper, which causes problems whenever you dispatch on the agent type because it is not what it is actually passed around, so we need a new variant of the macro/a new macro to make the type refer to the wrapper
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T13:36:39Z
body: > the model will have abysmal performance after the removal for indexing so I don't think this is worth doing.

Yes, you are right. At the moment we can just access the vector entries using the ID field. If we allow removal of agents, we then every time we access a property must first find the index corresponding to agent ID. This is not just a huge hit to performance - it will likely also prevent parallelization . 

Well, actually, this isn't a surprise, this is the fundamental reason ABMs are not parallelizable _in general_: the removal of agents.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T13:37:18Z
body: > yes, but the problem is that the `StructVector` implementation passes around the wrapper, which causes problems whenever you dispatch on the agent type because it is not what it is actually passed around, so we need a new variant of the macro/a new macro to make the type refer to the wrapper

I udnerstand the problem but I don't fully undertstand the proposed solution. Perhaps you can leave comments on the PR for the contributor to implement.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-12T16:06:34Z
body: There are two possible solutions:

1. Do not accept the type itself when doing `StandardABM(Type, ...; container = StructVector)` but instead accept something like  `StandardABM(SoA(Type), ...; container = StructVector)`. This means that you need to change any signature annotation to `Type` to `SoA(Type)`, which is a bit cumbersome.

2. A new macro e.g. `@agentsoa ...` or create a new variant `@agent :soa ...` and then the `Type` will already be a `SoA(Type)` so no need to change the signatures.

The second solution seems better to me, and the first version of the proposal seems better than the second, so `@agentsoa` has my vote.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-12T16:43:10Z
body: mmmh, thinking about it, 2. is very tricky and it is too opaque for the user e.g. `typeof` of an agent created inside the step won't have the same type as `model[id]`, so it will cause tricky bugs. Going with 1. instead the user is in full control of all the nuances. We just need to add to the docs that `model[id]` will be of type `SoA(Type)` instead of `Type` and so signatures should be adjusted accordingly.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-12T17:40:46Z
body: Sound like to me both options don't fix all the problems,. Perhaps the real solution is to simply document clearly that when you do `model[id]` the agent type isn't what you expect. Actually, we should only worry about this if multi-agent models are even a viable option with strcut of arrays. So far we have only tested it with a single agent type and there it seems to be performant. We went to hell and back to make the multi agent normal case performant, let's see how easy it will be here. The `@multiagent` macro clearly doen't work for struct vectors.
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-12T18:59:21Z
body: yes, the first option I suggested doesn't do actually anything very sophisticated, you just give a convenience function to access the wrapper type of the model, and you force the user to acknowledge that this difference exist by rejecting normal types. This is important even in cases there is only one agent type because the agent constructor won't have the same type of the indexing in the model. So I think there is value in adding this even with single agent models.

In any case, everything is settled from a discussion standpoint in my opinion, and just the work needs to be done :D
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-18T15:04:23Z
body: Jolting down some brief ideas for multi-agent types with this approach after the discussion in the JuliaDynamics meeting

- the container can be a tuple/named tuple of struct vectors , each corresponding to an agent type. 
- the "id" has to be come a composite type. Users would have to request an agent by type and id, such as `model[(Bank, 23)]`. 
- similarly, the `allagents` function can have an extra argument verfsion that returns all agents of a given type.

This approach has problems with space. What does `nearby_ids` do for such a data structure? What sorts of IDs would be stored in the space containers? At least for integration downstream in e.g., BeforeIT.jl, there is no space. So we don't have to worry about this. SO there is a real use case for a specialized struct-vector model that does not allow for spaces.
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-18T15:16:53Z
body: oh yeah we still have the problem that exists in this PR: the agent type is this wrapper type. But there your solution is fine for me at least:



> Going with 1. instead the user is in full control of all the nuances. We just need to add to the docs that `model[id]` will be of type `SoA(Type)` instead of `Type` and so signatures should be adjusted accordingly.


author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-24T14:28:10Z
body: Documentation fails because of "This is a scheduled Ubuntu 20.04 retirement. Ubuntu 20.04 LTS runner will be removed on 2025-04-15. For more details, see https://github.com/actions/runner-images/issues/11101". So the runner should be updated.
author_association: MEMBER
user: login: bergio13
created_at: 2025-04-26T13:36:41Z
body: I reckon the main changes have been implemented for the single agent type and the PR is now ready to be merged
author_association: CONTRIBUTOR
user: login: bergio13
created_at: 2025-04-27T08:17:13Z
body: > To me the PR seems in good shape, thanks @bergio13 for the work!

Thank you for your help and your reviews !
author_association: CONTRIBUTOR
user: login: ameligrana
created_at: 2025-04-27T18:17:35Z
body: Can you bump to 6.3 @bergio13 and add to CHANGELOG.md that this new experimental feature is now available?
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-28T18:14:44Z
body: I merged so that we can consider this contribution for GSOC, I think that if there is any minor problem with this PR, we can tackle it in a small follow-up
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-28T18:25:15Z
body: > I merged so that we can consider this contribution for GSOC, I think that if there is any minor problem with this PR, we can tackle it in a small follow-up

please don't merge PRs that introduce new interface elements without an approving review from someone that has already left a review (in this case, me).
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-28T18:25:42Z
body: For now, please don't tag a release, and if you have done so retract it. I will review the code base as is now whenI can. 
author_association: MEMBER
user: login: ameligrana
created_at: 2025-04-28T18:52:19Z
body: yes, I didn't want to release any new version until everything was finalized
author_association: MEMBER
user: login: Datseris
created_at: 2025-04-28T19:52:30Z
body: @bergio13 I've left some comments on this PR, please do consider making a second PR that addresses them.
author_association: MEMBER
user: login: bergio13
created_at: 2025-04-30T16:55:38Z
body: > @bergio13 I've left some comments on this PR, please do consider making a second PR that addresses them.

Sure, I will soon try to address the issues you highlighted
author_association: CONTRIBUTOR