Repository: projectmesa/mesa

Issue:
Allow creating multiple agents directly
It would be nice if there was some utility function to create multiple agents directly (via the AgentSet or the model).

This shouldn't be needed, for example:
```Python
        for i in range(self.num_agents):
            BarCustomer(i, self, memory_size, crowd_threshold, num_strategies)
```
Loops might still be nice if you want to create complicated agents and positon them etc, but an elegant alternative should be present in the AgentSet or model for really simple operations.


Discussion:
Is the sole purpose of the utility function to create an AgentSet where the agents have `unique_id`'s `[0, 1, 2, .... num_agents-1]`?

If this Is the case, then there is no need to create this utility because of fixing #2213?
I mainly think loops are unnecessary in most cases, but solving #2213 first would help with this issue, since you don't need to input an `unique_id` anymore.

I don't have a clear user API in mind as this point. There could also be a bit of discussion where this code should take place (model, agent, agentset or other).

@adamamer20 how do you populate the AgentSet / create multiple agents in Mesa-frames?
> I mainly think loops are unnecessary in most cases, but solving #2213 first would help with this issue, since you don't need to input an `unique_id` anymore.
> 
> I don't have a clear user API in mind as this point. There could also be a bit of discussion where this code should take place (model, agent, agentset or other).
> 
> @adamamer20 how do you populate the AgentSet / create multiple agents in Mesa-frames?

The syntax is AgentSet.add(agents) or AgentSet + agents or AgentSet += agents where agents is anything that could be constructed into a DF (so a dictionary, a sequence of sequences or a DF itself). It must have a 'unique_id' integer column or if sequence of sequences it will try to take the first value as unique_id.

An example would be:

```python
df = pd.DataFrame({'unique_id': [0, 1, 2], 'sugar': [0, 3, 5]})
AgentSetDF.add(df)
# equivalent to 
AgentSetDF += df
# equivalent to (although a bit slower because creates a copy) 
AgentSetDF = AgentSetDF + df
```
> Loops might still be nice if you want to create complicated agents and positon them etc, but an elegant alternative should be present in the AgentSet or model for really simple operations.

>I mainly think loops are unnecessary in most cases, but solving https://github.com/projectmesa/mesa/issues/2213 first would help with this issue, since you don't need to input an unique_id anymore.

The default way to solve this is some kind of factory method/factory pattern. In python, one way to do it is through a factory class method. It will be hard, however, to fully standardize this given the degrees of freedom users have in defining their own Agent subclasses. A factory method is typically a class level method and the API could look something like this

```python


Agent.create_agents(SomeBaseAgentClass, model, dataframe_with_attributes)

```

Not sure what you mean about loops not being necessary because any factory will have some internal loop to create each individual agent. 





Thanks for both your insights! I'm not sure if this can be implemented more intuitively than the current best-practice of just looping.

Certainly not a priority, let's focus on more valuable features for now.
is this issue still open?

@advait-zx yes, but I'm not sure if "good first issue" is still representative of the difficulty of this issue. If you want to tackle this, please first discuss your proposed API here with us.



Patch:
diff --git a/mesa/agent.py b/mesa/agent.py
index 044b40acecf..5cfc1131e2c 100644
--- a/mesa/agent.py
+++ b/mesa/agent.py
@@ -85,6 +85,54 @@ def step(self) -> None:
     def advance(self) -> None:  # noqa: D102
         pass
 
+    @classmethod
+    def create_agents(cls, model: Model, n: int, *args, **kwargs) -> AgentSet[Agent]:
+        """Create N agents.
+
+        Args:
+            model: the model to which the agents belong
+            args: arguments to pass onto agent instances
+                  each arg is either a single object or a sequence of length n
+            n: the number of agents to create
+            kwargs: keyword arguments to pass onto agent instances
+                   each keyword arg is either a single object or a sequence of length n
+
+        Returns:
+            AgentSet containing the agents created.
+
+        """
+
+        class ListLike:
+            """Helper class to make default arguments act as if they are in a list of length N."""
+
+            def __init__(self, value):
+                self.value = value
+
+            def __getitem__(self, i):
+                return self.value
+
+        listlike_args = []
+        for arg in args:
+            if isinstance(arg, (list | np.ndarray | tuple)) and len(arg) == n:
+                listlike_args.append(arg)
+            else:
+                listlike_args.append(ListLike(arg))
+
+        listlike_kwargs = {}
+        for k, v in kwargs.items():
+            if isinstance(v, (list | np.ndarray | tuple)) and len(v) == n:
+                listlike_kwargs[k] = v
+            else:
+                listlike_kwargs[k] = ListLike(v)
+
+        agents = []
+        for i in range(n):
+            instance_args = [arg[i] for arg in listlike_args]
+            instance_kwargs = {k: v[i] for k, v in listlike_kwargs.items()}
+            agent = cls(model, *instance_args, **instance_kwargs)
+            agents.append(agent)
+        return AgentSet(agents, random=model.random)
+
     @property
     def random(self) -> Random:
         """Return a seeded stdlib rng."""


Test patch:
diff --git a/tests/test_agent.py b/tests/test_agent.py
index 70183ca07bc..b1ca0ba0eb7 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -170,6 +170,30 @@ def test_agent_rng():
     assert agent.rng is model.rng
 
 
+def test_agent_create():
+    """Test create agent factory method."""
+
+    class TestAgent(Agent):
+        def __init__(self, model, attr, def_attr, a=0, b=0):
+            super().__init__(model)
+            self.some_attribute = attr
+            self.some_default_value = def_attr
+            self.a = a
+            self.b = b
+
+    model = Model(seed=42)
+    n = 10
+    some_attribute = model.rng.random(n)
+    a = tuple([model.random.random() for _ in range(n)])
+    TestAgent.create_agents(model, n, some_attribute, 5, a=a, b=7)
+
+    for agent, value, a_i in zip(model.agents, some_attribute, a):
+        assert agent.some_attribute == value
+        assert agent.some_default_value == 5
+        assert agent.a == a_i
+        assert agent.b == 7
+
+
 def test_agent_add_remove_discard():
     """Test adding, removing and discarding agents from AgentSet."""
     model = Model()