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

PoC: Multiple-agent types scheduling and datacollection #1142

Closed
wants to merge 2 commits into from

Conversation

EwoutH
Copy link
Member

@EwoutH EwoutH commented Jan 22, 2022

Proof of concept. This code is not meant for production.

This commit adds support for multiple agent-types, including scheduling and data collection.

The scheduler is based on the RandomActivationByBreed scheduler from the wolf_sheep example. It supports RandomActivation by agent type, in the order that they are initialized. See examples/wolf_sheep/wolf_sheep/schedule.py.

The DataCollection module is modified to support multiple agent types. It uses a dictionary structure for all agent reporters and records, with the agent class as the key.

The ChartModule has been extended to enable data collection both Agent and Model variables, and agents of different types. Contains currently a breaking change by requiring "Type" key in the ChartModule input dictionary, which can either be "Model" or "Agent". If "Agent", a key "Agent_type" is also required, which should be the exact name of the class as a string (__class__.__name__).

A Mesa Model class with a datacollector can now look like this:

class ExampleModel(Model):
    def __init__(self, n_dogs, n_cats):
        super().__init__()
        self.schedule = RandomActivationByType(self)

        self.population = n_dogs + n_cats

        for _ in range(self.n_cats):
            cat = Cat(self.next_id(), self)
            self.schedule.add(cat)

        for _ in range(self.n_dogs):
            dog = Dog(self.next_id(), self, population_dist)
            self.schedule.add(dog)

        model_reporters = {"Population": "population",
        agent_reporters = {Cat: {"Weight": "weight", "Size": "size"},
                           Dog: {"Weight": "weight"}}

        self.datacollector = DataCollector(
            model_reporters=model_reporters,
            agent_reporters=agent_reporters)

Charts for a Mesa Server can now look like this:

chart_pop = ChartModule(
    [{"Type": "Model", "Label": "Population", "Color": "Black"}], data_collector_name="datacollector"
)
chart_weight = ChartModule(
    [{"Type": "Agent", "Agent_type": "Cat", "Label": "Weight", "Color": "Blue"},
     {"Type": "Agent", "Agent_type": "Dog", "Label": "Weight", "Color": "Red"}], data_collector_name="datacollector"
)
chart_size = ChartModule(
    [{"Type": "Agent", "Agent_type": "Cat", "Label": "Size", "Color": "Blue"}],
    data_collector_name="datacollector"
)
server = ModularServer(
    ExampleModel, [chart_pop, chart_weight, chart_size], "Cats and Dogs", {"n_dogs": 10, "n_cats": 10}
)

To-do

  • Scheduler: Allow custom order of agent types in staging (and not just the order in which their created).
  • DataCollector: Ensure it still works with only a single/default agent type
  • ChartVisualisation:
    • 1) Try to reduce breaking changes
    • 2) ensure it still works with a single/default agent-type
    • 3) Allow different kinds of statistical processing then mean (median, sum, min, max, standard deviation, etc.) (maybe implement this in DataCollector)
  • Cleanup
  • Testing
  • Documentation (including examples)

Proof of concept. This code is not meant for production.

This commit adds support for multiple agent-types, including scheduling and data collection.

The scheduler is based on the RandomActivationByBreed scheduler from the wolf_sheep example. It supports RandomActivation by type, in the order that they are initialized. See https://github.com/projectmesa/mesa/blob/246c69d592a82e89c75d555c79736c3f619d434a/examples/wolf_sheep/wolf_sheep/schedule.py

The DataCollection module is modified to support multiple agent types. It uses a dictionary structure for all agent reporters and records, with the agent class as the key.

The ChartModule has been extended to enable data collection both Agent and Model variables, and agents of different types. Contains currently a breaking change by requiring "Type" key in the ChartModule input dictionary, which can either be "Model" or "Agent". If "Agent", a key "Agent_type" is also required.

To-do:
 - Scheduler: Allow custom order of agent types in staging (and not just the order in which their created).
 - DataCollector: Ensure it still works with only a single/default agent type
 - ChartVisualisation: 1) Try to reduce breaking changes, 2) ensure it still works with a single/default agent-type, 3) Allow different kinds of statistical processing then mean (median, sum, min, max, standard deviation, etc.) (maybe implement this in DataCollector)

 - Cleanup
 - Testing
 - Documentation (including examples)
@rht
Copy link
Contributor

rht commented Jan 22, 2022

The Agents.jl implementation is much simpler and modular, because they can just use multiple dispatch. :/

@EwoutH
Copy link
Member Author

EwoutH commented Jan 23, 2022

