-
Notifications
You must be signed in to change notification settings - Fork 929
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
refactor: Remove dependence on model.schedule, add clock to Model #1942
Conversation
The use of model.agents looks good to me. One quick question: you also changed how time is handled. It was retrieved from the schedule, and now you track it internally. This is because you don't want to depend on schedule, which makes sense. However, from a conceptual point of view, is this the nicest, longer-term way of handling time? |
Thank you for raising the issue. In this PR, I track it within the def advance_time(self):
self.time += 1 In the user's model step def step(self):
self.agents.shuffle().do("step")
self.datacollector.collect(self)
self.advance_time() This is how it is done in abcEconomics. |
I think tying time to |
At least by default. I think some concept of actual time would also be very useful. See #1912 (reply in thread) |
I had thought about
There would be the |
Regarding with the term, |
To be pedantic, |
In my view, the moment you allow for this, you should go all the way and have a discrete event-style event list at the heart of everything. If you want traditional ABM behavior, you just schedule evenly-spaced events. Each event would be then a call to building on @rht on No idea what a resulting clear API could look like. |
This could integrate the advance time part into the step, right? |
There is a way to track the number of calls to from collections import defaultdict
from functools import wraps
def count_calls(func):
name = func.__name__
@wraps(func)
def wrapper(self, *args, **kwargs):
# creates the instance counter if necessary
counter = getattr(self, "time", None)
if counter is None:
counter = 0
setattr(self,"time", counter + 1)
return func(self, *args, **kwargs)
wrapper._is_count_call_wrapper = True
return wrapper
class CountStep(type):
def __new__(cls, name, bases, attrs):
if name != Model.__name__:
try:
step_method = attrs["step"]
except KeyError:
pass
else:
attrs["step"] = count_calls(step_method)
return super(CountStep, cls).__new__(cls, name, bases, attrs)
class Model(metaclass=CountStep):
pass
class MyModel(Model):
def step(self):
print(self.time) If we run this model = MyModel()
for _ in range(10):
model.step() we nicely get 1 ... 10. |
Not sure how to read something like this. What would it mean if you say |
(I'm probably digressing too much here on DES...) Considering #1912 (reply in thread)
I'm drawing example from the Eurace@Unibi model, one of the most elaborate macroeconomic model that has existed. Taking an excerpt from the paper on Eurace@Unibi
If we were to be able to model a reduced version of Eurace@Unibi in Mesa, for pedagogical purpose. Extending Mesa to describe events would be necessary. |
Colleagues of mine have been doing pandemic modeling with models involving between 150 thousand and 25 million agents. The only way to make this computationally feasible was by switching from calender-based to event-based activation. So at some point figuring out how to support this in MESA would be great. In the meantime, however, there is still the issue of tracking the time of the simulation. Would it make sense to do it along the lines of the metaclass example I have given above? |
While it is more convenient to the user, I find the implementation to be too complex for the reader of Mesa code. The library code needs to be simple enough without requiring one to spend an effort to decipher the implementation to what amount to tracking the time automatically. |
I see it as the step period that is not necessarily an integer. All the events that happen within the step, are sorted based on their activation times, and are executed in order. That event1 fires at time 0.06674, event2 at 0.1054, event3 at 2.9979, which information is used to decide their execution order. Edit: |
What do you think of this approach instead? class Clock:
def __init__(self):
self.steps = 0
self.time = 0
def step(deltat=1): # deltat is more mnemonic than timestep
self.steps += 1
self.time += deltat
class MyModel(mesa.Model):
def __init__(self, ...):
self.clock = Clock()
def step(self):
self.agents.shuffle().do("step")
self.datacollector.collect(self)
# This is sufficiently mnemonic, as a replacement of self.schedule.step()
self.clock.step() And so, we reuse the existing ABM terms without having to invent new terms. It's FSM all the way down. |
That's exactly what I had in mind. Only instead of creating a new class, I would just integrate it in the Model class. Edit: and don't have to call the clock explicitly, that should |
Why do I use a library for something? To avoid having to write boilerplate code. All models need to track time, so if I use a library, the library should handle this for me. With this suggestion, I must add Clock myself and remember to advance it. It also adds two lines of code to any model I make. Also, speaking from experience teaching MESA for the last 3 years at the MSc level, this is something that will easily trip up new users. So, no, I don't like this suggestion.
So, here I have a different view. For me, the cleanliness of the API and the use of the library come first. So be it if a clean and easy-to-use API requires some more obscure Python machinery. Because, who is going to read the source code? Only users who are invested in the library and already have some programming background. So, as long as the code is well documented and explained at a high level (i.e., what does it do) and with some detail on how this is achieved, I prefer such a solution over forcing my user always to add boilerplate code. |
I appreciate the criticism and see the point regarding with the annoyance of having to manually carry along the At the very least, Regarding with PEP 20:
I interpret it as overall simplicity, instead of simple API but obscure implementation. |
That said, I still think there might be a solution where a |
This seems the simplest approach for now. |
I am fine with a simple solution, although I would advocate calling super over adding another method to the model class. It is still only one additional line, but at least for me and in my teaching, I always advocated calling super anyway. Some other thoughts
|
I think the emphasis on the implementation simplicity of the code should be orthogonal to the questioning of the maintainers' time commitment. There would definitely be a situation where both the code is simple and readable, together with active maintenance. (On my end, #1933 on #574 is definitely on my radar; I just need some time to digest them.) To localize the discussion on the system clock: that said, #1942 (comment) handles the |
Let’s separate some issues here:
1 and 2 are implementation discussions. I think everyone agrees they should be done, so let’s (continue) discussing how. Maybe in separate issues or PRs though, and I think it might be useful to do 1 first and then 2. 3 and 4 are long term and conceptual. In any case, you probably want a central clock in the model, right? So it doesn’t block 1 or 2, and we can continue discussing 3 and 4 in their respective discussions. 5 important, but can quickly get very broad. If it’s not about this specific implementation anymore I would say spin off into a new discussion. 6 also important, but can get personal, and thus maybe face to face stuff (or very well-thought out written out). (might still be missing some stuff) In general, I would suggest issues and PRs to be atomic, and only focus on one coherent issue. Of course it can touch other stuff, and therefore spin-off new discussions (which is great in general, also about meta things like user/contributor friendliness), but let’s try to spin-off those discussions in separate threads, or discuss them in a face to face dev meeting. That helps to keep the PRs on topic. |
Thanks a lot for summarizing the sometimes confusing discussion @EwoutH ! However I disagree on
From the discussion in #1912 I think it is still unclear if we want to actually get rid of schedulers or not. I think we need to continue to discuss this first before we continue this path here. Because if we want to keep schedulers I think they are the right place to keep track of time and so there is no need to decouple the logic. I mean the whole discussion can be viewed as an advantage of schedulers - it is clear that they need to track time. For example, if we remove schedulers, but add a Clock instance we don't really gain anything. Same for tracking time inside the model instance - we just further clutter the model namespace, but keep a tied coupling. This isn't necessarily bad, but we should really first discuss about the future of schedulers before arguing about implementation details. And the best place for this discussion is #1912. |
The gain: |
Right, I now understand the complication:
While it isn’t best practice, there might be models out there that rely on this behavior. |
(sorry misclicked) Let me think a bit about possible solutions. Maybe we can get away with throwing a clear warning in the right place. Edit: I feel in both the old and new implementation we make assumptions about for which agents data will be collected. In the new one we definitely have to make that explicit. |
Another option could be adding some switch like |
I am removing the commit "time: Remove agent.remove in remove" so that this PR is ready to merge as is. |
Sorry but for me this doesn't solve the issue:
I'm not happy about altering If you want, I can try to come up with an implementation in the weekend. |
I think the simplest solution would be to check if model.schedule exists and if it does collect data from its agents. Otherwise use model.agents. This would be backwards compatible, but allow removing the scheduler. And agree that in a future datacollector it should be made explicit which agent data is collected. /edit and of course don't remove agents from model.agents if they are only removed from the scheduler |
I agree with @Corvince proposed solution. |
Good idea, also agreed. @rht would you like to implement it? |
Done. You can check the last commit of this PR. |
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.
Good to go! Thanks a lot, we churned out a lot of conceptual things together with this PR. I will try to do a quick write up tomorrow.
I can merge later today if preferred. I recommend either squashing or cleaning up the commits.
) * refactor: Remove dependence on model.schedule * model: Implement internal clock * time: Call self.model.advance_time() in step() This ensures that the scheduler's clock and the model's clock are updated and are in sync. * Ensure advance_time call in schedulers happen only once in a model step * Turn model steps and time to be private attribute * Rename advance_time to _advance_time * Annotate model._steps * Remove _advance_time from tests This is because schedule.step already calls _advance_time under the hood. * model: Rename _time to time_ * Rename _steps to steps_ * Revert applying _advance_time in schedulers step * feat: Automatically call _advance_time right after model step() Solution drafted by and partially attributed to ChatGPT: https://chat.openai.com/share/d9b9c6c6-17d0-4eb9-9eae-484402bed756 * fix: Make sure agent removes itself in schedule.remove * fix: Do step() wrapping in scheduler instead of model * fix: JupyterViz: replace model.steps with model.steps_ * Rename steps_ -> _steps, time_ -> _time * agent_records: Use model.agents only when model has no scheduler --------- Co-authored-by: Ewout ter Hoeven <E.M.terHoeven@student.tudelft.nl>
) * refactor: Remove dependence on model.schedule * model: Implement internal clock * time: Call self.model.advance_time() in step() This ensures that the scheduler's clock and the model's clock are updated and are in sync. * Ensure advance_time call in schedulers happen only once in a model step * Turn model steps and time to be private attribute * Rename advance_time to _advance_time * Annotate model._steps * Remove _advance_time from tests This is because schedule.step already calls _advance_time under the hood. * model: Rename _time to time_ * Rename _steps to steps_ * Revert applying _advance_time in schedulers step * feat: Automatically call _advance_time right after model step() Solution drafted by and partially attributed to ChatGPT: https://chat.openai.com/share/d9b9c6c6-17d0-4eb9-9eae-484402bed756 * fix: Make sure agent removes itself in schedule.remove * fix: Do step() wrapping in scheduler instead of model * fix: JupyterViz: replace model.steps with model.steps_ * Rename steps_ -> _steps, time_ -> _time * agent_records: Use model.agents only when model has no scheduler --------- Co-authored-by: Ewout ter Hoeven <E.M.terHoeven@student.tudelft.nl>
This is still the case in the current codebase right? If so, it's a bit weird that users should call an private function to increase the time. |
The current AgentSet API allows users to define the structure and order of agent actions without initializing a scheduler object. However, a scheduler object is currently necessary until this pull request addresses the issue by incorporating the steps and time attributes directly into the model (which were previously only tracked in the scheduler).
A complication arises for users who still use the scheduler object, as they now need to manually specify
model.advance_time()
within the model'sstep()
function. To resolve this, the pull request proposes a solution with assistance from ChatGPT, involving the injection ofmodel.advance_time()
into the scheduler'sstep()
function. This modification aims to streamline the process for users and enhance the overall functionality of the AgentSet API.