Repository: projectmesa/mesa

Issue:
Batchrunner does not handle exotic input parameters?
My model class takes inputs such as below:

class ExampleModel(Model):
    def __init__(self,
                 seed: int = 0,
                 input_data: Type[pd.DataFrame] = pd.read_csv('InputData.csv'),
                 scenario: str = 'baseline',
                 social_network_parameters: dict = {
                     "average_degree": 10, "rewiring_probability": 0.1},
                 year_period: int = 30,
                 etc. ) -> None:

It would be great if we could have a way to iterate through a list of strings for instance. In my case, the scenario parameter will select part of the Data Frame input based on the scenario string that is given. Ideally, I would like to give batch_run a list such as: ['baseline', 'scenario1', 'scenario2'].

It seems that because the batch_run function wants to launch batches based on iterables it takes the name of my first Data Frame column as an input for the input_data parameter or the first letter of my string (i.e., 'b' in 'baseline' for the example above) for the scenario parameter.

Although it was already not possible to do too many exotic things with input parameters to iterate through in the old BatchRunner classes, at least we could have strings, dictionaries, and so on as input parameters because of the "fixed_parameters" input of the BatchRunner classes. Now it seems batch_run will base its batch of simulations based on any iterable. I am not sure how that could be fixed but I think it could improve Mesa's functionalities. 

PS: I do love the simplicity of the new batch_run function, so I am not advocating going back to the old BatchRunner classes but just improving batch_run.


