-
Notifications
You must be signed in to change notification settings - Fork 923
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
Encapsulate cell movement in properties #2333
Conversation
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
I think I like this, it makes it much more explicit what's happening. It's like modifying One thing I'm curious about if I will review in more detail tomorrow! |
Ha! Given enough time I think @quaquel will turn every mesa behaviour into a descriptor 😂 No, I really like this and this somehow put me over the edge to finally try to fully grok descriptors. Let me write in more detail when I have more time, but I think using just a property with a setter might be better |
A space does not need to know anything about Agents. A space has cells. A cell can have agents. Agents and cells need to be aware of each other. A space is primarily a container for cells, nothing more. Multiple spaces should not be a problem. You just assign another cell to the attribute, and you're done.
The shortest summary is "reusable properties".
I think you need a getter because the agent needs to know its cell for e.g., a random walk, but curious to see what more you have to say in detail. Given the fact that both @Corvince and @EwoutH like this idea, I'll try to update the tests asap to make sure this is fully covered again. |
(I just looked at it from my phone, still need to do a thorough review) |
for more information, see https://pre-commit.ci
My main objection would be that it might make some things easier if we have a clear "cell" definition. That is with a reusable descriptor we have the advantage of allowing several "cell" descriptor from several spaces. In practice I think this isn't very common. In the case of a "normal" property we could have the following: class HasCell:
_cell: Cell | None = None
@property
def cell(self):
return self._cell
@cell.setter
def cell(self, cell: Cell):
... # Your logic from CellDescriptor.__set__ And then we can have separate movement mixin class CellMovement:
def move_to(self, cell: Cell):
self.cell = cell
... # More movement functions And finally the CellAgent: class CellAgent(Agent, HasCell, CellMovement):
.... In this case its unambigious and easy. We know we have to assign to "cell". If the cell descriptor can be assigned to any name, how would we know the right name? Further advantages are that one could theoretically overwrite the |
Not sure I fully understand your point, but a descriptor has the Also, the I like your name |
Sorry if I wasn't clear. The movement mixin was just an example (although I would like to have the clearer move_to API, even if redundant. But this is irrelevant for my point here). The point is that for any mixin or rather anything wanting to operate on the cell property it needs to know the name. Of course the user knows the name, but our framework doesn't. So in the example move_to method the line |
Ok, that makes sense. If we want to add any additional functionality like the MoveTo example, then yes we need to lock down the name of the attribute to which the current cell is assigned. |
A quick follow-up to @Corvince point about the naming of the attribute. There is, in fact, already a case like this. It is up to the user to actively remove an agent from the cell before calling This leaves the other idea: instead of having a Of course this still leaves room for more sophisticated movement methods that for example use a heading or something. |
One thing I would find interesting is how this would work with multiple grids. For example, I currently have a model in which Agents move both in fine grained areas (postal code “cells”) and which are part of larger scale municipalities. So here you have like a nested spaces with two resolutions. Another case would be to completely orthogonal spaces, for example a space representing some physical representation and another representing social, economic or other relations (maybe a cell space isn’t suitable for that). CC @wang-boyu (I’m especially curious how you see this (and the whole cell space) integrating with Mesa-Geo in the long term) |
|
Just making sure we don’t box ourselves in / prohibit a use case unintentionally. |
Yes, I see this as a perfect base case for the ideas discussed in #2292. After this is merged I would update #2292 to incorporate the named coordinates and create a "Movement" mixin class. Then CellAgent can become just a class inherting from Agent, HasCell and Movement without any MRO related headaches (because of those, only Agent has an init method). I think this will come out nicely, but once implemented we can see how it plays out by adapting some examples. |
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
Performance benchmarks:
|
Performance benchmarks:
|
This comment was marked as duplicate.
This comment was marked as duplicate.
There are 6 ruff errors otherwise this looks good to go from my side (but a lot of things are based on my ideas so I would like another reviewer to approve) |
Disclaimer: I haven't read #2292. I'm not fully sure why having a While @quaquel 's stance on accommodating LLM can be found at #2237 (comment), I find that making it less likely for LLM to fall into a gotcha / to help it to reason better actually doubles as to help human reasoners as well. Why can't we have both |
The idea is that |
@@ -31,7 +42,7 @@ def step(self): | |||
|
|||
# If unhappy, move: | |||
if similar < self.homophily: | |||
self.move_to(self.model.grid.select_random_empty_cell()) | |||
self.cell = self.model.grid.select_random_empty_cell() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have both, then why is this not self.move_to
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please read the discussion both here and in #2292.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't sound inclusive to me. This was a genuine question, yet I was told to comb through the wall of text to get my answer. The 1 approval from a third position to merge is wearing me down, when a feedback from someone else not biased with the design choice was asked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sorry for the short answer and the merge, but move_to is added back in via #2292.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, then you have my post-merge approval.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this. This looks really robust, and it looks like it's more robust than previous movement methods.
I left a few thoughts when going through the diff.
def step(self): | ||
"""One step of the agent.""" | ||
self.random_move() | ||
self.cell = self.cell.neighborhood.select_random_cell() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to have this both an one-liner and in a single location!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I like the shortness of it now. Where in mesa 2.1 or so, you needed a dedicated random walker class (see here to now having it as a oneliner.
super().__init__(model) | ||
self._fully_grown = fully_grown | ||
self._fully_grown = True if countdown == 0 else False # Noqa: SIM210 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An comment explaining this might be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I might better have left that change out. Basically, there was a fixme statement in the docstring to go from 2 arguments (countdown value and whether or not being fully grown) to only 1. If countdown is 0, you are fully grown, otherwise you are not.
assert agent not in cell2.agents | ||
|
||
agent.remove() | ||
assert agent not in model._all_agents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completely unrelated, doesn't agent not in model.agents
work? If so, we might want to implementent some construct so that it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case I explicitly am testing for the hard reference because if that is gone, I know that the object can be garbage collected.
The syntax in general is indeed valid.
cell.add_agent(self) | ||
|
||
|
||
class CellAgent(Agent, HasCell): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that this Agent is moving, this name might get confusing with an Agent that sits in a fixed place, and the whole grid is covered with such (like the patches
in NetLogo). For now fine, but maybe something we should keep in mind as the cell space develops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's pick up that point in #2292
This PR encapsulates agent movement between cells using a
HasCell
mixin with property descriptors. It replaces the explicitmove_to
method with cell attribute assignment.Motive
The goal is to simplify the API for agent movement in cell-based spaces and lay the groundwork for more flexible agent behaviors. This approach allows movement to be handled through simple attribute assignment rather than explicit method calls.
Implementation
HasCell
mixin class that implements cell assignment logic using property descriptorsCellAgent
to use theHasCell
mixinmove_to
method fromCellAgent
CellAgent.remove
to handle cell removal when an agent is removedUsage Examples
Previous approach:
New approach:
In the Schelling model:
Python Constructs and Patterns Used
HasCell
mixin uses the@property
decorator and a setter to create a managed attribute for the cell. This allows for custom logic to be executed when the cell is accessed or modified.HasCell
class is designed as a mixin, allowing its functionality to be easily combined with other classes through multiple inheritance.Additional Notes
move_to
method may be reintroduced with enhanced functionality in a future PR