Skip to content

Commit d412813

Browse files
raghumanimehtaCopilotSPDonaghy
authored
Raghumanimehta/778 get cost (#798)
* disabled zoom * resets the state space to the default range of the plot * reset button location changed * changed some weights * get_remaining_cost implemented * basic test added * added some more tests, failing still * the partial tests are failing * index errors * still broken, will use mocks now * no partial tests are passing * all tests passed * added a projection based test * linting * Update src/local_pathfinding/test/test_ompl_path.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/local_pathfinding/local_pathfinding/local_path.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * added back setCostThreshold in ompl_objectives.py * local_path.py, update_if_needed docstring change * variable change * removed simplifySolution * linting error fix * added a factor to force inclusivity * Update src/local_pathfinding/test/test_ompl_path.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * updated docus * LocalPathState constructor now calls update_state method * Update src/local_pathfinding/local_pathfinding/local_path.py Co-authored-by: Sean Donaghy <118148642+SPDonaghy@users.noreply.github.com> * last -> prev * comment fixes * comments updated * Update src/local_pathfinding/local_pathfinding/ompl_path.py Co-authored-by: Sean Donaghy <118148642+SPDonaghy@users.noreply.github.com> * Fix formatting issues in docstring of get_remaining_cost * Clean up comment in OMPL path calculation Remove unnecessary comment about degenerate segment. * imporved comment + structure of the funciton * comment changes * removed pointer terminology wherever required --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sean Donaghy <118148642+SPDonaghy@users.noreply.github.com>
1 parent ea03e37 commit d412813

File tree

6 files changed

+381
-95
lines changed

6 files changed

+381
-95
lines changed

src/local_pathfinding/local_pathfinding/local_path.py

Lines changed: 99 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import math
44
from typing import List, Optional
55

6+
import custom_interfaces.msg as ci
67
from rclpy.impl.rcutils_logger import RcutilsLogger
78
from shapely.geometry import LineString, MultiPolygon
89

910
import local_pathfinding.coord_systems as cs
10-
import custom_interfaces.msg as ci
1111
import local_pathfinding.obstacles as ob
1212
from 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

7793
class 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

src/local_pathfinding/local_pathfinding/node_navigate.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class Sailbot(Node):
5252
global_path (ci.Path): Path that we are following.
5353
filtered_wind_sensor (ci.WindSensor): Filtered data from the wind sensors.
5454
desired_heading (ci.DesiredHeading): current desired heading.
55+
prev_lp_wp_index: local waypoint that Polaris has already traversed/seen. Polaris is moving
56+
towards prev_lp_wp_index + 1 waypoint.
5557
5658
Attributes:
5759
local_path (LocalPath): The path that `Sailbot` is following.
@@ -124,7 +126,7 @@ def __init__(self):
124126

125127
# attributes
126128
self.local_path = LocalPath(parent_logger=self.get_logger())
127-
self.local_waypoint_index = 0
129+
self.prev_lp_wp_index = 0
128130
self.global_waypoint_index = -1
129131
self.saved_target_global_waypoint = None
130132
self.use_mock_land = self.get_parameter("use_mock_land").get_parameter_value().bool_value
@@ -292,11 +294,11 @@ def get_desired_heading(self) -> float:
292294
self.global_waypoint_index
293295
]
294296

295-
desired_heading, self.local_waypoint_index = self.local_path.update_if_needed(
297+
desired_heading, self.prev_lp_wp_index = self.local_path.update_if_needed(
296298
self.gps,
297299
self.ais_ships,
298300
self.global_path,
299-
self.local_waypoint_index,
301+
self.prev_lp_wp_index,
300302
received_new_global_waypoint,
301303
self.saved_target_global_waypoint,
302304
self.filtered_wind_sensor,

src/local_pathfinding/local_pathfinding/ompl_objectives.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
UPWIND_COST_MULTIPLIER = 1.0
1313
DOWNWIND_COST_MULTIPLIER = 1.0
1414
ZERO_SPEED_COST = 1.0
15-
ACCEPTABLE_COST_THRESHOLD = 0.0
16-
WIND_OBJECTIVE_WEIGHT = 1.0
17-
TIME_OBJECTIVE_WEIGHT = 1.0
15+
ACCEPTABLE_COST_THRESHOLD = 0.85
16+
WIND_OBJECTIVE_WEIGHT = 0.85
17+
TIME_OBJECTIVE_WEIGHT = 0.15
1818

1919

2020
# Estimated Boat Speeds (kmph) as function of True Wind Speed (kmph)
@@ -109,9 +109,6 @@ def wind_direction_cost(s1: cs.XY, s2: cs.XY, tw_direction_rad: float) -> float:
109109
tw_angle_rad = abs(wcs.get_true_wind_angle(segment_true_bearing_rad, tw_direction_rad))
110110
cos_angle = math.cos(tw_angle_rad)
111111

112-
# The target point of sail (POS) is a beam reach for max speed and stability
113-
# A beam reach is when the true wind direction is perpendicular to the heading of the boat
114-
# That is why we assign the min cost of 0 to any segment that corresponds to a beam reach
115112
if cos_angle > 0:
116113
return UPWIND_COST_MULTIPLIER * cos_angle
117114
else:

0 commit comments

Comments
 (0)