Discussion:
I found a workaround: treat the batch_run parameters input as a "variable_parameters" input. In other words, only input your variable parameters (e.g., in a list) and have the fixed parameters as default inputs of the class. (In my case I would have "parameters={['baseline', 'scenario1', 'scenario2']}" and I set the other parameters as the default of the model class. But maybe there is a better way?
I think being able to pass in a DataFrame as params to `batch_run` is a useful feature.
Hi @jwalzberg. 
I have the same type of issue, a str passed as a parameter to batch_run is then interpreted character by character, not ideal! 
Some parameters might not be able to be put as default args in the constructor of the model class. Then what is best practice? The same thing goes with @rht 's argument of Dataframes. 
I think these are important additions to the library!

> I have the same type of issue, a str passed as a parameter to batch_run is then interpreted character by character, not ideal!

I actually don't fully understand this problem. Shouldn't it work if you wrap the string inside a list?
Here is an example
```python
from mesa.batchrunner import _make_model_kwargs
print(_make_model_kwargs({"key": "value"}))
print(_make_model_kwargs({"key": ["value"]}))
```
which outputs
```
[{'key': 'v'}, {'key': 'a'}, {'key': 'l'}, {'key': 'u'}, {'key': 'e'}]
[{'key': 'value'}]
```
```
[{'key': 'v'}, {'key': 'a'}, {'key': 'l'}, {'key': 'u'}, {'key': 'e'}]
```
This is definitely a gotcha, but this is something that can be quickly fixed (into Mesa main branch).
With #1289 (just merged), the output will be `[{'key': 'value'}]` instead of `[{'key': 'v'}, {'key': 'a'}, {'key': 'l'}, {'key': 'u'}, {'key': 'e'}]`. However, since the DataFrame as an input is not yet implemented, this issue must remain open.
Thanks a lot @rht !

Is there any timeline for the implementation of DataFrames as inputs?
I'd prioritize implementing #1263 over this issue, but you are welcome to help to implement the DF input.

Resolving patch:
diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py
--- a/mesa/batchrunner.py
+++ b/mesa/batchrunner.py
@@ -15,7 +15,6 @@
 from warnings import warn
 from typing import (
     Any,
-    Counter,
     Dict,
     Iterable,
     List,
@@ -67,7 +66,13 @@ def batch_run(
         [description]
     """
 
-    kwargs_list = _make_model_kwargs(parameters) * iterations
+    runs_list = []
+    run_id = 0
+    for iteration in range(iterations):
+        for kwargs in _make_model_kwargs(parameters):
+            runs_list.append((run_id, iteration, kwargs))
+            run_id += 1
+
     process_func = partial(
         _model_run_func,
         model_cls,
@@ -75,34 +80,19 @@ def batch_run(
         data_collection_period=data_collection_period,
     )
 
-    total_iterations = len(kwargs_list)
-    run_counter = count()
-
     results: List[Dict[str, Any]] = []
 
-    with tqdm(total=total_iterations, disable=not display_progress) as pbar:
-        iteration_counter: Counter[Tuple[Any, ...]] = Counter()
-
-        def _fn(paramValues, rawdata):
-            iteration_counter[paramValues] += 1
-            iteration = iteration_counter[paramValues]
-            run_id = next(run_counter)
-            data = []
-            for run_data in rawdata:
-                out = {"RunId": run_id, "iteration": iteration - 1}
-                out.update(run_data)
-                data.append(out)
-            results.extend(data)
-            pbar.update()
-
+    with tqdm(total=len(runs_list), disable=not display_progress) as pbar:
         if number_processes == 1:
-            for kwargs in kwargs_list:
-                paramValues, rawdata = process_func(kwargs)
-                _fn(paramValues, rawdata)
+            for run in runs_list:
+                data = process_func(run)
+                results.extend(data)
+                pbar.update()
         else:
             with Pool(number_processes) as p:
-                for paramValues, rawdata in p.imap_unordered(process_func, kwargs_list):
-                    _fn(paramValues, rawdata)
+                for data in p.imap_unordered(process_func, runs_list):
+                    results.extend(data)
+                    pbar.update()
 
     return results
 
@@ -140,18 +130,18 @@ def _make_model_kwargs(
 
 def _model_run_func(
     model_cls: Type[Model],
-    kwargs: Dict[str, Any],
+    run: Tuple[int, int, Dict[str, Any]],
     max_steps: int,
     data_collection_period: int,
-) -> Tuple[Tuple[Any, ...], List[Dict[str, Any]]]:
+) -> List[Dict[str, Any]]:
     """Run a single model run and collect model and agent data.
 
     Parameters
     ----------
     model_cls : Type[Model]
         The model class to batch-run
-    kwargs : Dict[str, Any]
-        model kwargs used for this run
+    run: Tuple[int, int, Dict[str, Any]]
+        The run id, iteration number, and kwargs for this run
     max_steps : int
         Maximum number of model steps after which the model halts, by default 1000
     data_collection_period : int
@@ -159,9 +149,10 @@ def _model_run_func(
 
     Returns
     -------
-    Tuple[Tuple[Any, ...], List[Dict[str, Any]]]
+    List[Dict[str, Any]]
         Return model_data, agent_data from the reporters
     """
+    run_id, iteration, kwargs = run
     model = model_cls(**kwargs)
     while model.running and model.schedule.steps <= max_steps:
         model.step()
@@ -178,15 +169,30 @@ def _model_run_func(
         # If there are agent_reporters, then create an entry for each agent
         if all_agents_data:
             stepdata = [
-                {**{"Step": step}, **kwargs, **model_data, **agent_data}
+                {
+                    "RunId": run_id,
+                    "iteration": iteration,
+                    "Step": step,
+                    **kwargs,
+                    **model_data,
+                    **agent_data,
+                }
                 for agent_data in all_agents_data
             ]
         # If there is only model data, then create a single entry for the step
         else:
-            stepdata = [{**{"Step": step}, **kwargs, **model_data}]
+            stepdata = [
+                {
+                    "RunId": run_id,
+                    "iteration": iteration,
+                    "Step": step,
+                    **kwargs,
+                    **model_data,
+                }
+            ]
         data.extend(stepdata)
 
-    return tuple(kwargs.values()), data
+    return data
 
 
 def _collect_data(


Test patch:
diff --git a/tests/test_batch_run.py b/tests/test_batch_run.py
--- a/tests/test_batch_run.py
+++ b/tests/test_batch_run.py
@@ -50,6 +50,7 @@ def __init__(
         fixed_model_param=None,
         schedule=None,
         enable_agent_reporters=True,
+        n_agents=3,
         **kwargs
     ):
         super().__init__()
@@ -57,7 +58,7 @@ def __init__(
         self.variable_model_param = variable_model_param
         self.variable_agent_param = variable_agent_param
         self.fixed_model_param = fixed_model_param
-        self.n_agents = 3
+        self.n_agents = n_agents
         if enable_agent_reporters:
             agent_reporters = {"agent_id": "unique_id", "agent_local": "local"}
         else:
@@ -145,3 +146,52 @@ def test_batch_run_no_agent_reporters():
 
 def test_batch_run_single_core():
     batch_run(MockModel, {}, number_processes=1, iterations=10)
+
+
+def test_batch_run_unhashable_param():
+    result = batch_run(
+        MockModel,
+        {
+            "n_agents": 2,
+            "variable_model_params": [{"key": "value"}],
+        },
+        iterations=2,
+    )
+    template = {
+        "Step": 1000,
+        "reported_model_param": 42,
+        "agent_local": 250.0,
+        "n_agents": 2,
+        "variable_model_params": {"key": "value"},
+    }
+
+    assert result == [
+        {
+            "RunId": 0,
+            "iteration": 0,
+            "AgentID": 0,
+            "agent_id": 0,
+            **template,
+        },
+        {
+            "RunId": 0,
+            "iteration": 0,
+            "AgentID": 1,
+            "agent_id": 1,
+            **template,
+        },
+        {
+            "RunId": 1,
+            "iteration": 1,
+            "AgentID": 0,
+            "agent_id": 0,
+            **template,
+        },
+        {
+            "RunId": 1,
+            "iteration": 1,
+            "AgentID": 1,
+            "agent_id": 1,
+            **template,
+        },
+    ]