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

new feature: AgentSet.get can retrieve one or more then one attribute #2044

Merged
merged 7 commits into from
Feb 21, 2024

Conversation

quaquel
Copy link
Contributor

@quaquel quaquel commented Feb 20, 2024

This adds a small additional feature to AgentSet. The get method now takes a string or list of strings. This allows you to retrieve multiple attributes in one go. I also added a test to cover this new feature.

Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
Schelling small 🔵 -0.4% [-0.7%, -0.1%] 🔵 +0.5% [+0.3%, +0.6%]
Schelling large 🔵 +11.0% [-18.0%, +50.3%] 🔵 -0.9% [-1.9%, -0.3%]
WolfSheep small 🔵 -1.4% [-1.7%, -0.9%] 🔵 -0.2% [-0.3%, -0.0%]
WolfSheep large 🔵 -1.4% [-2.4%, -0.6%] 🔵 -0.9% [-1.4%, -0.6%]
BoidFlockers small 🔵 -2.1% [-2.6%, -1.5%] 🔵 +0.8% [+0.1%, +1.6%]
BoidFlockers large 🔵 -0.8% [-1.4%, -0.2%] 🔵 +2.9% [+2.4%, +3.4%]

Copy link
Contributor

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

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

Thanks, looks useful! One minor questions on the output.

mesa/agent.py Outdated

Returns:
list[Any]: A list of attribute values from each agent in the set.
list[Any]: A list of attribute values for each agent in the set.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still correct? Would it be nested in some sort when a list of attrs is inputted?

Maybe make a little bit more explicit what is returned when a str is inputted for attr_names, and what when a list of strings is inputted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see the updated text

@EwoutH
Copy link
Contributor

EwoutH commented Feb 20, 2024

Would returning a dict with the attr_names as keys and a list of agent values as value be more elegant?

return_dict = {
    "attr_name1": [value1, value2],
    "attr_name2": [value1, value2]
}

@quaquel
Copy link
Contributor Author

quaquel commented Feb 20, 2024

I went back and forth on that. In the end, I stuck with the current design which also does not return a dict in case of just one attribute. Moreover, the caller knows the attributes and their order so it has all the information it needs and can easily create a list of dicts if desired.

Also, I prefer to keep the agent order in the return first. Long story short: this is in line with e.g. numpy which is row major in how the data is structured.

return [getattr(agent, attr_name) for agent in self._agents]

if isinstance(attr_names, str):
return [getattr(agent, attr_names) for agent in self._agents]
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should default to None (getattr(agent, attr_names, None)). This way it would be easier to collect across different agent types, which might or might not have the specified attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see your point but I am torn. I personally probably would prefer getting the error explicitly rather than being confronted with None values downstream.

Copy link
Contributor

Choose a reason for hiding this comment

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

Past discussion: projectmesa/mesa-examples#90. The summary is that it depends on the coder's background: in JavaScript, it is not surprising for obj.attr to return undefined and not raise an error. However, Python doesn't distinguish between null and undefined, and so there is an information loss when returning None. It is ambiguous and could mean the modeler intentionally chooses to return None instead of an absence of attribute.
I prefer no fallback to None.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could add a boolean switch (something like allow_undefined) that defaults to False. When False it isn't allowed and an error is returned, when True it is allowed and None values are returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that would make the code internally more complicated. You would get two additional nested if/else.

The easier solution is for the user to do agentset.select("condition to ensure attribute exists").get().

Also, why allow None in the first place? It only gives more downstream problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think its not necesarily an "error" if we collect None instead of triggering an AttributeError. It depends on the use case. If you quickly want to get some attributes on all of your agents, but some agents don't have that attribute you still get some result. Otherwise you first have to pre-select the right AgentSet and then collect the attributes.

I think the cleanest solution would probably be to mimic getattr in that users can provide a default value for agentset.get() and pass that to getattr, if set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

implementation wise this is a bit tricky. default in getattr is not a keyword argument but an argument. So you cannot use None to signal you don't specify default.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can defer this discussion and implementation in a separate PR. The current behavior in this PR is consistent with the existing API, which does not fall back to undefined/None.

@EwoutH
Copy link
Contributor

EwoutH commented Feb 20, 2024

Since the output format is different, and there are two separate code paths anyway, I was also thinking if it shouldn't be a separate method. Something like get_multiple or so.

@quaquel
Copy link
Contributor Author

quaquel commented Feb 20, 2024

I came to this code while playing with the data collection. Somewhere you have to go down either path (str, list of str). In the end, I saw pushing it into the agentset was the most convenient, but I am fine by separating it here into 2 methods and moving the if upwards

mesa/agent.py Outdated

Args:
attr_name (str): The name of the attribute to retrieve from each agent.
attr_names (str | List[str]): The name(s) of the attribute(s) to retrieve from each agent.
Copy link
Contributor

Choose a reason for hiding this comment

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

The List here shouldn't be capitalized.

Copy link
Contributor

Choose a reason for hiding this comment

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

The return value also has a capitalized List. This may be a small typo, but it can't be automatically fixed by pyupgrade.

@rht
Copy link
Contributor

rht commented Feb 21, 2024

Other than the List typo, LGTM.

@rht rht added the feature Release notes label label Feb 21, 2024
@quaquel
Copy link
Contributor Author

quaquel commented Feb 21, 2024

Other than the List typo, LGTM.

The List typo is fixed and I made the attribute error behavior explicit in the docstring

@rht
Copy link
Contributor

rht commented Feb 21, 2024

The List typo is fixed and I made the attribute error behavior explicit in the docstring

I still see capitalized List, and the last commit 76bb633 only added the AttributeError documentation.

@quaquel
Copy link
Contributor Author

quaquel commented Feb 21, 2024

Oh, you meant in the docstring?

@rht
Copy link
Contributor

rht commented Feb 21, 2024

Yes, that's why pyupgrade can't parse it, because it's a string. There is 1 more: https://github.com/projectmesa/mesa/pull/2044/files#diff-1dffe661c1ef6a8c750afb07bd029251b7c35f7793b044c5dde893c59236f0c3R272.

@quaquel
Copy link
Contributor Author

quaquel commented Feb 21, 2024

Yes, that's why pyupgrade can't parse it, because it's a string. There is 1 more: https://github.com/projectmesa/mesa/pull/2044/files#diff-1dffe661c1ef6a8c750afb07bd029251b7c35f7793b044c5dde893c59236f0c3R272.

And they are a pain to find in the IDE. But its fixed now.

@rht rht merged commit 3bafa14 into projectmesa:main Feb 21, 2024
12 checks passed
@EwoutH EwoutH added enhancement Release notes label and removed feature Release notes label labels Apr 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants