Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add create_agents factory method to Agent #2351

Merged
merged 26 commits into from
Dec 4, 2024

Conversation

quaquel
Copy link
Member

@quaquel quaquel commented Oct 13, 2024

Summary

Add a new create_agents factory method to the Agent class that enables batch creation of agents with flexible parameter handling. This method simplifies the creation of multiple agents by supporting both uniform and per-agent parameter values.

Motive

This addresses #2221 by providing a more efficient and flexible way to create multiple agents. Currently, users need to write explicit loops when creating multiple agents with varying parameters. This factory method streamlines the process and makes the code more readable and maintainable, especially when dealing with agents that have multiple initialization parameters.

Implementation

  • Added create_agents as a class method to Agent
  • The method accepts:
    • Required model and n parameters for the model instance and number of agents
    • Variable positional and keyword arguments that can be either:
      • Single values (applied to all agents)
      • Sequences of length n (unique value for each agent)
  • Uses a helper ListLike class to make single values behave like sequences
  • Returns an AgentSet containing all created agents
  • Added unit tests to verify functionality with both uniform and per-agent parameters

Usage Examples

Basic usage with uniform parameters:

class Sheep(Agent):
    def __init__(self, model, energy, age=0):
        super().__init__(model)
        self.energy = energy
        self.age = age

# Create 10 sheep with same energy and age
sheep = Sheep.create_agents(model, 10, 100, age=0)

Mixed uniform and per-agent parameters:

# Create 100 sheep with varying energy levels but same age
energies = model.rng.uniform(50, 150, size=100)
sheep = Sheep.create_agents(model, 100, energies, age=0)

Complex initialization with multiple varying parameters:

n = 50
energies = model.rng.uniform(50, 150, size=n)
ages = model.rng.integers(0, 5, size=n)
locations = [(x, y) for x, y in zip(model.rng.integers(0, 10, n), 
                                   model.rng.integers(0, 10, n))]

sheep = Sheep.create_agents(model, n, energies, 
                           locations=locations, ages=ages)

Additional Notes

  • Current limitations:
    • Handle sequences that should be applied uniformly to all agents (e.g., if an agent parameter itself is a list) is possible, but not convenient.
    • No built-in support for distribution objects as parameters
    • These limitations could be addressed in future iterations based on user feedback
  • Full backward compatibility maintained - existing agent creation patterns continue to work

@quaquel quaquel added discuss feature Release notes label 2 - WIP labels Oct 13, 2024
@quaquel quaquel added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Oct 13, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔴 +11.6% [+10.6%, +12.6%] 🟢 -3.9% [-4.0%, -3.6%]
BoltzmannWealth large 🔴 +15.7% [+15.3%, +16.3%] 🔵 -0.3% [-0.8%, +0.1%]
Schelling small 🔵 +0.9% [+0.3%, +1.4%] 🔵 +0.1% [-0.1%, +0.3%]
Schelling large 🔵 +1.4% [+0.3%, +2.6%] 🔵 +1.3% [-0.6%, +3.4%]
WolfSheep small 🔴 +4.1% [+3.8%, +4.5%] 🟢 -7.6% [-10.8%, -4.5%]
WolfSheep large 🔴 +5.7% [+4.1%, +8.1%] 🔵 +3.7% [+0.7%, +7.4%]
BoidFlockers small 🔵 +1.2% [+0.7%, +1.6%] 🔵 -0.8% [-1.5%, -0.2%]
BoidFlockers large 🔵 +2.4% [+1.2%, +3.7%] 🔵 +0.4% [-0.1%, +0.9%]

@projectmesa projectmesa deleted a comment from github-actions bot Oct 13, 2024
@EwoutH
Copy link
Member

EwoutH commented Oct 13, 2024

Thanks for taking a shot as this, I’m intrigued why it increases code complexity in the examples instead of decreases it. Probably because of the lists that need to be created before create is called.

Let me think a bit about this. Maybe some elegant AgentSet stuff can help here.

Talking about that, would we like to (optionally) return an AgentSet with the just created Agents? Then you can directly start chaining.

@quaquel
Copy link
Member Author

quaquel commented Oct 13, 2024

Thanks for taking a shot as this, I’m intrigued why it increases code complexity in the examples instead of decreases it. Probably because of the lists that need to be created before create is called.

Lines of code and code complexity are not the same thing. But yes, since we are using python.random rather than numpy.random, we need list expressions. This adds a bit of overhead because we iterate seperately over n_agents for all arguments.

Talking about that, would we like to (optionally) return an AgentSet with the just created Agents? Then you can directly start chaining.

