@@ -1061,13 +1061,21 @@ def move_to(
1061
1061
randomized order to break ties. The final position of each agent is then
1062
1062
updated in-place.
1063
1063
1064
+ In practice, this method leverages Polars for efficient DataFrame operations.
1065
+ For very large models or specialized data structures (like ``AgentContainer``),
1066
+ consider referencing the minimal UDP approach in
1067
+ ``space/utils/spatial_utils/grid_pairs.py``. You can adapt similar logic
1068
+ (e.g., using lazy frames or partial computations) to maintain performance
1069
+ at scale. If you observe performance degradation for large neighborhoods,
1070
+ you may need to refactor or optimize further.
1071
+
1064
1072
Parameters
1065
1073
----------
1066
1074
agents : AgentLike
1067
1075
A DataFrame-like structure containing agent information. Must include
1068
1076
at least the following columns:
1069
- - ``agent_id ``: a unique identifier for each agent
1070
- - ``dim_0``, ``dim_1``: the current positions of agents
1077
+ - ``unique_id ``: a unique identifier for each agent
1078
+ - ``dim_0``, ``dim_1``: the current positions of agents (in ``agents.pos``)
1071
1079
- Optionally ``vision`` if ``radius`` is not provided
1072
1080
attr_names : str or list of str
1073
1081
The name(s) of the attribute(s) used for ranking the neighborhood cells.
@@ -1079,7 +1087,8 @@ def move_to(
1079
1087
- ``"min"`` for ascending order
1080
1088
1081
1089
If a single string is provided, it is applied to all attributes in
1082
- ``attr_names``.
1090
+ ``attr_names``. **Note**: this method strongly assumes that the length
1091
+ of ``attr_names`` matches the length of ``rank_order``.
1083
1092
radius : int or pl.Series, optional
1084
1093
The radius (or per-agent radii) defining the neighborhood around agents.
1085
1094
If not provided, this method attempts to use the ``vision`` column from
@@ -1095,7 +1104,52 @@ def move_to(
1095
1104
-------
1096
1105
None
1097
1106
This method updates agent positions in-place based on the computed best moves.
1107
+
1108
+ Raises
1109
+ ------
1110
+ ValueError
1111
+ If the lengths of ``attr_names`` and ``rank_order`` do not match, or if
1112
+ ``radius`` is not provided and the ``agents`` DataFrame does not contain
1113
+ a ``vision`` attribute.
1114
+
1115
+ Notes
1116
+ -----
1117
+ - Type definitions (e.g., ``AgentLike``) are typically maintained in
1118
+ ``mesa_frames.types``.
1119
+ - For advanced usage or adapting this method to a custom data structure
1120
+ (e.g., an ``AgentContainer``), you may reference the logic in
1121
+ ``space/utils/spatial_utils/grid_pairs.py`` for a minimal UDP approach.
1122
+ Using lazy frames or partial computations might help if performance
1123
+ scales poorly with large data sets.
1124
+
1125
+ This method performs the following steps:
1126
+ 1. Compute the neighborhood for each agent using ``self.get_neighborhood``.
1127
+ 2. Join the neighborhood data with cell information (``self.cells``) and agent
1128
+ positions (from ``agents.pos``).
1129
+ 3. Optionally shuffle the agent order to break ties.
1130
+ 4. Sort the neighborhood cells by the specified attribute(s) and order(s).
1131
+ 5. Iteratively select the best moves, ensuring no cell is claimed by multiple agents.
1132
+ 6. Call ``self.move_agents`` to finalize the updated positions of each agent.
1133
+
1134
+ Examples
1135
+ --------
1136
+ >>> # Assume we have a DataFrame 'agents' with columns:
1137
+ >>> # ['unique_id', 'dim_0', 'dim_1', 'vision', 'food_availability', 'safety_score']
1138
+ >>> # and a space object 'space' in a mesa-frames model.
1139
+ >>>
1140
+ >>> # We want to move each agent to the best available cell within its vision
1141
+ >>> # radius, prioritizing cells with higher 'food_availability' and 'safety_score'.
1142
+ >>> space.move_to(
1143
+ ... agents=agents,
1144
+ ... attr_names=["food_availability", "safety_score"],
1145
+ ... rank_order=["max", "max"], # rank both attributes in descending order
1146
+ ... radius=None, # use each agent's 'vision' column
1147
+ ... include_center=False, # do not include the agent's current cell
1148
+ ... shuffle=True # randomize the order in which agents move
1149
+ ... )
1150
+ >>> # After this call, each agent's position in 'agents' will be updated in-place.
1098
1151
"""
1152
+
1099
1153
# Ensure attr_names and rank_order are lists of the same length
1100
1154
if isinstance (attr_names , str ):
1101
1155
attr_names = [attr_names ]
@@ -1121,18 +1175,13 @@ def move_to(
1121
1175
)
1122
1176
neighborhood = neighborhood .join (self .cells , on = ["dim_0" , "dim_1" ])
1123
1177
1124
- # Determine the agent identifier column
1125
- agent_id_col = "agent_id" if "agent_id" in agents .columns else "unique_id"
1126
-
1127
- # Add a column to identify the center agent
1128
- join_result = neighborhood .join (
1129
- agents .select (["dim_0" , "dim_1" , agent_id_col ]),
1130
- left_on = ["dim_0_center" , "dim_1_center" ],
1131
- right_on = ["dim_0" , "dim_1" ]
1132
- )
1133
-
1178
+ # Add a column to identify the center agent (the one evaluating moves)
1134
1179
neighborhood = neighborhood .with_columns (
1135
- agent_id_center = join_result [agent_id_col ]
1180
+ agent_id_center = neighborhood .join (
1181
+ agents .pos ,
1182
+ left_on = ["dim_0_center" , "dim_1_center" ],
1183
+ right_on = ["dim_0" , "dim_1" ],
1184
+ )["unique_id" ]
1136
1185
)
1137
1186
1138
1187
# Determine the processing order of agents
@@ -1180,43 +1229,30 @@ def move_to(
1180
1229
1181
1230
# Iteratively select the best moves
1182
1231
best_moves = pl .DataFrame ()
1183
- max_iterations = min (len (agents ) * 2 , 1000 ) # Safeguard against infinite loops
1184
- iteration_count = 0
1185
-
1186
- while len (best_moves ) < len (agents ) and iteration_count < max_iterations :
1187
- iteration_count += 1
1188
-
1232
+ while len (best_moves ) < len (agents ):
1189
1233
# Count how many times each (dim_0, dim_1) is being claimed
1190
1234
neighborhood = neighborhood .with_columns (
1191
1235
priority = pl .col ("agent_order" ).cum_count ().over (["dim_0" , "dim_1" ])
1192
1236
)
1193
-
1194
1237
new_best_moves = (
1195
1238
neighborhood .group_by ("agent_id_center" , maintain_order = True )
1196
1239
.first ()
1197
1240
.unique (subset = ["dim_0" , "dim_1" ], keep = "first" , maintain_order = True )
1198
1241
)
1199
-
1200
1242
condition = (
1201
1243
pl .col ("blocking_agent_id" ).is_null ()
1202
1244
| (pl .col ("blocking_agent_id" ) == pl .col ("agent_id_center" ))
1203
1245
)
1204
-
1205
1246
if len (best_moves ) > 0 :
1206
1247
condition = condition | pl .col ("blocking_agent_id" ).is_in (
1207
1248
best_moves ["agent_id_center" ]
1208
1249
)
1209
-
1210
1250
condition = condition & (pl .col ("priority" ) == 1 )
1211
1251
new_best_moves = new_best_moves .filter (condition )
1212
-
1213
1252
if len (new_best_moves ) == 0 :
1214
1253
break
1215
-
1254
+
1216
1255
best_moves = pl .concat ([best_moves , new_best_moves ])
1217
-
1218
- # Update neighborhood to exclude agents that already have a move
1219
- # and cells that are already claimed
1220
1256
neighborhood = neighborhood .filter (
1221
1257
~ pl .col ("agent_id_center" ).is_in (best_moves ["agent_id_center" ])
1222
1258
)
@@ -1226,20 +1262,11 @@ def move_to(
1226
1262
1227
1263
# Move agents to their new positions
1228
1264
if len (best_moves ) > 0 :
1229
- try :
1230
- self .move_agents (
1231
- best_moves .sort ("agent_order" )["agent_id_center" ],
1232
- best_moves .sort ("agent_order" ).select (["dim_0" , "dim_1" ])
1233
- )
1234
- except Exception as e :
1235
- # Check if the agent exists in the model
1236
- available_agents = set (self .model .agents [agent_id_col ].to_list ()) if hasattr (self .model , 'agents' ) else set ()
1237
- missing_agents = [a for a in best_moves ["agent_id_center" ].to_list () if a not in available_agents ]
1238
-
1239
- if missing_agents and available_agents :
1240
- raise ValueError (f"Some agents are not present in the model: { missing_agents } " )
1241
- else :
1242
- raise ValueError (f"Error moving agents: { e } " )
1265
+ self .move_agents (
1266
+ best_moves .sort ("agent_order" )["agent_id_center" ],
1267
+ best_moves .sort ("agent_order" ).select (["dim_0" , "dim_1" ])
1268
+ )
1269
+
1243
1270
1244
1271
1245
1272
0 commit comments