33import math
44from typing import List , Optional
55
6+ import custom_interfaces .msg as ci
67from rclpy .impl .rcutils_logger import RcutilsLogger
78from shapely .geometry import LineString , MultiPolygon
89
910import local_pathfinding .coord_systems as cs
10- import custom_interfaces .msg as ci
1111import local_pathfinding .obstacles as ob
1212from local_pathfinding .ompl_path import OMPLPath
1313
@@ -46,6 +46,34 @@ def __init__(
4646 filtered_wind_sensor : ci .WindSensor ,
4747 planner : str ,
4848 ):
49+ self .update_state (gps , ais_ships , filtered_wind_sensor )
50+
51+ if not (global_path and global_path .waypoints ):
52+ raise ValueError ("Cannot create a LocalPathState with an empty global_path" )
53+ self .global_path = global_path
54+ self .reference_latlon = target_global_waypoint
55+
56+ if not planner :
57+ raise ValueError ("planner must not be None" )
58+ self .planner = planner
59+
60+ # obstacles are initialized by OMPLPath right before solving
61+ self .obstacles : List [ob .Obstacle ] = []
62+
63+ def update_state (
64+ self , gps : ci .GPS , ais_ships : ci .AISShips , filtered_wind_sensor : ci .WindSensor
65+ ):
66+ """Updates the changeable environment without changing the path or reference_latlon
67+
68+ This method updates only the dynamic state variables (position, heading, speed,
69+ ais_ships, wind) without changing the reference coordinate system or global path.
70+ Used when continuing on an existing path to avoid coordinate system mismatches.
71+
72+ Args:
73+ gps (ci.GPS): Current GPS position and heading data
74+ ais_ships (ci.AISShips): Updated AIS ship data
75+ filtered_wind_sensor (ci.WindSensor): Updated wind sensor data
76+ """
4977 if not gps :
5078 raise ValueError ("gps must not be None" )
5179 self .position = gps .lat_lon
@@ -61,27 +89,15 @@ def __init__(
6189 self .wind_speed = filtered_wind_sensor .speed .speed
6290 self .wind_direction = filtered_wind_sensor .direction
6391
64- if not (global_path and global_path .waypoints ):
65- raise ValueError ("Cannot create a LocalPathState with an empty global_path" )
66- self .global_path = global_path
67- self .reference_latlon = target_global_waypoint
68-
69- if not planner :
70- raise ValueError ("planner must not be None" )
71- self .planner = planner
72-
73- # obstacles are initialized by OMPLPath right before solving
74- self .obstacles : List [ob .Obstacle ] = []
75-
7692
7793class LocalPath :
7894 """Sets and updates the OMPL path and the local waypoints
7995
8096 Attributes:
8197 _logger (RcutilsLogger): ROS logger.
8298 _ompl_path (Optional[OMPLPath]): Raw representation of the path from OMPL.
83- _waypoint_index: Local waypoint index (i.e. pointer to the next local waypoint that the
84- boat is following)
99+ _prev_lp_wp_index (int): index of the local waypoint that Polaris has already traversed in
100+ the path array
85101 path (Path): Collection of coordinates that form the local path to the next
86102 global waypoint.
87103 state (LocalPathState): the current local path state.
@@ -94,16 +110,17 @@ def __init__(self, parent_logger: RcutilsLogger):
94110 self .state : Optional [LocalPathState ] = None
95111
96112 @staticmethod
97- def calculate_desired_heading_and_waypoint_index (
113+ def calculate_desired_heading_and_wp_index (
98114 path : ci .Path , waypoint_index : int , boat_lat_lon : ci .HelperLatLon
99115 ):
100116 """Calculates the desired heading using GEODESIC. Updates the waypoint index (i.e. change
101117 the waypoint) if the boat is close enough to the current waypoint.
102118
103119 Args:
104120 path (ci.Path): Array of waypoints
105- waypoint_index (int): Pointer to the current local waypoint index in path array
106- (i.e. the waypoint sailbot is heading towards)
121+ prev_lp_wp_index (int): index of the local waypoint that Polaris has already traversed
122+ in the path array
123+ (i.e. the waypoint sailbot traversed, sailbot is heading towards waypoint_index + 1)
107124 boat_lat_lon (ci.HelperLatLon): boat coordinates
108125
109126 Returns:
@@ -162,7 +179,7 @@ def update_if_needed(
162179 gps : ci .GPS ,
163180 ais_ships : ci .AISShips ,
164181 global_path : ci .Path ,
165- local_waypoint_index : int ,
182+ prev_lp_wp_index : int ,
166183 received_new_global_waypoint : bool ,
167184 target_global_waypoint : ci .HelperLatLon ,
168185 filtered_wind_sensor : ci .WindSensor ,
@@ -184,7 +201,9 @@ def update_if_needed(
184201 gps (ci.GPS): Current GPS position and heading data.
185202 ais_ships (ci.AISShips): AIS data for nearby ships (obstacles).
186203 global_path (ci.Path): The global path plan to the destination.
187- local_waypoint_index (int): Current index in the local waypoint list.
204+ prev_lp_wp_index (int): Current index in the local waypoint list.
205+ This is the index that the boat last traversed. The boat is heading towards
206+ the index following prev_lp_wp_index
188207 received_new_global_waypoint (bool): Flag indicating if a new global
189208 waypoint was received.
190209 target_global_waypoint (ci.HelperLatLon): Target waypoint from global path.
@@ -199,53 +218,71 @@ def update_if_needed(
199218 - Updated waypoint index
200219 The method decides whether to return the heading for new path or old path
201220 """
202- # this raises ValueError if any of the parameters are not properly initialized
203- self ._waypoint_index = local_waypoint_index
204- state = LocalPathState (
221+ self ._prev_lp_wp_index = prev_lp_wp_index
222+ old_ompl_path = self ._ompl_path
223+
224+ if (
225+ (received_new_global_waypoint or old_ompl_path is None )
226+ or (self .path is None )
227+ or (self .state is None )
228+ ):
229+ new_state = LocalPathState (
230+ gps , ais_ships , global_path , target_global_waypoint , filtered_wind_sensor , planner
231+ )
232+ new_ompl_path = OMPLPath (
233+ parent_logger = self ._logger ,
234+ local_path_state = new_state ,
235+ land_multi_polygon = land_multi_polygon ,
236+ )
237+ heading_new_path , wp_index = self .calculate_desired_heading_and_wp_index (
238+ new_ompl_path .get_path (), 0 , gps .lat_lon
239+ )
240+ if received_new_global_waypoint :
241+ self ._logger .debug ("Updating local path because we have a new global waypoint" )
242+ else :
243+ self ._logger .debug ("old path is None" )
244+ self .state = new_state
245+ self ._update (new_ompl_path )
246+ return heading_new_path , wp_index
247+ else :
248+ self .state .update_state (gps , ais_ships , filtered_wind_sensor )
249+
250+ # create a new state with most up-to-date environment information. This is done to compare
251+ # a new OMPL path based on this state with the OMPL path that we have been using. Since the
252+ # paths are based on environment snapshots of when the path was created, this is important
253+ # to ensure that we are not on a bad path.
254+ new_state = LocalPathState (
205255 gps , ais_ships , global_path , target_global_waypoint , filtered_wind_sensor , planner
206256 )
207- self .state = state
208- ompl_path = OMPLPath (
257+ new_ompl_path = OMPLPath (
209258 parent_logger = self ._logger ,
210- local_path_state = state ,
259+ local_path_state = new_state ,
211260 land_multi_polygon = land_multi_polygon ,
212261 )
213- old_ompl_path = self ._ompl_path
214262
215- heading_new_path , wp_index = self .calculate_desired_heading_and_waypoint_index (
216- ompl_path .get_path (), 0 , gps .lat_lon
263+ heading_new_path , wp_index = self .calculate_desired_heading_and_wp_index (
264+ new_ompl_path .get_path (), 0 , gps .lat_lon
217265 )
218-
219- if received_new_global_waypoint :
220- self ._logger .debug ("Updating local path because we have a new global waypoint" )
221- self ._update (ompl_path )
222- return heading_new_path , wp_index
223-
224- if old_ompl_path is None or self .path is None :
225- # continue on the same path
226- self ._logger .debug ("old path is none" )
227- self ._update (ompl_path )
228- return heading_new_path , wp_index
229-
230- heading_old_path , updated_wp_index = self .calculate_desired_heading_and_waypoint_index (
231- old_ompl_path .get_path (), local_waypoint_index , gps .lat_lon
266+ heading_old_path , updated_wp_index = self .calculate_desired_heading_and_wp_index (
267+ old_ompl_path .get_path (), prev_lp_wp_index , gps .lat_lon
232268 )
233- # check if the current path goes through a collision zone.
234- # No need to check for new path since it's fresh and ompl doesn't generate path that
235- # go through a collision zone
269+
236270 if self .in_collision_zone (
237- local_waypoint_index , self .state .reference_latlon , self .path , self .state .obstacles
271+ prev_lp_wp_index , self .state .reference_latlon , self .path , self .state .obstacles
238272 ):
239273 self ._logger .debug ("old path is in collision zone" )
240- self ._update (ompl_path )
274+ self .state = new_state
275+ self ._update (new_ompl_path )
241276 return heading_new_path , wp_index
242277
243278 heading_diff_old_path = cs .calculate_heading_diff (self .state .heading , heading_old_path )
244279 heading_diff_new_path = cs .calculate_heading_diff (self .state .heading , heading_new_path )
245280
246- old_cost = old_ompl_path . get_cost (updated_wp_index )
247- new_cost = ompl_path . get_cost (wp_index )
281+ old_prev_lp_wp_index = max (updated_wp_index - 1 , 0 )
282+ new_prev_lp_wp_index = max (wp_index - 1 , 0 )
248283
284+ old_cost = old_ompl_path .get_remaining_cost (old_prev_lp_wp_index , gps .lat_lon )
285+ new_cost = new_ompl_path .get_remaining_cost (new_prev_lp_wp_index , gps .lat_lon )
249286 max_cost = max (old_cost , new_cost , 1 )
250287 old_cost_normalized = old_cost / max_cost
251288 new_cost_normalized = new_cost / max_cost
@@ -264,24 +301,21 @@ def update_if_needed(
264301 metric_new = w_h * heading_diff_new_normalized + w_c * new_cost_normalized
265302
266303 self ._logger .debug (
267- f"(old cost: { old_cost :.2f} , "
268- f"new cost: { new_cost :.2f} )"
269- f", metric_old: { metric_old :.2f} , "
270- f"metric_new: { metric_new :.2f} , "
271- f"old_cost_normalized: { old_cost_normalized :.2f} , "
272- f"new_cost_normalized: { new_cost_normalized :.2f} "
273- )
304+ f"(old cost: { old_cost :.2f} , "
305+ f"new cost: { new_cost :.2f} )"
306+ f", metric_old: { metric_old :.2f} , "
307+ f"metric_new: { metric_new :.2f} , "
308+ f"old_cost_normalized: { old_cost_normalized :.2f} , "
309+ f"new_cost_normalized: { new_cost_normalized :.2f} "
310+ )
274311 if metric_new < metric_old :
275- self ._logger .debug (
276- "New path is cheaper, updating local path "
277- )
278- self ._update (ompl_path )
312+ self ._logger .debug ("New path is cheaper, updating local path " )
313+ self .state = new_state
314+ self ._update (new_ompl_path )
279315 return heading_new_path , wp_index
280316 else :
281- self ._logger .debug (
282- "old path is cheaper, continuing on the same path"
283- )
284- return heading_old_path , wp_index
317+ self ._logger .debug ("old path is cheaper, continuing on the same path" )
318+ return heading_old_path , updated_wp_index
285319
286320 def _update (self , ompl_path : OMPLPath ):
287321
0 commit comments