I have been wondering about this as well. On the one hand, it woudl be convenient to have it. On the other hand, it would add overhead and I can't immediately think of a use case were you want to operate on the agents immediately in Model.__init__

@quaquel quaquel removed the 2 - WIP label Oct 15, 2024
@quaquel
Copy link
Member Author

quaquel commented Oct 15, 2024

I have updated the code a little bit. You can now pass either a sequence of length n (i.e., the number of agents to create) or any other object as argument/keyword argument. If a given argument is a sequence of length n the code assumes that each entry is for a different agent. Otherwise, the code assumes that the entire object needs to be passed to each Agent.

@quaquel
Copy link
Member Author

quaquel commented Oct 16, 2024

I updated the benchmarks to use self.rgn. This cleans up the number of lines of code a bit and is quite convenient.

The current implementation needs further refinement. I check explicitly for each argument and keyword argument if it is a list and if its length equals the number of agents to be created. This is too restrictive. For example, a tuple or ndarray fails on this test. It also rules out generator objects (as in the boltzman position example where I have to wrap zip inside list). However, I also think this might become quite a convenient way to instantiate many agents.

What do others think? I am mainly looking for feedback at the API level and input to develop this generator method one step further.

Also, it might be convenient to have some additional methods on CellCollection, one to draw N cells with replacement and one to draw N cells without replacement. For placing agents randomly both are quite usefull. The first in case of a a Grid where capacity > 1. The second where capacity == 1 and N agents < total number of cells.

@quaquel quaquel mentioned this pull request Oct 19, 2024
@quaquel
Copy link
Member Author

quaquel commented Nov 4, 2024

I have picked this up again, and it's ready for a review.

It now supports list, numpy array, and tuples. Since the code relies on checking whether the length of each keyword argument is equal to n, it is not possible to support generators directly. You can check the benchmark models for examples on how to use it, but for convenience, I put it here as well:

    Wolf.create_agents(
        self,
        self.initial_wolves,
        self.rng.random(self.initial_wolves) * 2 * wolf_gain_from_food,
        wolf_reproduce,
        wolf_gain_from_food,
        [
            self.grid.all_cells.select_random_cell()
            for _ in range(self.initial_wolves)
        ],  # this is why I want to add draw_cells or something like that to CellCollection in a seperate PR
    )

   # in the simple case of width == height, you could reduce this to a one-liner
    positions = list(
        zip(
            self.rng.integers(0, self.grid.width, n),
            self.rng.integers(0, self.grid.height, n),
        )
    )
    MoneyAgent.create_agents(self, self.num_agents, pos=positions)

@quaquel quaquel requested a review from EwoutH November 4, 2024 18:46
@quaquel quaquel linked an issue Nov 4, 2024 that may be closed by this pull request
@EwoutH
Copy link
Member

EwoutH commented Dec 3, 2024

Could you rebase this PR and resolve conflicts? The benchmark models got removed, for example.

Sorry for the trouble.

@quaquel
Copy link
Member Author

quaquel commented Dec 3, 2024

done

Copy link
Member

@tpike3 tpike3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@EwoutH
Copy link
Member

EwoutH commented Dec 3, 2024

I was contemplating a bit about when this would be really useful (yeah yeah I know, I opened the original issue), especially because those newly created agents are now lost in the void of model.agents. What if you want to do anything with these just created agents?

What might be really useful if it returns a convenient AgentSet with the just created Agents. Then, you can immediately let them do something, track them separate as a group, etc.

@quaquel
Copy link
Member Author

quaquel commented Dec 3, 2024

What might be really useful if it returns a convenient AgentSet with the just created Agents.

I have been going back and forth on this. It's easy to add and might be useful in some cases, but I'm not sure it would be used a lot. Happy to add it in if you think it is useful.

@EwoutH
Copy link
Member

EwoutH commented Dec 3, 2024

Can would personally like it. I can also add it tomorrow.

@quaquel
Copy link
Member Author

quaquel commented Dec 3, 2024

just added it

Copy link
Member

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Please update the type hints and docstring, after that go ahead and merge.

@quaquel quaquel merged commit adad7a2 into projectmesa:main Dec 4, 2024
11 checks passed
@EwoutH EwoutH removed the discuss label Dec 4, 2024
@EwoutH
Copy link
Member

EwoutH commented Dec 4, 2024

I updated the PR description based on the final implementation, could you check it?

@quaquel quaquel changed the title Add create_agent factory method to Agent Add create_agents factory method to Agent Dec 4, 2024
@quaquel quaquel deleted the create_agents branch December 10, 2024 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow creating multiple agents directly
4 participants