@@ -1061,13 +1061,21 @@ def move_to(
10611061 randomized order to break ties. The final position of each agent is then
10621062 updated in-place.
10631063
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+
10641072 Parameters
10651073 ----------
10661074 agents : AgentLike
10671075 A DataFrame-like structure containing agent information. Must include
10681076 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``)
10711079 - Optionally ``vision`` if ``radius`` is not provided
10721080 attr_names : str or list of str
10731081 The name(s) of the attribute(s) used for ranking the neighborhood cells.
@@ -1079,7 +1087,8 @@ def move_to(
10791087 - ``"min"`` for ascending order
10801088
10811089 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``.
10831092 radius : int or pl.Series, optional
10841093 The radius (or per-agent radii) defining the neighborhood around agents.
10851094 If not provided, this method attempts to use the ``vision`` column from
@@ -1095,7 +1104,52 @@ def move_to(
10951104 -------
10961105 None
10971106 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.
10981151 """
1152+
10991153 # Ensure attr_names and rank_order are lists of the same length
11001154 if isinstance (attr_names , str ):
11011155 attr_names = [attr_names ]
@@ -1121,18 +1175,13 @@ def move_to(
11211175 )
11221176 neighborhood = neighborhood .join (self .cells , on = ["dim_0" , "dim_1" ])
11231177
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)
11341179 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" ]
11361185 )
11371186
11381187 # Determine the processing order of agents
@@ -1180,43 +1229,30 @@ def move_to(
11801229
11811230 # Iteratively select the best moves
11821231 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 ):
11891233 # Count how many times each (dim_0, dim_1) is being claimed
11901234 neighborhood = neighborhood .with_columns (
11911235 priority = pl .col ("agent_order" ).cum_count ().over (["dim_0" , "dim_1" ])
11921236 )
1193-
11941237 new_best_moves = (
11951238 neighborhood .group_by ("agent_id_center" , maintain_order = True )
11961239 .first ()
11971240 .unique (subset = ["dim_0" , "dim_1" ], keep = "first" , maintain_order = True )
11981241 )
1199-
12001242 condition = (
12011243 pl .col ("blocking_agent_id" ).is_null ()
12021244 | (pl .col ("blocking_agent_id" ) == pl .col ("agent_id_center" ))
12031245 )
1204-
12051246 if len (best_moves ) > 0 :
12061247 condition = condition | pl .col ("blocking_agent_id" ).is_in (
12071248 best_moves ["agent_id_center" ]
12081249 )
1209-
12101250 condition = condition & (pl .col ("priority" ) == 1 )
12111251 new_best_moves = new_best_moves .filter (condition )
1212-
12131252 if len (new_best_moves ) == 0 :
12141253 break
1215-
1254+
12161255 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
12201256 neighborhood = neighborhood .filter (
12211257 ~ pl .col ("agent_id_center" ).is_in (best_moves ["agent_id_center" ])
12221258 )
@@ -1226,20 +1262,11 @@ def move_to(
12261262
12271263 # Move agents to their new positions
12281264 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+
12431270
12441271
12451272
0 commit comments