From d90b0f72cd65d5875eb9e4012990699cfe886c5c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 18:57:57 +0100 Subject: [PATCH 01/16] further updates --- benchmarks/WolfSheep/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index e69de29bb2d..18b86ab19ba 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -0,0 +1,14 @@ +from wolf_sheep import WolfSheep + +if __name__ == "__main__": + # for profiling this benchmark model + import time + + # model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) + model = WolfSheep(15, 100, 100, 1000, 500, 0.4, 0.2, 20) + + start_time = time.perf_counter() + for _ in range(100): + model.step() + + print(time.perf_counter() - start_time) From 95864900d9e7981c8ea7240e34672f3fe792d681 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 19:13:00 +0100 Subject: [PATCH 02/16] Update benchmarks/WolfSheep/__init__.py --- benchmarks/WolfSheep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index 18b86ab19ba..98b1e9fdfed 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -1,4 +1,4 @@ -from wolf_sheep import WolfSheep +from .wolf_sheep import WolfSheep if __name__ == "__main__": # for profiling this benchmark model From 2759244eb350ce8e38b31b08cbea46e40998893a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 27 Aug 2024 14:01:46 +0200 Subject: [PATCH 03/16] Update __init__.py --- benchmarks/WolfSheep/__init__.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index 98b1e9fdfed..e69de29bb2d 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -1,14 +0,0 @@ -from .wolf_sheep import WolfSheep - -if __name__ == "__main__": - # for profiling this benchmark model - import time - - # model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) - model = WolfSheep(15, 100, 100, 1000, 500, 0.4, 0.2, 20) - - start_time = time.perf_counter() - for _ in range(100): - model.step() - - print(time.perf_counter() - start_time) From 2f1cd4d0fa72d7edc3cb259d5249f49875706061 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 4 Sep 2024 15:11:21 +0200 Subject: [PATCH 04/16] add a default value to agentset.get supersedes #2067 --- mesa/agent.py | 43 +++++++++++++++++++++++++++++-------------- tests/test_agent.py | 12 ++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 225e89cda79..d952d1b0eca 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -142,12 +142,12 @@ def __contains__(self, agent: Agent) -> bool: return agent in self._agents def select( - self, - filter_func: Callable[[Agent], bool] | None = None, - at_most: int | float = float("inf"), - inplace: bool = False, - agent_type: type[Agent] | None = None, - n: int | None = None, + self, + filter_func: Callable[[Agent], bool] | None = None, + at_most: int | float = float("inf"), + inplace: bool = False, + agent_type: type[Agent] | None = None, + n: int | None = None, ) -> AgentSet: """ Select a subset of agents from the AgentSet based on a filter function and/or quantity limit. @@ -190,7 +190,7 @@ def agent_generator(filter_func, agent_type, at_most): if count >= at_most: break if (not filter_func or filter_func(agent)) and ( - not agent_type or isinstance(agent, agent_type) + not agent_type or isinstance(agent, agent_type) ): yield agent count += 1 @@ -225,10 +225,10 @@ def shuffle(self, inplace: bool = False) -> AgentSet: ) def sort( - self, - key: Callable[[Agent], Any] | str, - ascending: bool = False, - inplace: bool = False, + self, + key: Callable[[Agent], Any] | str, + ascending: bool = False, + inplace: bool = False, ) -> AgentSet: """ Sort the agents in the AgentSet based on a specified attribute or custom function. @@ -348,12 +348,16 @@ def agg(self, attribute: str, func: Callable) -> Any: values = self.get(attribute) return func(values) - def get(self, attr_names: str | list[str]) -> list[Any]: + def get(self, *args) -> list[Any]: """ Retrieve the specified attribute(s) from each agent in the AgentSet. Args: attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent. + default (Any): The default value of the attribute(s) to retrieve from each agent if + the agent does not have the attribute. + If no default value is passed, an AttributeError is raised if + an agent does not have the attribute. Returns: list[Any]: A list with the attribute value for each agent in the set if attr_names is a str @@ -363,12 +367,23 @@ def get(self, attr_names: str | list[str]) -> list[Any]: AttributeError if an agent does not have the specified attribute(s) """ + attr_names, *args = args + + def get_attr(*args): + obj, attr_name, *args = args + try: + return getattr(obj, attr_name) + except AttributeError as e: + if len(args) > 0: + return args[0] + else: + raise e if isinstance(attr_names, str): - return [getattr(agent, attr_names) for agent in self._agents] + return [get_attr(agent, attr_names, *args) for agent in self._agents] else: return [ - [getattr(agent, attr_name) for attr_name in attr_names] + [get_attr(agent, attr_name, *args) for attr_name in attr_names] for agent in self._agents ] diff --git a/tests/test_agent.py b/tests/test_agent.py index e78a4dcec93..559ecc150a3 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -276,6 +276,18 @@ def remove_function(agent): assert len(agentset) == 0 +def test_agentset_get(): + model = Model() + agents = [TestAgent(i, model) for i in range(10)] + + agentset = model.agents + + with pytest.raises(AttributeError): + agentset.get("unknown_attribute") + + results = agentset.get("unknown_attribute", True) + assert all(results) is True + def test_agentset_agg(): model = Model() agents = [TestAgent(model) for i in range(10)] From d168eb6f9336a736d34446c75e1c405977d97b9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:13:20 +0000 Subject: [PATCH 05/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/agent.py | 22 +++++++++++----------- tests/test_agent.py | 1 + 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index d952d1b0eca..4328c7a4b9a 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -142,12 +142,12 @@ def __contains__(self, agent: Agent) -> bool: return agent in self._agents def select( - self, - filter_func: Callable[[Agent], bool] | None = None, - at_most: int | float = float("inf"), - inplace: bool = False, - agent_type: type[Agent] | None = None, - n: int | None = None, + self, + filter_func: Callable[[Agent], bool] | None = None, + at_most: int | float = float("inf"), + inplace: bool = False, + agent_type: type[Agent] | None = None, + n: int | None = None, ) -> AgentSet: """ Select a subset of agents from the AgentSet based on a filter function and/or quantity limit. @@ -190,7 +190,7 @@ def agent_generator(filter_func, agent_type, at_most): if count >= at_most: break if (not filter_func or filter_func(agent)) and ( - not agent_type or isinstance(agent, agent_type) + not agent_type or isinstance(agent, agent_type) ): yield agent count += 1 @@ -225,10 +225,10 @@ def shuffle(self, inplace: bool = False) -> AgentSet: ) def sort( - self, - key: Callable[[Agent], Any] | str, - ascending: bool = False, - inplace: bool = False, + self, + key: Callable[[Agent], Any] | str, + ascending: bool = False, + inplace: bool = False, ) -> AgentSet: """ Sort the agents in the AgentSet based on a specified attribute or custom function. diff --git a/tests/test_agent.py b/tests/test_agent.py index 559ecc150a3..0c05b78e4fc 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -288,6 +288,7 @@ def test_agentset_get(): results = agentset.get("unknown_attribute", True) assert all(results) is True + def test_agentset_agg(): model = Model() agents = [TestAgent(model) for i in range(10)] From 2a84336ba3279b36ea25693a9ad61263c8d75a7a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 5 Sep 2024 14:03:42 +0200 Subject: [PATCH 06/16] make arguments explicit and add skip option --- mesa/agent.py | 80 +++++++++++++++++++++++++++++++-------------- tests/test_agent.py | 22 +++++++++++-- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 4328c7a4b9a..375baeaceba 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -348,44 +348,74 @@ def agg(self, attribute: str, func: Callable) -> Any: values = self.get(attribute) return func(values) - def get(self, *args) -> list[Any]: + def get(self, attr_names: str | list[str], handle_missing: str = "error", default_value: Any = None) -> list[Any] | list[list[Any]]: """ Retrieve the specified attribute(s) from each agent in the AgentSet. Args: attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent. - default (Any): The default value of the attribute(s) to retrieve from each agent if - the agent does not have the attribute. - If no default value is passed, an AttributeError is raised if - an agent does not have the attribute. + handle_missing (str, optional): How to handle missing attributes. Can be + + -'error' (default), raises an AttributeError if attribute is missing. + -'skip' which skips the agents missing the attribute. + 'default' to return the specified `default_value`. + + default_value (Any, optional): Used in conjunction with ``handle_missing='default`` + The default value to return if 'handle_missing' is set to 'default' + and the agent does not have the attribute. Returns: - list[Any]: A list with the attribute value for each agent in the set if attr_names is a str - list[list[Any]]: A list with a list of attribute values for each agent in the set if attr_names is a list of str + list[Any]: A list of attribute values for each agent if attr_names is a str. + list[list[Any]]: A list of lists of attribute values for each agent if attr_names is a list of str. Raises: - AttributeError if an agent does not have the specified attribute(s) - + AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s). """ - attr_names, *args = args - def get_attr(*args): - obj, attr_name, *args = args - try: - return getattr(obj, attr_name) - except AttributeError as e: - if len(args) > 0: - return args[0] + # Check if the attr_names is a single string or a list of attributes. + is_single_attr = isinstance(attr_names, str) + + # Branch earlier based on the `handle_missing` option to avoid repeated checks + match handle_missing: + case "error": + if is_single_attr: + return [self._get_or_raise(agent, attr_names) for agent in self._agents] else: - raise e + return [[self._get_or_raise(agent, attr) for attr in attr_names] for agent in self._agents] - if isinstance(attr_names, str): - return [get_attr(agent, attr_names, *args) for agent in self._agents] - else: - return [ - [get_attr(agent, attr_name, *args) for attr_name in attr_names] - for agent in self._agents - ] + case "default": + if is_single_attr: + return [self._get_with_default(agent, attr_names, default_value) for agent in self._agents] + else: + return [[self._get_with_default(agent, attr, default_value) for attr in attr_names] for agent in self._agents] + + case "skip": + if is_single_attr: + return [val for agent in self._agents if (val := self._get_skip(agent, attr_names)) is not None] + else: + return [ + [val for attr in attr_names if (val := self._get_skip(agent, attr)) is not None] + for agent in self._agents + ] + + case _: + raise ValueError(f"Unknown handle_missing option: {handle_missing}, " + "should be one of 'error', 'skip', or 'default'") + + # Helper method to get an attribute or raise an error if it's missing + def _get_or_raise(self, agent: Any, attr_name: str) -> Any: + return getattr(agent, attr_name) + + # Helper method to get an attribute with a default value if missing + def _get_with_default(self, agent: Any, attr_name: str, default: Any) -> Any: + return getattr(agent, attr_name, default) + + # Helper method to skip missing attributes by returning None + def _get_skip(self, agent: Any, attr_name: str) -> Any: + try: + return getattr(agent, attr_name) + except AttributeError: + return None def set(self, attr_name: str, value: Any) -> AgentSet: """ diff --git a/tests/test_agent.py b/tests/test_agent.py index 0c05b78e4fc..e201f846b49 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -278,16 +278,34 @@ def remove_function(agent): def test_agentset_get(): model = Model() - agents = [TestAgent(i, model) for i in range(10)] + _ = [TestAgent(i, model) for i in range(10)] agentset = model.agents + agentset.set("a", 5) + agentset.set("b", 6) + + values = agentset.get(["a", "b"]) + assert all([(a == 5) & (b == 6) for a,b in values]) + with pytest.raises(AttributeError): agentset.get("unknown_attribute") - results = agentset.get("unknown_attribute", True) + results = agentset.get("unknown_attribute", handle_missing="default", default_value=True) assert all(results) is True + values = agentset.get(["a", "unknown_attribute"], handle_missing="default", default_value=True) + assert all([(a == 5) & (b == True) for a,b in values]) + + results = agentset.get("unknown_attribute", handle_missing="skip") + assert len(results) == 0 + + values = agentset.get(["a", "unknown_attribute"], handle_missing="skip") + assert all([(a == 5) for a, in values]) + + with pytest.raises(ValueError): + agentset.get("unknown_attribute", handle_missing="some nonsense value") + def test_agentset_agg(): model = Model() From 8bc05b16a56190a37b17852ee863ef1e4b1f01fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:03:51 +0000 Subject: [PATCH 07/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/agent.py | 47 ++++++++++++++++++++++++++++++++++++--------- tests/test_agent.py | 14 +++++++++----- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 375baeaceba..9cd38a8e1bd 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -348,7 +348,12 @@ def agg(self, attribute: str, func: Callable) -> Any: values = self.get(attribute) return func(values) - def get(self, attr_names: str | list[str], handle_missing: str = "error", default_value: Any = None) -> list[Any] | list[list[Any]]: + def get( + self, + attr_names: str | list[str], + handle_missing: str = "error", + default_value: Any = None, + ) -> list[Any] | list[list[Any]]: """ Retrieve the specified attribute(s) from each agent in the AgentSet. @@ -379,28 +384,52 @@ def get(self, attr_names: str | list[str], handle_missing: str = "error", defaul match handle_missing: case "error": if is_single_attr: - return [self._get_or_raise(agent, attr_names) for agent in self._agents] + return [ + self._get_or_raise(agent, attr_names) for agent in self._agents + ] else: - return [[self._get_or_raise(agent, attr) for attr in attr_names] for agent in self._agents] + return [ + [self._get_or_raise(agent, attr) for attr in attr_names] + for agent in self._agents + ] case "default": if is_single_attr: - return [self._get_with_default(agent, attr_names, default_value) for agent in self._agents] + return [ + self._get_with_default(agent, attr_names, default_value) + for agent in self._agents + ] else: - return [[self._get_with_default(agent, attr, default_value) for attr in attr_names] for agent in self._agents] + return [ + [ + self._get_with_default(agent, attr, default_value) + for attr in attr_names + ] + for agent in self._agents + ] case "skip": if is_single_attr: - return [val for agent in self._agents if (val := self._get_skip(agent, attr_names)) is not None] + return [ + val + for agent in self._agents + if (val := self._get_skip(agent, attr_names)) is not None + ] else: return [ - [val for attr in attr_names if (val := self._get_skip(agent, attr)) is not None] + [ + val + for attr in attr_names + if (val := self._get_skip(agent, attr)) is not None + ] for agent in self._agents ] case _: - raise ValueError(f"Unknown handle_missing option: {handle_missing}, " - "should be one of 'error', 'skip', or 'default'") + raise ValueError( + f"Unknown handle_missing option: {handle_missing}, " + "should be one of 'error', 'skip', or 'default'" + ) # Helper method to get an attribute or raise an error if it's missing def _get_or_raise(self, agent: Any, attr_name: str) -> Any: diff --git a/tests/test_agent.py b/tests/test_agent.py index e201f846b49..f3913b80b8a 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -286,22 +286,26 @@ def test_agentset_get(): agentset.set("b", 6) values = agentset.get(["a", "b"]) - assert all([(a == 5) & (b == 6) for a,b in values]) + assert all([(a == 5) & (b == 6) for a, b in values]) with pytest.raises(AttributeError): agentset.get("unknown_attribute") - results = agentset.get("unknown_attribute", handle_missing="default", default_value=True) + results = agentset.get( + "unknown_attribute", handle_missing="default", default_value=True + ) assert all(results) is True - values = agentset.get(["a", "unknown_attribute"], handle_missing="default", default_value=True) - assert all([(a == 5) & (b == True) for a,b in values]) + values = agentset.get( + ["a", "unknown_attribute"], handle_missing="default", default_value=True + ) + assert all([(a == 5) & (b == True) for a, b in values]) results = agentset.get("unknown_attribute", handle_missing="skip") assert len(results) == 0 values = agentset.get(["a", "unknown_attribute"], handle_missing="skip") - assert all([(a == 5) for a, in values]) + assert all([(a == 5) for (a,) in values]) with pytest.raises(ValueError): agentset.get("unknown_attribute", handle_missing="some nonsense value") From 713373a925cb945a2f825c89eb20b132249955e5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 5 Sep 2024 14:06:27 +0200 Subject: [PATCH 08/16] ruff related fixes --- tests/test_agent.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_agent.py b/tests/test_agent.py index f3913b80b8a..8486035b377 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -286,7 +286,7 @@ def test_agentset_get(): agentset.set("b", 6) values = agentset.get(["a", "b"]) - assert all([(a == 5) & (b == 6) for a, b in values]) + assert all((a == 5) & (b == 6) for a,b in values) with pytest.raises(AttributeError): agentset.get("unknown_attribute") @@ -296,16 +296,14 @@ def test_agentset_get(): ) assert all(results) is True - values = agentset.get( - ["a", "unknown_attribute"], handle_missing="default", default_value=True - ) - assert all([(a == 5) & (b == True) for a, b in values]) + values = agentset.get(["a", "unknown_attribute"], handle_missing="default", default_value=True) + assert all((a == 5) & b for a,b in values) results = agentset.get("unknown_attribute", handle_missing="skip") assert len(results) == 0 values = agentset.get(["a", "unknown_attribute"], handle_missing="skip") - assert all([(a == 5) for (a,) in values]) + assert all((a == 5) for a, in values) with pytest.raises(ValueError): agentset.get("unknown_attribute", handle_missing="some nonsense value") From 81c4a6df1b148cb21fb7c4c7e7bbbb32ccdf7dc1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:07:29 +0000 Subject: [PATCH 09/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_agent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_agent.py b/tests/test_agent.py index 8486035b377..4aac00be646 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -286,7 +286,7 @@ def test_agentset_get(): agentset.set("b", 6) values = agentset.get(["a", "b"]) - assert all((a == 5) & (b == 6) for a,b in values) + assert all((a == 5) & (b == 6) for a, b in values) with pytest.raises(AttributeError): agentset.get("unknown_attribute") @@ -296,14 +296,16 @@ def test_agentset_get(): ) assert all(results) is True - values = agentset.get(["a", "unknown_attribute"], handle_missing="default", default_value=True) - assert all((a == 5) & b for a,b in values) + values = agentset.get( + ["a", "unknown_attribute"], handle_missing="default", default_value=True + ) + assert all((a == 5) & b for a, b in values) results = agentset.get("unknown_attribute", handle_missing="skip") assert len(results) == 0 values = agentset.get(["a", "unknown_attribute"], handle_missing="skip") - assert all((a == 5) for a, in values) + assert all((a == 5) for (a,) in values) with pytest.raises(ValueError): agentset.get("unknown_attribute", handle_missing="some nonsense value") From 721884cdc48ca1a6e3063e197b039f67096dc1fc Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Thu, 5 Sep 2024 19:03:36 +0200 Subject: [PATCH 10/16] test_agent.py: Add some more cases for get --- tests/test_agent.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_agent.py b/tests/test_agent.py index 4aac00be646..876bb47a70b 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -285,31 +285,59 @@ def test_agentset_get(): agentset.set("a", 5) agentset.set("b", 6) + # Case 1: Normal retrieval of existing attributes values = agentset.get(["a", "b"]) assert all((a == 5) & (b == 6) for a, b in values) + # Case 2: Raise AttributeError when attribute doesn't exist with pytest.raises(AttributeError): agentset.get("unknown_attribute") + # Case 3: Use default value when attribute is missing results = agentset.get( "unknown_attribute", handle_missing="default", default_value=True ) assert all(results) is True + # Case 4: Retrieve mixed attributes with default value for missing ones values = agentset.get( ["a", "unknown_attribute"], handle_missing="default", default_value=True ) assert all((a == 5) & b for a, b in values) + # Case 5: Skip agents missing the attribute results = agentset.get("unknown_attribute", handle_missing="skip") assert len(results) == 0 + # Case 6: Skip missing attributes in mixed attribute retrieval values = agentset.get(["a", "unknown_attribute"], handle_missing="skip") assert all((a == 5) for (a,) in values) + # Case 7: Invalid handle_missing value raises ValueError with pytest.raises(ValueError): agentset.get("unknown_attribute", handle_missing="some nonsense value") + # Case 8: Retrieve multiple attributes with mixed existence and 'default' handling + values = agentset.get( + ["a", "b", "unknown_attribute"], handle_missing="default", default_value=0 + ) + assert all((a == 5) & (b == 6) & (unknown == 0) for a, b, unknown in values) + + # Case 9: Retrieve multiple attributes with 'skip' handling for missing attributes + values = agentset.get(["a", "b", "unknown_attribute"], handle_missing="skip") + assert all((a == 5) & (b == 6) for a, b in values) # No unknown_attribute included + + # Case 10: 'default' handling when one attribute is completely missing from some agents + agentset.set("c", 8) # Only some agents have attribute 'c' + values = agentset.get( + ["a", "c"], handle_missing="default", default_value=-1 + ) + assert all((a == 5) & (c in [8, -1]) for a, c in values) + + # Case 11: Skip handling when one attribute is completely missing from some agents + values = agentset.get(["a", "c"], handle_missing="skip") + assert all(a == 5 for (a,) in values) # Only agents with 'a' are returned + def test_agentset_agg(): model = Model() From 73d9346b75e228a0b68a200092f909110f83cb02 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:03:42 +0000 Subject: [PATCH 11/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_agent.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_agent.py b/tests/test_agent.py index 876bb47a70b..b119a224904 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -329,9 +329,7 @@ def test_agentset_get(): # Case 10: 'default' handling when one attribute is completely missing from some agents agentset.set("c", 8) # Only some agents have attribute 'c' - values = agentset.get( - ["a", "c"], handle_missing="default", default_value=-1 - ) + values = agentset.get(["a", "c"], handle_missing="default", default_value=-1) assert all((a == 5) & (c in [8, -1]) for a, c in values) # Case 11: Skip handling when one attribute is completely missing from some agents From bd20fa657587dabb40d65fad132e2af579e65815 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 5 Sep 2024 22:41:02 +0200 Subject: [PATCH 12/16] Update mesa/agent.py Co-authored-by: Corvince <13568919+Corvince@users.noreply.github.com> --- mesa/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/agent.py b/mesa/agent.py index 9cd38a8e1bd..5508830beeb 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -351,7 +351,7 @@ def agg(self, attribute: str, func: Callable) -> Any: def get( self, attr_names: str | list[str], - handle_missing: str = "error", + handle_missing: Literal["error", "skip", "default"] = "error", default_value: Any = None, ) -> list[Any] | list[list[Any]]: """ From 507e2525a1ec97e6da6f4449d94fd02274373c66 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:42:47 +0000 Subject: [PATCH 13/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/agent.py b/mesa/agent.py index 5508830beeb..970b03dae60 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -351,7 +351,7 @@ def agg(self, attribute: str, func: Callable) -> Any: def get( self, attr_names: str | list[str], - handle_missing: Literal["error", "skip", "default"] = "error", + handle_missing: Literal[error, skip, default] = "error", default_value: Any = None, ) -> list[Any] | list[list[Any]]: """ From 576a389a8e98cb35c79e50701621c8ee05ca44b4 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Fri, 6 Sep 2024 10:33:25 +0200 Subject: [PATCH 14/16] Update get bug and tests --- mesa/agent.py | 126 ++++++++++++++++++-------------------------- tests/test_agent.py | 6 +-- 2 files changed, 53 insertions(+), 79 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 970b03dae60..19bbc5b052f 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -20,7 +20,7 @@ from random import Random # mypy -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: # We ensure that these are not imported during runtime to prevent cyclic @@ -351,7 +351,7 @@ def agg(self, attribute: str, func: Callable) -> Any: def get( self, attr_names: str | list[str], - handle_missing: Literal[error, skip, default] = "error", + handle_missing: Literal["error", "skip", "default"] = "error", default_value: Any = None, ) -> list[Any] | list[list[Any]]: """ @@ -359,15 +359,12 @@ def get( Args: attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent. - handle_missing (str, optional): How to handle missing attributes. Can be - - -'error' (default), raises an AttributeError if attribute is missing. - -'skip' which skips the agents missing the attribute. - 'default' to return the specified `default_value`. - - default_value (Any, optional): Used in conjunction with ``handle_missing='default`` - The default value to return if 'handle_missing' is set to 'default' - and the agent does not have the attribute. + handle_missing (str, optional): How to handle missing attributes. Can be: + - 'error' (default): raises an AttributeError if attribute is missing. + - 'skip': skips the agents missing the attribute. + - 'default': returns the specified default_value. + default_value (Any, optional): The default value to return if 'handle_missing' is set to 'default' + and the agent does not have the attribute. Returns: list[Any]: A list of attribute values for each agent if attr_names is a str. @@ -375,76 +372,53 @@ def get( Raises: AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s). + ValueError: If an unknown 'handle_missing' option is provided. """ - - # Check if the attr_names is a single string or a list of attributes. is_single_attr = isinstance(attr_names, str) - # Branch earlier based on the `handle_missing` option to avoid repeated checks - match handle_missing: - case "error": - if is_single_attr: - return [ - self._get_or_raise(agent, attr_names) for agent in self._agents - ] - else: - return [ - [self._get_or_raise(agent, attr) for attr in attr_names] - for agent in self._agents - ] - - case "default": - if is_single_attr: - return [ - self._get_with_default(agent, attr_names, default_value) - for agent in self._agents - ] - else: - return [ - [ - self._get_with_default(agent, attr, default_value) - for attr in attr_names - ] - for agent in self._agents - ] - - case "skip": - if is_single_attr: - return [ - val - for agent in self._agents - if (val := self._get_skip(agent, attr_names)) is not None - ] - else: - return [ - [ - val - for attr in attr_names - if (val := self._get_skip(agent, attr)) is not None - ] - for agent in self._agents + if handle_missing == "error": + if is_single_attr: + return [getattr(agent, attr_names) for agent in self._agents] + else: + return [ + [getattr(agent, attr) for attr in attr_names] + for agent in self._agents + ] + + elif handle_missing == "default": + if is_single_attr: + return [ + getattr(agent, attr_names, default_value) for agent in self._agents + ] + else: + return [ + [getattr(agent, attr, default_value) for attr in attr_names] + for agent in self._agents + ] + + elif handle_missing == "skip": + if is_single_attr: + return [ + getattr(agent, attr_names) + for agent in self._agents + if hasattr(agent, attr_names) + ] + else: + return [ + [ + getattr(agent, attr) + for attr in attr_names + if hasattr(agent, attr) ] + for agent in self._agents + if any(hasattr(agent, attr) for attr in attr_names) + ] - case _: - raise ValueError( - f"Unknown handle_missing option: {handle_missing}, " - "should be one of 'error', 'skip', or 'default'" - ) - - # Helper method to get an attribute or raise an error if it's missing - def _get_or_raise(self, agent: Any, attr_name: str) -> Any: - return getattr(agent, attr_name) - - # Helper method to get an attribute with a default value if missing - def _get_with_default(self, agent: Any, attr_name: str, default: Any) -> Any: - return getattr(agent, attr_name, default) - - # Helper method to skip missing attributes by returning None - def _get_skip(self, agent: Any, attr_name: str) -> Any: - try: - return getattr(agent, attr_name) - except AttributeError: - return None + else: + raise ValueError( + f"Unknown handle_missing option: {handle_missing}, " + "should be one of 'error', 'skip', or 'default'" + ) def set(self, attr_name: str, value: Any) -> AgentSet: """ diff --git a/tests/test_agent.py b/tests/test_agent.py index b119a224904..195a3fe44d5 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -303,7 +303,7 @@ def test_agentset_get(): values = agentset.get( ["a", "unknown_attribute"], handle_missing="default", default_value=True ) - assert all((a == 5) & b for a, b in values) + assert all((a == 5) & (unknown is True) for a, unknown in values) # Case 5: Skip agents missing the attribute results = agentset.get("unknown_attribute", handle_missing="skip") @@ -328,13 +328,13 @@ def test_agentset_get(): assert all((a == 5) & (b == 6) for a, b in values) # No unknown_attribute included # Case 10: 'default' handling when one attribute is completely missing from some agents - agentset.set("c", 8) # Only some agents have attribute 'c' + agentset.select(at_most=0.5).set("c", 8) # Only some agents have attribute 'c' values = agentset.get(["a", "c"], handle_missing="default", default_value=-1) assert all((a == 5) & (c in [8, -1]) for a, c in values) # Case 11: Skip handling when one attribute is completely missing from some agents values = agentset.get(["a", "c"], handle_missing="skip") - assert all(a == 5 for (a,) in values) # Only agents with 'a' are returned + assert all(5 in a for a in values) # Only agents with 'a' are returned def test_agentset_agg(): From b623cd1f72e065a3db6d9eb119d187b7b8652c51 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 6 Sep 2024 16:04:53 +0200 Subject: [PATCH 15/16] remove skip and update tests --- mesa/agent.py | 27 ++++----------------------- tests/test_agent.py | 21 +++------------------ 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 19bbc5b052f..5816ddf060a 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -351,7 +351,7 @@ def agg(self, attribute: str, func: Callable) -> Any: def get( self, attr_names: str | list[str], - handle_missing: Literal["error", "skip", "default"] = "error", + handle_missing: Literal["error", "default"] = "error", default_value: Any = None, ) -> list[Any] | list[list[Any]]: """ @@ -361,14 +361,13 @@ def get( attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent. handle_missing (str, optional): How to handle missing attributes. Can be: - 'error' (default): raises an AttributeError if attribute is missing. - - 'skip': skips the agents missing the attribute. - 'default': returns the specified default_value. default_value (Any, optional): The default value to return if 'handle_missing' is set to 'default' and the agent does not have the attribute. Returns: - list[Any]: A list of attribute values for each agent if attr_names is a str. - list[list[Any]]: A list of lists of attribute values for each agent if attr_names is a list of str. + list[Any]: A list with the attribute value for each agent if attr_names is a str. + list[list[Any]]: A list with a lists of attribute values for each agent if attr_names is a list of str. Raises: AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s). @@ -396,28 +395,10 @@ def get( for agent in self._agents ] - elif handle_missing == "skip": - if is_single_attr: - return [ - getattr(agent, attr_names) - for agent in self._agents - if hasattr(agent, attr_names) - ] - else: - return [ - [ - getattr(agent, attr) - for attr in attr_names - if hasattr(agent, attr) - ] - for agent in self._agents - if any(hasattr(agent, attr) for attr in attr_names) - ] - else: raise ValueError( f"Unknown handle_missing option: {handle_missing}, " - "should be one of 'error', 'skip', or 'default'" + "should be one of 'error' or 'default'" ) def set(self, attr_name: str, value: Any) -> AgentSet: diff --git a/tests/test_agent.py b/tests/test_agent.py index 195a3fe44d5..3438776a83d 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -305,36 +305,21 @@ def test_agentset_get(): ) assert all((a == 5) & (unknown is True) for a, unknown in values) - # Case 5: Skip agents missing the attribute - results = agentset.get("unknown_attribute", handle_missing="skip") - assert len(results) == 0 - - # Case 6: Skip missing attributes in mixed attribute retrieval - values = agentset.get(["a", "unknown_attribute"], handle_missing="skip") - assert all((a == 5) for (a,) in values) - - # Case 7: Invalid handle_missing value raises ValueError + # Case 5: Invalid handle_missing value raises ValueError with pytest.raises(ValueError): agentset.get("unknown_attribute", handle_missing="some nonsense value") - # Case 8: Retrieve multiple attributes with mixed existence and 'default' handling + # Case 6: Retrieve multiple attributes with mixed existence and 'default' handling values = agentset.get( ["a", "b", "unknown_attribute"], handle_missing="default", default_value=0 ) assert all((a == 5) & (b == 6) & (unknown == 0) for a, b, unknown in values) - # Case 9: Retrieve multiple attributes with 'skip' handling for missing attributes - values = agentset.get(["a", "b", "unknown_attribute"], handle_missing="skip") - assert all((a == 5) & (b == 6) for a, b in values) # No unknown_attribute included - - # Case 10: 'default' handling when one attribute is completely missing from some agents + # Case 7: 'default' handling when one attribute is completely missing from some agents agentset.select(at_most=0.5).set("c", 8) # Only some agents have attribute 'c' values = agentset.get(["a", "c"], handle_missing="default", default_value=-1) assert all((a == 5) & (c in [8, -1]) for a, c in values) - # Case 11: Skip handling when one attribute is completely missing from some agents - values = agentset.get(["a", "c"], handle_missing="skip") - assert all(5 in a for a in values) # Only agents with 'a' are returned def test_agentset_agg(): From 9d33f687c5b502e6e72c0639b341bb863ab94ec8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:05:02 +0000 Subject: [PATCH 16/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_agent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_agent.py b/tests/test_agent.py index 3438776a83d..787cfc0bc75 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -321,7 +321,6 @@ def test_agentset_get(): assert all((a == 5) & (c in [8, -1]) for a, c in values) - def test_agentset_agg(): model = Model() agents = [TestAgent(model) for i in range(10)]