@@ -59,7 +59,6 @@ def __init__(self, model, dimensions, torus, capacity, neighborhood_type):
59
59
from typing_extensions import Any , Self
60
60
61
61
62
- from mesa_frames .concrete .polars .agentset import AgentSetPolars
63
62
from mesa_frames .concrete .agents import AgentsDF
64
63
from mesa_frames .abstract .agents import AgentContainer , AgentSetDF
65
64
from mesa_frames .abstract .mixin import CopyMixin , DataFrameMixin
@@ -77,12 +76,13 @@ def __init__(self, model, dimensions, torus, capacity, neighborhood_type):
77
76
Series ,
78
77
SpaceCoordinate ,
79
78
SpaceCoordinates ,
79
+ AgentLike
80
80
)
81
81
82
82
ESPG = int
83
83
84
84
85
- AgentLike = Union [ AgentSetPolars , pl . DataFrame ]
85
+
86
86
87
87
if TYPE_CHECKING :
88
88
from mesa_frames .concrete .model import ModelDF
@@ -1050,36 +1050,98 @@ def move_to(
1050
1050
include_center : bool = True ,
1051
1051
shuffle : bool = True
1052
1052
) -> None :
1053
+ """
1054
+ Move agents to new positions based on neighborhood ranking.
1055
+
1056
+ This method determines each agent's potential moves by computing their
1057
+ local neighborhood (with optional inclusion of the center cell). For each
1058
+ agent, the method ranks all possible moves according to specified attribute(s)
1059
+ and rank order(s). If multiple agents contend for the same cell, the method
1060
+ applies a tie-breaking approach. Agents can optionally be processed in a
1061
+ randomized order to break ties. The final position of each agent is then
1062
+ updated in-place.
1063
+
1064
+ Parameters
1065
+ ----------
1066
+ agents : AgentLike
1067
+ A DataFrame-like structure containing agent information. Must include
1068
+ at least the following columns:
1069
+ - ``agent_id``: a unique identifier for each agent
1070
+ - ``dim_0``, ``dim_1``: the current positions of agents
1071
+ - Optionally ``vision`` if ``radius`` is not provided
1072
+ attr_names : str or list of str
1073
+ The name(s) of the attribute(s) used for ranking the neighborhood cells.
1074
+ If multiple attributes are provided, each should have a corresponding
1075
+ entry in ``rank_order``.
1076
+ rank_order : str or list of str, optional
1077
+ The ranking order for each attribute. Accepts:
1078
+ - ``"max"`` (default) for descending order
1079
+ - ``"min"`` for ascending order
1080
+
1081
+ If a single string is provided, it is applied to all attributes in
1082
+ ``attr_names``.
1083
+ radius : int or pl.Series, optional
1084
+ The radius (or per-agent radii) defining the neighborhood around agents.
1085
+ If not provided, this method attempts to use the ``vision`` column from
1086
+ ``agents``. If ``vision`` is not found, a ``ValueError`` is raised.
1087
+ include_center : bool, optional
1088
+ If ``True`` (default), the agent's current position is included in its
1089
+ neighborhood.
1090
+ shuffle : bool, optional
1091
+ If ``True`` (default), the order of agents is randomized to break ties.
1092
+ If ``False``, agents are processed in the order they appear in the data.
1093
+
1094
+ Returns
1095
+ -------
1096
+ None
1097
+ This method updates agent positions in-place based on the computed best moves.
1098
+ """
1099
+ # Ensure attr_names and rank_order are lists of the same length
1053
1100
if isinstance (attr_names , str ):
1054
1101
attr_names = [attr_names ]
1055
1102
if isinstance (rank_order , str ):
1056
1103
rank_order = [rank_order ] * len (attr_names )
1057
1104
if len (attr_names ) != len (rank_order ):
1058
1105
raise ValueError ("attr_names and rank_order must have the same length" )
1106
+
1107
+ # Handle the neighborhood radius
1059
1108
if radius is None :
1060
1109
if "vision" in agents .columns :
1061
1110
radius = agents ["vision" ]
1062
1111
else :
1063
- raise ValueError ("radius must be specified if agents do not have a 'vision' attribute" )
1112
+ raise ValueError (
1113
+ "radius must be specified if agents do not have a 'vision' attribute"
1114
+ )
1115
+
1116
+ # Get neighborhood and join with cell information
1064
1117
neighborhood = self .get_neighborhood (
1065
- radius = radius ,
1066
- agents = agents ,
1118
+ radius = radius ,
1119
+ agents = agents ,
1067
1120
include_center = include_center
1068
1121
)
1069
1122
neighborhood = neighborhood .join (self .cells , on = ["dim_0" , "dim_1" ])
1123
+
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
+
1070
1134
neighborhood = neighborhood .with_columns (
1071
- agent_id_center = neighborhood .join (
1072
- agents .pos ,
1073
- left_on = ["dim_0_center" , "dim_1_center" ],
1074
- right_on = ["dim_0" , "dim_1" ],
1075
- )["unique_id" ]
1135
+ agent_id_center = join_result [agent_id_col ]
1076
1136
)
1137
+
1138
+ # Determine the processing order of agents
1077
1139
if shuffle :
1078
1140
agent_order = (
1079
1141
neighborhood
1080
1142
.unique (subset = ["agent_id_center" ], keep = "first" )
1081
1143
.select ("agent_id_center" )
1082
- .sample (fraction = 1.0 , seed = self .model .random .integers (0 , 2 ** 31 - 1 ))
1144
+ .sample (fraction = 1.0 , seed = self .model .random .integers (0 , 2 ** 31 - 1 ))
1083
1145
.with_row_index ("agent_order" )
1084
1146
)
1085
1147
else :
@@ -1089,16 +1151,24 @@ def move_to(
1089
1151
.with_row_index ("agent_order" )
1090
1152
.select (["agent_id_center" , "agent_order" ])
1091
1153
)
1154
+
1155
+ # Join the processing order with the neighborhood
1092
1156
neighborhood = neighborhood .join (agent_order , on = "agent_id_center" )
1157
+
1158
+ # Prepare sorting columns and order
1093
1159
sort_cols = []
1094
1160
sort_desc = []
1095
1161
for attr , order in zip (attr_names , rank_order ):
1096
1162
sort_cols .append (attr )
1097
1163
sort_desc .append (order .lower () == "max" )
1164
+
1165
+ # Sort the neighborhood cells by specified attributes and then by location
1098
1166
neighborhood = neighborhood .sort (
1099
1167
sort_cols + ["radius" , "dim_0" , "dim_1" ],
1100
1168
descending = sort_desc + [False , False , False ]
1101
1169
)
1170
+
1171
+ # Join to track if another agent has blocked a cell
1102
1172
neighborhood = neighborhood .join (
1103
1173
agent_order .select (
1104
1174
pl .col ("agent_id_center" ).alias ("agent_id" ),
@@ -1107,39 +1177,71 @@ def move_to(
1107
1177
on = "agent_id" ,
1108
1178
how = "left" ,
1109
1179
).rename ({"agent_id" : "blocking_agent_id" })
1180
+
1181
+ # Iteratively select the best moves
1110
1182
best_moves = pl .DataFrame ()
1111
- while len (best_moves ) < len (agents ):
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
+
1189
+ # Count how many times each (dim_0, dim_1) is being claimed
1112
1190
neighborhood = neighborhood .with_columns (
1113
1191
priority = pl .col ("agent_order" ).cum_count ().over (["dim_0" , "dim_1" ])
1114
1192
)
1193
+
1115
1194
new_best_moves = (
1116
1195
neighborhood .group_by ("agent_id_center" , maintain_order = True )
1117
1196
.first ()
1118
1197
.unique (subset = ["dim_0" , "dim_1" ], keep = "first" , maintain_order = True )
1119
1198
)
1120
- condition = pl .col ("blocking_agent_id" ).is_null () | (
1121
- pl .col ("blocking_agent_id" ) == pl .col ("agent_id_center" )
1199
+
1200
+ condition = (
1201
+ pl .col ("blocking_agent_id" ).is_null ()
1202
+ | (pl .col ("blocking_agent_id" ) == pl .col ("agent_id_center" ))
1122
1203
)
1204
+
1123
1205
if len (best_moves ) > 0 :
1124
1206
condition = condition | pl .col ("blocking_agent_id" ).is_in (
1125
1207
best_moves ["agent_id_center" ]
1126
1208
)
1209
+
1127
1210
condition = condition & (pl .col ("priority" ) == 1 )
1128
1211
new_best_moves = new_best_moves .filter (condition )
1212
+
1129
1213
if len (new_best_moves ) == 0 :
1130
1214
break
1215
+
1131
1216
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
1132
1220
neighborhood = neighborhood .filter (
1133
1221
~ pl .col ("agent_id_center" ).is_in (best_moves ["agent_id_center" ])
1134
1222
)
1135
1223
neighborhood = neighborhood .join (
1136
1224
best_moves .select (["dim_0" , "dim_1" ]), on = ["dim_0" , "dim_1" ], how = "anti"
1137
1225
)
1226
+
1227
+ # Move agents to their new positions
1138
1228
if len (best_moves ) > 0 :
1139
- self .move_agents (
1140
- best_moves .sort ("agent_order" )["agent_id_center" ],
1141
- best_moves .sort ("agent_order" ).select (["dim_0" , "dim_1" ])
1142
- )
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 } " )
1243
+
1244
+
1143
1245
1144
1246
1145
1247
@property
0 commit comments