-
Notifications
You must be signed in to change notification settings - Fork 883
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 support for Observables to MESA #2291
base: main
Are you sure you want to change the base?
Conversation
Looks really interesting! I will review it in detail tomorrow, and try to play with it. |
Could you update or rebase this branch on |
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
A quick update: I started adding tests. The Observable and HasObservable are now fully tested. Still need to do the test for computed/computable and for the SignalList. |
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
Performance benchmarks:
|
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 didn't review every line of code, but I would like to see this move forward. The user API is clean enough, considering what's possible in Python, and it's all in the experimental space.
Thanks for working on this, curious where it will go!
|
||
__all__ = [ | ||
"Observable", | ||
"ObservableList", |
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.
Would an ObservableDict
(also) be useful, to be able to track which Agent the value originated from?
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 ObservableDict
would indeed also be useful. Not sure what you mean with
to be able to track which Agent the value originated from
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 think I see what @EwoutH means here where it would be something like {agent.unique_id: <observables>}
for more information, see https://pre-commit.ci
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.
Just curious why this is here? Did we maybe want to add this to the docs, so users have an example to follow? Maybe subset of the getting started and experimental how 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.
This will be removed before merging, was just there during development. Good catch!
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 is awesome! I think as the develops this and the datacollection update will inextricably intertwined.
Let us know what you want to complete before we merge.
I want to do a bit of further code cleaning (like the tests.py file) and improve code coverage a bit further. Beyond that, any feedback or ideas are welcome here or in #2281. |
see #2281
This PR adds various new classes to MESA. The key public-facing ones are, at the moment,
Observable,
ObservableList,
andHasObservables
.Observable
is a descriptor that is used to declare a given attribute to be observable. Being observable means changing the attribute using the assignment operator=
willl result in the sending of a signal. Callbacks can subscribe to this signal.ObservableList
is a proof of principle on how the idea of an observable can be expanded to collections. Future extensions could include, e.g.,ObservableDict
.HasObservables
is a mixin that can be used with e.g.,Model
orAgent
. It is required if you want to useObservable
orObservableList
.HasObservable
contains the key logic for subscribing to signals and the emitting of signals.Observable
emits achange
signal when its attribute value is changed via the assignment operator (i.e.,=
).ObservableList
at the moment defines five signal types:"replace", "remove", "insert", "append", "change"
."change"
is emmited when the entire list is changed via the assignment operator (i.e.,=
). The other signals are tied to the respective list operations. Currently, signals are following the approach used in traitlets. The main reason for making the signals identical to traitlets is that it should make integration with solara easier, which already supports traitlets.The emitted signals are instances of an
AttributeDict
. This is in line with Traitlets. It is just a dict with attribute-style access to the fields. By default, a signal has four fields: "owner", "type", "old", and "new". It is possible to add additiona fields if the user wants. For example, theObservableList
signals (accept"change"
) all also include"index"
.I have also added a WIP
Computed
andComputable
approach. In javascript-style signals, but, e.g., also in solara, a Computed is a callable that is dependent on one or more signals. When it receives a signal that any of its dependencies has changed, it will mark itself as being outdated. It will only update its value if it is called to do so. This can be used to set up a hybrid push/pull structure where Obervables push signals, but a Computed is only recalculated on a pull if it knows its underlying data has changed. A Computed is also Observable, so it will emit a signal if it knows it is outdated. This allows the chaining of observables and computed's.API
Below, there is a quick sketch of the resulting API. We define Obervable attributes at the class level. We can use the attribute normally in the rest of the code.
Implementation Details
After a long discussion in #2281, I decided to implement everything from scratch. Existing libraries like traitlets or psygnal add too much overhead because they are not tailored to the specific use case of ABMs with potentially many updates to an attribute.
future extensions
This PR puts in place the basic machinery, but there are ample options for further improvements:
model.agents
should emitagent_added
andagent_removed
signals. At the moment, there is no machinery in place to make it easy to add this functionality. I don't think it is overly difficult, but I want to take a closer look soon.Computed
is heavily inspired by signals and Solara. It involves the autodiscovery of dependencies. The params library, instead, forces users to pass the list of dependencies explicitly. I am not sure which approach is better.Computed
. It requires the user to do 2 things: declare an attribute as being computable at the class level and assign a computed instance to this attribute during object initialization. If I fix the first bullet, it might be possible to improve this API and eliminate theComputable
declaration.Observable
andObservableList
both have a list of strings that enumerate the signals that can be sent. However, there is no explicit declaration anywhere of the fields that are part of any signal. A signal is an instance of anAttributeDict
, following traitlets, but it is completely up to the user to decide what fields will be in it. This means that the user is dependend on the docstring, or some test code to discover what is in a signal, rather than that it can be declared and checked explicitly via e.g., a type checker.notify
method, which is responsible for emitting the signals requires the name of the observable (name
), the old value (old
), the new value (new
), and the signal type (type
). Further elements can be added as desired. For example, most signals fromObservableList
includeindex
. However, for e.g., anappend
signal, havingold
in there makes little sense.