I read a bit up about multiple dispatch, and it indeed seems very useful in this case to be able to handle both the model and different agent types. Julia is a very interesting language!

A few Python packages implement this. multipledispatch seems unmaintained, while plum is newer and actively maintained but less used.

Another option that could simplify things is using Structural Pattern Matching. This does require Python 3.10 however, and dropping support for all previous versions might be a bit too early.

Moves the metric aggegration code to the datacollector, in a new function get_agent_metric(). It returns a single value from the agent variables, using either a function in the statistics module or one of the built-in functions "min", "max", "sum" or "len".

In the ChartModule an optional "Metric" can now be called, which defaults to "mean".
@rht
Copy link
Contributor

rht commented Jan 23, 2022

Actually, upon rereading Agents.jl's implementation, they use a flat list of agents and reconstruct the set of agents grouped by type on-the-fly.

And so the main design decision is whether we should use a flat dict like before (and generates the agents_by_type on the fly like Agents.jl does), or a dict of dict.
@EwoutH is it correct that all the scheduler implementations need to have agents_by_type in this PR?

@EwoutH
Copy link
Member Author

EwoutH commented Jan 23, 2022

I moved the metric aggegration code to the datacollector, in a new function get_agent_metric(). It returns a single value from the agent variables, using either a function in the statistics module or one of the built-in functions "min", "max", "sum" or "len".

Example:

ExampleModel.datacollector.get_agent_metric("Weight", Cat, "min")

In the ChartModule an optional "Metric" can now be called, which defaults to "mean".

Example:

chart_size = ChartModule(
    [{"Type": "Agent", "Agent_type": "Cat", "Label": "Size", "Metric": "max", "Color": "Blue"}],
    data_collector_name="datacollector"
)

Again, both architecture and implementation notes are much appreciated!

(also CC @tpike3)

@EwoutH
Copy link
Member Author

EwoutH commented Jan 23, 2022

And so the main design decision is whether we should use a flat dict like before (and generates the agents_by_type on the fly like Agents.jl does), or a dict of dict.

Yes, this is a big design decision indeed. Love to hear your and everyone elses opinion about it!

One idea I had, it to detect in the datacollector if the first layer of keys of the agent_reporters is a Class, and if so, use the agents_by_type construct in a nested dict. If not, just use the regular implementation.

Another idea is to let the agents_by_type just default to the only agent class in the model, if there is only one. I think this may be the cleanest and simplest solution.

I think both options would be able to maintain backwards compatibility.

@EwoutH is it correct that all the scheduler implementations need to have agents_by_type in this PR?

In this current draft, yes. One of the above solutions should be able to mitigate this as well.

@tpike3
Copy link
Member

tpike3 commented Jan 23, 2022

@EwoutH This is awesome! Glad to see so much progress, particularly as I have a very bad habit of ignoring the visualization piece.

To make sure I am understanding correctly, right now the current approach has a flat dict self._agents and a dict of dict self.agents_by_type. The question is do we want to calculate the agents_by_type on the fly? Is that correct or am I missing something.

