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

Find single agent in agentset #2316

Open
Corvince opened this issue Sep 22, 2024 · 8 comments
Open

Find single agent in agentset #2316

Corvince opened this issue Sep 22, 2024 · 8 comments

Comments

@Corvince
Copy link
Contributor

Corvince commented Sep 22, 2024

What's the problem this feature will solve?
As @rht pointed out in #2314 (comment) There is currently no easy way to retrieve a single agent by it's I'd.

Describe the solution you'd like
I see two ways to solve this

  1. Add an "agents" property (in sense of @Property) that is a dict so users can do agentset.agents[unique_id]
  2. Add a "find" function to agentset that works like "select", but returns a single agent or None.

Of course it would also be possible to add both.

@EwoutH
Copy link
Member

EwoutH commented Sep 22, 2024

Thanks for the write-up. Slightly related, I have a sample feature on my TODO list, which is a fast combination of shuffle and select. Sampling one could be one of the possibilities here.

  1. Add an "agents" property (in sense of @Property) that is a dict so users can do agentset.agents[unique_id]

Directly accessing by unique_id or a range of unique_ids would be very interesting! Maybe even allow a range for slicing?

One consideration is if we want to access by unique_id, or in the order of the agents in the AgentSet (after a sort or a shuffle).

Maybe the AgentSet should have a boolean attribute called in_order, which states if the AgentSet is in order of unique_id or not.

But I think we have to make a choice here if we want to allow accessing by unique_id (dict like) or allow accessing/slicing by position (list like).

  1. Add a "find" function to agentset that works like "select", but returns a single agent or None.

For reference, this feature would be roughly equivalent to NetLogo's one-of. They actually have a lot of these kind of functions: max-n-of max-one-of member? min-n-of min-one-of n-of neighbors neighbors4 one-of up-to-n-of who-are-not with with-max with-min.

@quaquel
Copy link
Member

quaquel commented Sep 22, 2024

Just some questions, trying to understand better what this will solve:

  1. What is the use case for this feature? I agree the feature does not exist directly (but is trivial via .select). But I need help understanding when you would want to use it. When would you know the unique_id but not have the agent itself?

2

Directly accessing by unique_id or a range of unique_ids would be very interesting! Maybe even allow a range for slicing?

A few thoughts. First, tyou can already slice an agentset because it implements Sequence. So you can do my_agentset[0:10], which will give you the first ten entries in the set. Second, slicing based on unique_id is not well defined. You have no idea what IDs are in the set, nor can you slice on them in a meaningful way unless the envisioned property is constantly sorting based on unique_id. But even then, there might be missing IDs/ IDs that might not be consecutive. Third, there already is a .sort method available if, for whatever use case, you want to sort based on unique_id. So again, why would we need this?

  1. Seeing the range of netlogo functions, that is bad design. How can a user ever hope to remember so many different functions?

@rht
Copy link
Contributor

rht commented Sep 22, 2024

From #2317 (comment)

If __getitem__ becomes def __getitem__(self, unique_id): return self._agents[unique_id] as the default, it would simplify the documentation here.

I also just note that I object fundamentally to this. It brakes the Sequence protocol.

  1. I think the agent ordering in an AgentSet is a less useful construct. In any practical models, most of the time, they would be shuffled anyway when a model step happens.
  2. model.agents[10:15] would be changing all the time, making it impossible to keep track of specific agents for data collection and debugging. If you want to sample 5 random agents every single time, you can do model.agents.select(at_most=5)
  3. the underlying data structure of model._agents is that of a dict, which has to be converted to list for every __getitem__. This is an expensive way to implement Sequence.

Seeing the range of netlogo functions, that is bad design. How can a user ever hope to remember so many different functions?

Digression:
Having lots of functions (incidentally, the - separator in the naming) seem to be derived from LISP/Scheme. The list of functions in Scheme R7RS standard libraries may be small, but for a specific implementation, the list of functions is huge. There are a dozen functions just for amap:

amap->alist. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
amap-args . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
amap-clean!. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
amap-clear!. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
amap-comparator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
amap-contains? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
amap-copy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
amap-count . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
amap-delete! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
amap-difference! . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
amap-empty-copy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
amap-empty?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
amap-entries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
amap-find . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
amap-fold . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
amap-for-each . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212

@quaquel
Copy link
Member

quaquel commented Sep 22, 2024

Yes, using __getitem__ is slow, and I don't see it commonly used. When building AgentSet it just got added in because it was easy to add, and the internal dict is ordered already. But, likewise, I still fail to see a use case for doing agent lookups based on unique_id that are not currently already covered by select.

@rht
Copy link
Contributor

rht commented Sep 22, 2024

But, likewise, I still fail to see a use case for doing agent lookups based on unique_id that are not currently already covered by select.

It's covered by select, but it is very lengthy for the user to do so (model.agents.select(lambda agent: agent.unique_id == 1)[0]), instead of model.agents.find(1) / model.agents.agents_dict[1] / model.agents[1].

@quaquel
Copy link
Member

quaquel commented Sep 22, 2024

It's covered by select, but it is very lengthy for the user to do so (model.agents.select(lambda agent: agent.unique_id == 1)[0]), instead of model.agents.find(1) / model.agents.agents_dict[1] / model.agents[1].

Fair enough, but again: when would a user want to use this? The only use case that has been claimed, but not developed in any clear detail is debugging.

@rht
Copy link
Contributor

rht commented Sep 22, 2024

I do have a specific example: in the past model I wrote, one class of the agents consists of systemically important banks, the other class being other financial institutions. I had to be able to track and analyze the evolution of a particular named bank (e.g. Barclays plc) under an external shock (data collection).

(This is a separate point: this also means that each agents attributes was initialized from concrete data, and the removal of explicit unique ID in Mesa 3.0 makes it harder to use the ISIN of the banks as the unique ID.)

@quaquel
Copy link
Member

quaquel commented Sep 22, 2024

Ok, but then it is about finding 1 specific agent given some selection criterion (which can be unique_id). Still, it might be anything that the user wants to have as its selection criterion. I can see an argument for having a dedicated find method for this. So something like the following:

def find(self, select_function: Callable) -> Agent:
    '''Find the first agent that meets the select function.

    Args:
        select_function: A callable that is called with an agent and returns a boolean

    '''
    for agent in self._agents:
        if select_function(agent):
            return agent
    return None  # or perhaps even raise an exception?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants