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

Refactoring mesa.Agent, mesa.AgentSet, mesa.Model -> AgentSetDF, AgentsDF, ModelDF #8

Merged
merged 16 commits into from
Jun 25, 2024

Conversation

adamamer20
Copy link
Collaborator

@adamamer20 adamamer20 commented Jan 10, 2024

  1. Agents of the model are included in a unique container class AgentContainer, in its agents attribute. Agents of the same type are stored in a DataFrame in the AgentSet class. Multiple types (all agents of the model) are stored in a list of AgentSets. This avoids having many missing values (which would occupy memory) if Agents of different type were stored in the same Dataframe.
  2. Went with encapsulation, avoiding extensions and subclassing because they didn't always work well with storing additional attributes and it wasn't easy to extend the created classes by subclassing (as it's often done with base mesa.Agent).
  3. All operations are mutable: better aligment with base mesa API. Possibility of functional programming with the use of fast copy methods.
  4. All methods and dunder methods that can act on a subset of agents support a custom mask = "active" that operates only on active agents. The private attribute is a mask (pl.Expr, pl.Series, pd.Series). The public property is self.active_agents. If you wanted to remove the agents you could simply use the "discard" or "remove" methods.
  5. Dunder methods to facilitate interaction introduced on top. Maybe can be added also to base mesa.
  6. pandas and polars are currently implemented in two different classes. Creating a single AgentSet class and use backend as environment variable or a method .to like Pytorch is not currently possible because type hinting is only static (see discussion Towards a unique AgentSet class #12). However having an abstract AgentSet class makes it easy to extend to other backends with relative ease.

@adamamer20 adamamer20 added the enhancement Improvements to existing features or performance. label Jan 10, 2024
@adamamer20 adamamer20 added this to the 1.0.0 Beta Release milestone Jan 10, 2024
@adamamer20 adamamer20 linked an issue Jan 10, 2024 that may be closed by this pull request
@adamamer20 adamamer20 self-assigned this Jan 10, 2024
1) Agents of the same type are stored in a pd.DataFrame of the AgentSetDF class. This avoids having many missing values (which would occupy memory) if Agents or different type were stored in the same Dataframe. All agents of the model are stored in the AgentsDF class as a list of the AgentSetDF, which virtually supports the same operations as AgentSet.
2) Went with encapsulation, avoiding extensions and subclassing because they didn't always work well with storing additional attributes and it wasn't easy to extend the created classes by subclassing (as it's often done with base mesa.Agent).
3) All operations are inplace (no immutability): if we wanted to keep immutability, there couldn't be method chaining with encapsulation (since methods would have to return a pd.Dataframe) If we were to return an AgentSet. I think it also aligns well with base mesa API.
4) The "select" operations was changed to selecting "active_agents" (by a latent mask) which are the ones effectively used for the "do" operations (since usually you don't want to remove the other agents from the df but simply use a method on a part of agents of the DF). If you wanted to remove the agents you could simply use the "discard" or "remove" methods.
@adamamer20 adamamer20 changed the title Refactoring mesa.Agent and mesa.AgentSet Refactoring mesa.AgentSet Feb 3, 2024
self.agents = self.agents.add(MoneyAgentsDF(N, model=self))

def step(self):
self.agents = self.agents.do("step")
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

The API is simpler.

@rht
Copy link
Contributor

rht commented Feb 3, 2024

All operations are inplace (no immutability): if we wanted to keep immutability, there couldn't be method chaining with encapsulation (since methods would have to return a pd.Dataframe) If we were to return an AgentSet. I think it also aligns well with base mesa API.

Not sure if Polars supports inplace operations. Needs to check the documentation.

@EwoutH
Copy link
Member

EwoutH commented Apr 5, 2024

I like it on a high-level! Especially the active agents concept looks interesting.

Are there any parts that significantly differ from regular mesa?

Is there anything that you are in doubt of or would like to have a more in-depth review of?

@@ -67,45 +79,48 @@ class MoneyModelDF(ModelDF):
def __init__(self, N):
super().__init__()
self.num_agents = N
self.create_agents(N, {MoneyAgentDF: 1})
self.agents = self.agents.add(MoneyAgentsDF(N, model=self))
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be an inplace operation, based on the API of AgentSet?

self.agents.add(...)
# instead of
self.agents = self.agents.add(...)

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it because Polars doesn't support inplace operations?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rht I've set up a fast immutable system using custom DataFrame copies, aligning better with the pandas/polars API. I think it reduces confusion.
I'll update the script soon.

cls.model.agents.loc[wealthy_agents, "wealth"] -= 1
cls.model.agents.loc[other_agents, "wealth"] += 1
def give_money(self):
other_agents = self.agents.sample(len(self.active_agents), replace=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the term "active agent" an internal one, given that it is the ones selected by select? If so, it'd be confusing to use it as a public API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

but i declared it as a property so it should be fine to use it in the public api right? it returns a view.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I think selected_agents is clearer, but we can do it in another PR. I think the user is more used to how it is done in pandas/Polars, in that the selected agents are stored as a variable defined by the user themselves.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can change the term active_agents to selected_agents (and also the corresponding string mask to "selected"). The active_agents property has a setter so that user can define it directly with a mask, instead of using the select method.

Copy link
Collaborator Author

@adamamer20 adamamer20 left a comment

Choose a reason for hiding this comment

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

script needs update

@adamamer20
Copy link
Collaborator Author

@rht i'm a bit behind the schedule as i'm working only during the evenings because i still have some exams left. from mid june i will be completely free and i will give my complete attention to development!

@rht
Copy link
Contributor

rht commented Jun 4, 2024

No worries, looking forward it.

@adamamer20
Copy link
Collaborator Author

adamamer20 commented Jun 24, 2024

@rht I have made the last commit. For me it's ready to merge, let me know what you think about it. I went back to inplace operations because the API would be a bit more complex and too different from base mesa. I tried implementing a unique AgentSet class but type hinting becomes fairly complicated, especially with subclassing (see #12). Maybe in the future.

@rht
Copy link
Contributor

rht commented Jun 25, 2024

Great progress so far!

@adamamer20 adamamer20 requested a review from rht June 25, 2024 08:45
@adamamer20
Copy link
Collaborator Author

Thank you!

@adamamer20 adamamer20 merged commit f317ce1 into main Jun 25, 2024
6 checks passed
@adamamer20 adamamer20 deleted the AgentSet branch June 25, 2024 08:50
@adamamer20 adamamer20 changed the title Refactoring mesa.AgentSet Refactoring mesa.Agent, mesa.AgentSet, mesa.Model -> AgentSetDF, AgentsDF, ModelDF Jul 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvements to existing features or performance.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Refactoring mesa.Agent and mesa.AgentSet
3 participants