My initial response would be whatever is faster. (I will try and run some comparisons but due to my current job probably can't get to it until next weekend...sorry I am the slow one in this group)

My more thoughtful response is on the fly might allow for hypertypes. For the seminal wolf-sheep model this isn't a consideration but thinking of a social model where agents may change their type based on their situation (e.g. civil violence, politics, alliance) this could add some more capability where you can run the standard static type (e.g. wolf-sheep) but you can also allow for type adaption.

One idea I had, it to detect in the datacollector if the first layer of keys of the agent_reporters is a Class, and if so, use the agents_by_type construct in a nested dict. If not, just use the regular implementation.

This thought would mess up the class approach though because the type could also be an attribute and not just a class.

So I am not sure I helped or muddled the discussion, but I think grouping agents on the fly may offer some additional capabilities for more diverse model approaches.

@rht
Copy link
Contributor

rht commented Jan 23, 2022

Things to consider:

  • Generating the by-type grouping on the fly is definitely slower when the agent types are stable, but this should rarely be the limiting factor of a simulation. If someone needs the pre-grouped dict, they can implement this scheduler in their code. And if there is a lot of demand on this pre-grouped scheduler, then it can be added in the library.
  • If we are to detect whether the model uses random activation by type or not, in order to do the code selection, it'd be more semantic if the DataCollector checks the type of the scheduler object itself, i.e. check for isinstance(self.model.scheduler, RandomActivationByType). But this means that there is an order dependence: the scheduler object must be initiialized before the data collection object.

@EwoutH
Copy link
Member Author

EwoutH commented Jan 23, 2022

Thank you both for you responses according the architecture!

In the next week I will focus on finishing up my courses for this quarter (I'm still a student), and the week after that I'm a week on vacation and offline. So somewhere after February 7th, I will continue this effort.

If anyone in the mean time wants to continue or try some things, feel free to checkout my branch (https://github.com/EwoutH/mesa/tree/multiple-agent-types) or install it (pip install -e git+https://github.com/EwoutH/mesa@multiple-agent-types#egg=mesa).

A particularly piece of this PR wasn't about multi-agent support, but just about reading and plotting agent variables. Thus, I've created a separate PR, that implements this:

If we get that in first, we can built multiple-agent types on top of it. Maybe #1145 can even make the 0.9.0 release!

Also Tom, don't even sweat it for a second, your pretty involved as a maintainer. Thank you!

@tpike3
Copy link
Member

tpike3 commented Feb 3, 2022

@EwoutH sorry it didn't back the 0.9.0 release but these are pretty big challenges

@EwoutH
Copy link
Member Author

EwoutH commented Feb 3, 2022

No worries, this isn't even close to ready yet. Purely a proof of concept.

I hope to spend some more time on it in the coming weeks.

@tpike3
Copy link
Member

tpike3 commented Mar 20, 2022

@EwoutH Based on #1162, we can close this correct?

@EwoutH
Copy link
Member Author

EwoutH commented Mar 20, 2022

#1162 is only the scheduler, there is some DataCollector and Visualisation stuff in here to.

I did like to leave it open for a while, until I can properly implement it or someone else picks it up.

@tpike3
Copy link
Member

tpike3 commented May 15, 2022

Superseded by #1287 and #1162

@tpike3 tpike3 closed this May 15, 2022
@rht rht removed this from the v1.1.0 Safford Release milestone Oct 5, 2022
EwoutH added a commit to EwoutH/mesa that referenced this pull request Dec 17, 2023
Tracks agents in the model with a defaultdict.

This PR adds a new `agents` dictionary to the Mesa `Model` class, enabling native support for handling multiple agent types within models. This way all modules can know which agents and agents type are in the model at any given time, by calling `model.agents`.

NetLogo has had agent types, called [`breeds`](https://ccl.northwestern.edu/netlogo/docs/dict/breed.html), built-in from the start. It works perfectly in all NetLogo components, because it's a first class citizen and all components need to be designed to consider different breeds.

In Mesa, agent types are an afterthought at best. Almost nothing is currently designed with multiple agent types in mind. That has caused several issues and limitations over the years, including:

- projectmesa#348
- projectmesa#1142
- projectmesa#1162

Especially in scheduling, space and datacollection, lack of a native, consistent construct for agent types severely limits the possibilities. With the discussion about patches and "empty" this discussion done again. You might want empty to refer to all agents or only a subset of types or single type. That's currently cumbersome to implement.

Basically, by always having dictionary available of which agents of which types are in the model, you can always trust on a consistent construct to iterate over agents and agent types.

- The `Model` class now uses a `defaultdict` to store agents, ensuring a set is automatically created for each new agent type.
- The `Agent` class has been updated to leverage this feature, simplifying the registration process when an agent is created.
- The `remove` method in the `Agent` class now uses `discard`, providing a safer way to remove agents from the model.
tpike3 pushed a commit that referenced this pull request Dec 18, 2023
Tracks agents in the model with a defaultdict.

This PR adds a new `agents` dictionary to the Mesa `Model` class, enabling native support for handling multiple agent types within models. This way all modules can know which agents and agents type are in the model at any given time, by calling `model.agents`.

NetLogo has had agent types, called [`breeds`](https://ccl.northwestern.edu/netlogo/docs/dict/breed.html), built-in from the start. It works perfectly in all NetLogo components, because it's a first class citizen and all components need to be designed to consider different breeds.

In Mesa, agent types are an afterthought at best. Almost nothing is currently designed with multiple agent types in mind. That has caused several issues and limitations over the years, including:

- #348
- #1142
- #1162

Especially in scheduling, space and datacollection, lack of a native, consistent construct for agent types severely limits the possibilities. With the discussion about patches and "empty" this discussion done again. You might want empty to refer to all agents or only a subset of types or single type. That's currently cumbersome to implement.

Basically, by always having dictionary available of which agents of which types are in the model, you can always trust on a consistent construct to iterate over agents and agent types.

- The `Model` class now uses a `defaultdict` to store agents, ensuring a set is automatically created for each new agent type.
- The `Agent` class has been updated to leverage this feature, simplifying the registration process when an agent is created.
- The `remove` method in the `Agent` class now uses `discard`, providing a safer way to remove agents from the model.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants