Fix: IndexError in select_random_agent when cell collection is empty#2983
Fix: IndexError in select_random_agent when cell collection is empty#2983
Conversation
|
Performance benchmarks:
|
|
I disagree with this fix. In my view, returning None is a bad habit because it forces the user to perform a check on the return type. It is, in fact, more Pythonic, in my experience, to use try-except structures for control flow. See for example the first answer to this question on stack overflow. |
I have updated the PR according to the agreed fix in #2986. Kindly review it |
|
|
||
| T = TypeVar("T", bound="Cell") | ||
|
|
||
| _no_default = object() |
There was a problem hiding this comment.
Why did you choose to do it this way instead of using None as in dict.get? If the idea is to mimic a well established pattern like dict.get, why deviate from it?
There was a problem hiding this comment.
The reason for the sentinel is that we want to distinguish between two specific scenarios:
select_random_agent()(No argument) Should raiseLookupError.select_random_agent(default=None)(Explicit None) Should returnNone
If I defined the signature as def select_random_agent(self, default=None):, then 1 would automatically return None instead of raising an error. The function would have no way of knowing if the user forgot the argument (which should error) or explicitly passed None (which should not).
There was a problem hiding this comment.
Logically, I understood what was happening. I am just not sure what the cleanest solution is. Conceptually, we are taking inspiration from dict.get, which uses None. However, dict.get will never raise an exception, so it's not a perfect analogy.
So, I am fine with the suggested approach. It offers both exceptions as default, but also control, including the use of None.
2 minor quibbles: first, since this becomes part of the signature of the method, naming this matters. no_default is not really clear. In 32986, @Nithin9585 uses RAISES which is perhaps clearer. Second, for the same reason, I would not use an underscore in the name.
There was a problem hiding this comment.
I have renamed it to RAISES, as you requested.
quaquel
left a comment
There was a problem hiding this comment.
Can you have a look at the naming of the sentinel? Otherwise, this is good to go.
Summary
This PR fixes a crash in
CellCollection.select_random_agent()where calling the method on a collection with no agents raised anIndexError.It implements a hybrid solution (consensus reached in #2982 ):
Raises a descriptive LookupError if the collection is empty and no default is provided.
Adds a default parameter . Users with sparse grids can pass
default=Noneto avoid the overhead of exception handling.Bug / Issue
Fixes #2982
In sparse grids or simulations where agents move/die, it is common for a cell's neighborhood to contain no agents. What happened previously is that
select_random_agentattempted to callself.random.choice()onlist(self.agents). Ifself.agentswas empty,random.choiceraisedIndexError: Cannot choose from an empty sequence, causing the entire model to crash. The method now checks for empty collections first. Ifdefaultis not provided it raisesLookupError: Cannot select random agent from empty collection. Ifdefaultis provided then it returns the default value.Implementation
mesa/discrete_space/cell_collection.pytest_select_random_agent_empty_safeintests/test_cell_spaces.py:Testing
Bug Reproduction Script, credit @Nithin9585
Output Before
Output After
Additional Notes
This design balances the Pythonic "EAFP" principle with the performance needs of Agent-Based Modeling.
Maintainers preferred raising an Exception by default to avoid type-checking ambiguity.
Benchmarks provided by @Nithin9895 in #2982 showed that using
default=Noneis ~1.2x faster than try/except blocks on sparse grids (5% density). This opt-in path allows high-performance simulations to avoid the overhead of creating/catching exceptions thousands of times per step.Credits:
@Nithin9585 for the bug report, reproduction script, and performance benchmarks.
@quaquel and @EwoutH for the design discussion regarding EAFP vs. Performance.