ABM Agents.jl model properties as a nested dictionary

Dear Julia and the Agents.jl community,

I’m currently working on a multispecies agent-based model (ABM) with sardine and anchovy superindividuals, and I’m planning to transition to the multi-agent framework soon. My model includes numerous properties because it is a bioenergetic model with over 50 parameters for each species. These parameters change depending on the agent species, and I need my step functions to access them.

To organize these properties, I tried to set up a nested dictionary structure for the model parameters. Here’s a simplified version of my property function generator:

@agent struct Fish(NoSpaceAgent)
    # Basic characteristics
    species::Symbol           # :sardine, :anchovy
    type::Symbol              # :eggmass, :juvenile, :adult
end

function create_params_dict(Meggsardine, Megganchovy)

    # Define the dictionary
    model_parameters = Dict(
        :natural_mortalities => Dict(
            :sardine => Dict(
                :M_egg => Meggsardine,
                :Madults => 4.0),
            :anchovy => Dict(
                :M_egg => Megganchovy,
                :M0 => 5.0)
                ),
        :bioparams => Dict(
            :sardine => Dict(
                :energy => 5.0,
                :growth=> 2.5),
            :anchovy => Dict(
                :energy => 3,0,
                :growth => 8.0)
                ),
        :output => Dict(
            :sardine => Dict(
                :TotB => 0.0),
            :anchovy => Dict(
                :TotB => 0.0)
                ))

    return model_parameters
end

In this approach, I can provide different function arguments depending on the simulation goal, initialize fixed parameters, and set output keys to zero. The motivation behind using nested dictionaries is to avoid adding suffixes to parameter names to distinguish which species they refer to. As mentioned, I have many more parameters in the full model.

I have several different step functions in my model. While I initially considered using function dispatch based on the agent type, the code was becoming quite lengthy and functions were doing the same job just with different set of parameters. Instead, I opted to structure my step functions so that they access the appropriate subset of model properties depending on the agent’s :species. This way, each species-specific data is retrieved dynamically within the corresponding step function.

function egghatch!(Fish, model)
    if is_sardine(Fish)
        deb_species = NamedTuple(model.bioparams[:sardine])
    else
        deb_species = NamedTuple(model.bioparams[:anchovy])
    end

* function code*
        end

However, I’m running into issues when using functions like init_model_data() and collect_model_data(). The problem arises because the model data (mdata) can be either a vector or a generator function, and I’m unsure how to handle this effectively with my current dictionary structure.

Has anyone else dealt with a similar challenge? Any advice on how to improve the structure for better compatibility with init_model_data() and collect_model_data() in a multi-agent context would be greatly appreciated.

I’m sure there’s a more efficient and scalable way to structure the simulation framework.

Thanks in advance!

I am sorry, I don’t understand the problem. Can you please try to rephrase it, or give a concrete (runnable) example of the problem?

I also have not understood what you wish to achieve with init_model_data() as this is a low level function that really shouldn’t be called by the user directly unless the user is implementing an advanced and custom data collection routine that does not appear to be the case for you.

As far as I can tell if you wanted a particular set of fields from your model parameter dictionary you could make the following:

mdata = [
model -> model.bioparams[:sardine],
model -> model.bioparams[:anchovy],
]

this is completely fine code and when given to run! it would extract two model data, the sardine and anchovy bioparameters.

Thank you for replying! I will come asap with a runnable easy example, thank you!

@Datseris Dear George, thank you! your mdata version worked with the init_model_data() function. When I started using Agents.jl, I went through some of the examples and saw the possibility of using that function to initialize the mdata ad adata as dataframes. But I couldn’t make it work with a nested mdata dictionary.

To be complete, here it is a simpler and runnable version of how I set up the framework of my simulation. Nested model properties because I have many parameters and a complex_step() function to run specific agent_step functions at certain moments (also with @thread).

Hope you have a look and tell if there is something that could be definitely improved:
Thank you for helping again!

# WORKING CODE
using Agents

@agent struct Fish(NoSpaceAgent)
    # Basic characteristics
    species::Symbol           # :sardine, :anchovy
    type::Symbol
    energy::Float64              # :eggmass, :juvenile, :adult
end

# simplified model properties
function create_params_dict(Nsardine, Nanchovy)
    # Define the dictionary
    model_parameters = Dict(
        :environment => Dict(
            :time_step => 1.0,
            :Nsardine => Nsardine,
            :Nanchovy => Nanchovy,
        ),    
        :bioparams => Dict(
            :sardine => Dict(
                :growth=> 2.5),
            :anchovy => Dict(
                :growth => 8.0)
                ),
        :output => Dict(
            :sardine => Dict(
                :TotB => 0.0),
            :anchovy => Dict(
                :TotB => 0.0)
                ))
    return model_parameters
end

#stepping function

function grow(agent::Fish, model::ABM)

    #same function for both species but different params must be accessed
    # Get the energy and growth rate of the species
    growth = model.bioparams[agent.species][:growth]

    # Update the energy of the agent
    agent.energy += growth

    # If the agent has enough energy, it reproduces
    if agent.energy > 10.0
        add_agent!(Fish, model, agent.species, :juvenile, 1.0)
        if agent.species == :sardine
            model.environment[:Nsardine] += 1
        else
            model.environment[:Nanchovy] += 1
        end
        agent.energy = 0.0
    end
    return
end

function environment(model::ABM)
    # Update the environment and calculate biomass
    model.environment[:time_step] += 1

    # Update the total biomass
    model.output[:sardine][:TotB] = model.environment[:Nsardine] *
                                                 model.bioparams[:sardine][:growth]
    model.output[:anchovy][:TotB] = model.environment[:Nanchovy] *
                                                 model.bioparams[:anchovy][:growth]
end

function complex_step(model::ABM)

    # simplification of my model: I need a complex stepping
    # since I use @threads and call agent step functions in specific moment of the simulation

    # Update the agents - here I use @threads in my sim
    for agent in allagents(model)
        grow(agent, model)
    end
    # Update the environment
    environment(model)
end


function model_initialize(Nsardine, Nanchovy)
    properties = create_params_dict(Nsardine, Nanchovy) 
    model = ABM(
        Fish;
        properties = properties,
        model_step! = complex_step
    )

    for _ in 1:Nsardine
        add_agent!(Fish, model, :sardine, :adult, 1.0)
    end

    for _ in 1:Nanchovy
        add_agent!(Fish, model, :anchovy, :adult, 1.0)
    end
return model
end

abm = model_initialize(1, 1)

adata = [:type, :species, :energy]
mdata = [model -> model.environment[:time_step], 
model -> model.environment[:Nsardine], 
model -> model.environment[:Nanchovy], 
model -> model.output[:sardine][:TotB], 
model -> model.output[:anchovy][:TotB]]

results = []
df_agent = init_agent_dataframe(abm, adata)
df_model = init_model_dataframe(abm, mdata)

# Run the simulation
df_agent = run!(abm, 20; adata, mdata)

push!(results, df_agent)