ABM - Scheduler.bytype just for 1 agent function

Dear all,
I am running an ABM model with NoSpaceAgent representing sardines populations over time with three lifestages identified by the field type::Symbol and with 2 sexes (sex::Symbol).
Adults can spawn in a period of the year and I am trying to apply a weird reproduction rule (based on a previous model which was written in NetLogo); the function was letting reproducing bigger females first with the biggest male available. Males should never reproduce more than 10 times (10 different females; Engaged::Int64 < 10).
I created a MWE down here, a simplification of my simulation with comments and already my question listed, that I will report also here.

  1. basically all the functions working on the agents actually do not need the Sardine (agent) argument as I wrote them. In my simulation instead, other functions need the arguments function(agent,model) to work. This is confusing to me and I wonder if it can cause problems. especially adultspawn use a loop to go through the females and males, create copies of them and the access the model.agents[id] to modify the features. I guess it’s not the proper way to work but I started translating a previous ABM from Netlogo which was working in this way.

2. adultspawn can be modified work through an advanced scheduler.bytype? From the documentation for advanced scheduling I didn’t get how, it seems to me that the scheduler would be valid for all the functions in the simulation, yet I want to use the scheduler.bytype just for adultspawn

  1. I don’t understand why the reset_engaged(Sardine, model) version in the comment is not working.

Could you give me some advice or explanations? It would be amazing. Thank you all in advance!

using Agents

@agent Sardine NoSpaceAgent begin
    type::Symbol #adult
    sex::Symbol #male,female
    engaged::Int64
    size::Int64 #number of times the agent reproduced in 1 timestep
end

function generate_adult(N, model)
    for _ in 1:N
        type_agent = :adult
        sex_agent = rand((:male, :female))
        engaged_agent = 0
        size_agent = rand(100:200)
        add_agent!(Sardine, model, type_agent, sex_agent, engaged_agent, size_agent) 
    end
    return
end

# support function to sort agents by size
function sort_agent(agent_type, sex, model, feature)
    all_agents = collect(values(allagents(model)))
    sorted_agents = sort(filter(agent -> (agent.type == agent_type) && (agent.sex == sex), all_agents), by=agent -> getfield(agent,Symbol(feature)), rev=true)
    return sorted_agents
end

#scheduler default faster
properties = Dict(:timesteps => Int64(0))
model = ABM(Sardine; properties=properties)
generate_adult(40, model)


# stepping functions --
#increase timestep
 function evolve_model(model)
    model.timesteps += 1
    println("timestep ", model.timesteps)
    return
 end

 # each timestep I reset engaged and let them spawn
 function sardine_step(model) 
    # (?.1)here Sardine argument is useless, in my complete simulation 
    # however I have other function tht need Sardine argument to work at each timestep.
    reset_engaged(model)
    adultspawn(model)
    return
end

function reset_engaged(model) 
    for agent in allagents(model)
        agent.engaged = 0
    end
    return
end

# (?.2) not clear why this alternative was not working
#function reset_engaged(Sardine, model) 
#        Sardine.engaged = 0
#    return
#end

function adultspawn(model)
    #sort males and females by size
    sorted_female = sort_agent(:adult, :female, model, "size")
    sorted_males = sort_agent(:adult, :male, model, "size")

    for f in sorted_female

        current_male_index = 1
        #check if there are no males
        if isempty(sorted_males)
            println("no males available")
            break
        else
            partner = sorted_males[current_male_index]
        end

        #choose the biggest male for each sorted female
        if partner.engaged >= 10  #number of times reproduced
            #if reproduced more than 10 times, pick the second biggest male
            current_male_index += 1
            if current_male_index > length(sorted_males)
                println("No more males available for reproduction")
                break
            else
                partner = sorted_males[current_male_index]
            end
        end

        #add event of reproduction
        partner.engaged += 1
        f.engaged += 1

        # Update the agents in the model
        model.agents[partner.id].engaged = partner.engaged
        model.agents[f.id].engaged = f.engaged
    end

    return
end

# run the model and store results

adata = [:sex, :size, :engaged]
df_agent = init_agent_dataframe(model, adata)

df_agent = run!(model, sardine_step, evolve_model,100; adata)
#takes approx 1 sec at timestep --> slooow

df_agent1 = sort!(df_agent[1], :size, rev=true)
show(df_agent1, allrows=true, allcols=true)

#seems to work but it seems slow and I would like to use a scheduler.bytype 
#for adultspawn to not create copies of the agents but call the Sardine argument. 
#Is it possible?

In Agents.jl the idea of agent_step! and model_step! functions are used. The former describes what a single agent should do each time step when they’re activated. The latter describes everything else that should happen in the model each time step. That’s why their function signatures are agent_step!(agent, model) and model_step!(model) respectively. The order by which agents are activated (i.e. do the thing inside your agent_step! function) is determined by the scheduler function. Of course you can call your own agent_step! and model_step! functions whatever you want (like you already did in your code) but you should stick to the arguments they take in.

You’ve put Sardine here instead of sardine. Note the capital S which references your agent type that you’ve defined at the beginning of the file. You want to use sardine (or any other name for that argument) to reference an instance of Sardine which is one of your agents. If you want to define that the reset_engaged function can only be called with Sardine agents, you can specify the type reset_engaged(sardine::Sardine, model).

  1. Make it sardine_step(sardine, model)
  2. Consider moving reset_engaged(model) (or just its content, i.e. the for-loop) into your evolve_model(model) function.
  3. Make it adultspawn(sardine, model)
  4. Separate the sorting logic for sorted_female and sorted_males from the actual agent stepping function. The sorted_female list should be your scheduler function (i.e. which agents get activated in which order) and the sorted_males list could for example be stored as a model property that gets updated once per timestep inside the evolve_model function.
  5. Now that you’re scheduling according to your sorted list of adult females, you can remove the for-loop from inside the adultspawn function. This is now handled by Agents.jl in the background, so you don’t have to take care about looping over the set of agents anymore.
  6. To first have your model reset the engaged status of your sardines, add the agents_first keyword argument to your run! call: run!(model, sardine_step, evolve_model,100; adata, agents_first = false) This now first executes your evolve_model function once and then your sardine_step function for each agent. See here in the documentation.

I probably forgot a few things but hopefully this information gets you started and helps you get your model running correctly. Feel free to share your progress on this and also feel free to ask if you have further questions.

2 Likes

Many Many Thanks! I had time now to read in deep your comment and suggestion and now it’s much more clear to me.
I will try what you suggested and in case back here. Thank you again!