Skip to content

Commit 2e6b735

Browse files
Raghumanimehta/485 removing ompl path state constructor (#491)
* Removed OMPLPathState and added logic to calculate boat range and domain * Refactoring to remove OMPLPathState complete, all tests pass * updates after review * fixed linting issue to changes after review * small changes to LocalPathState docstring * renamed state_domain to start_box * rm type cast goal x and y to int & added dummy wind speed --------- Co-authored-by: Sean Donaghy <118148642+SPDonaghy@users.noreply.github.com>
1 parent 5939245 commit 2e6b735

File tree

3 files changed

+78
-61
lines changed

3 files changed

+78
-61
lines changed

src/local_pathfinding/local_pathfinding/local_path.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
from typing import List, Optional, Tuple
44

5-
from custom_interfaces.msg import GPS, AISShips, HelperLatLon, Path, WindSensor
5+
from custom_interfaces.msg import (
6+
GPS,
7+
AISShips,
8+
HelperLatLon,
9+
HelperSpeed,
10+
Path,
11+
WindSensor,
12+
)
613
from rclpy.impl.rcutils_logger import RcutilsLogger
714

815
from local_pathfinding.ompl_path import OMPLPath
@@ -11,10 +18,10 @@
1118
class LocalPathState:
1219
"""Gathers and stores the state of Sailbot.
1320
The attributes' units and conventions can be found in the ROS msgs they are derived from in the
14-
custom_interfaces repository.
21+
custom_interfaces package.
1522
1623
Attributes:
17-
`position` (Tuple[float, float]): Latitude and longitude of Sailbot.
24+
`position` (HelperLatLon): Latitude and longitude of Sailbot.
1825
`speed` (float): Speed of Sailbot.
1926
`heading` (float): Direction that Sailbot is pointing.
2027
`ais_ships` (List[HelperAISShip]): Information about nearby ships.
@@ -23,6 +30,7 @@ class LocalPathState:
2330
`wind_speed` (float): Wind speed.
2431
`wind_direction` (int): Wind direction.
2532
`planner` (str): Planner to use for the OMPL query.
33+
`reference (HelperLatLon): Lat and lon position of the next global waypoint.
2634
"""
2735

2836
def __init__(
@@ -35,9 +43,13 @@ def __init__(
3543
):
3644
"""Initializes the state from ROS msgs."""
3745
if gps: # TODO: remove when mock can be run
38-
self.position = (gps.lat_lon.latitude, gps.lat_lon.longitude)
46+
self.position = gps.lat_lon
3947
self.speed = gps.speed.speed
4048
self.heading = gps.heading.heading
49+
else:
50+
self.position = HelperLatLon(latitude=0.0, longitude=0.0)
51+
self.speed = 0.0
52+
self.heading = 0.0
4153

4254
if ais_ships: # TODO: remove when mock can be run
4355
self.ais_ships = [ship for ship in ais_ships.ships]
@@ -53,6 +65,13 @@ def __init__(
5365
if filtered_wind_sensor: # TODO: remove when mock can be run
5466
self.wind_speed = filtered_wind_sensor.speed.speed
5567
self.wind_direction = filtered_wind_sensor.direction
68+
else:
69+
self.wind_speed = HelperSpeed(speed=0.0)
70+
self.wind_direction = 0
71+
72+
self.reference_latlon = (
73+
self.global_path[-1] if self.global_path else HelperLatLon(latitude=0.0, longitude=0.0)
74+
)
5675

5776
self.planner = planner
5877

src/local_pathfinding/local_pathfinding/ompl_path.py

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import pyompl
1414
from custom_interfaces.msg import HelperLatLon
1515
from rclpy.impl.rcutils_logger import RcutilsLogger
16+
from shapely.geometry import MultiPolygon, Point, Polygon, box
1617

1718
import local_pathfinding.coord_systems as cs
19+
from local_pathfinding.coord_systems import XY
1820
from local_pathfinding.objectives import get_sailing_objective
1921

2022
if TYPE_CHECKING:
@@ -24,35 +26,13 @@
2426
# ou.setLogLevel(ou.LOG_WARN)
2527

2628

27-
class OMPLPathState:
28-
def __init__(self, local_path_state: LocalPathState, logger: RcutilsLogger):
29-
# TODO: derive OMPLPathState attributes from local_path_state
30-
self.heading_direction = 45.0
31-
self.wind_direction = 10.0
32-
self.wind_speed = 1.0
33-
34-
self.state_domain = (-1, 1)
35-
self.state_range = (-1, 1)
36-
self.start_state = (0.5, 0.4)
37-
self.goal_state = (0.5, -0.4)
38-
39-
self.reference_latlon = (
40-
local_path_state.global_path[-1]
41-
if local_path_state.global_path and len(local_path_state.global_path) > 0
42-
else HelperLatLon(latitude=0.0, longitude=0.0)
43-
)
44-
45-
if local_path_state:
46-
self.planner = None
47-
# self.planner = pyompl.RRTstar()
48-
49-
5029
class OMPLPath:
5130
"""Represents the general OMPL Path.
5231
5332
Attributes
5433
_logger (RcutilsLogger): ROS logger of this class.
5534
_simple_setup (og.SimpleSetup): OMPL SimpleSetup object.
35+
_box_buffer (float): buffer around the sailbot position and the goal position in km
5636
solved (bool): True if the path is a solution to the OMPL query, else false.
5737
"""
5838

@@ -69,6 +49,7 @@ def __init__(
6949
max_runtime (float): Maximum amount of time in seconds to look for a solution path.
7050
local_path_state (LocalPathState): State of Sailbot.
7151
"""
52+
self._box_buffer = 1
7253
self._logger = parent_logger.get_child(name="ompl_path")
7354

7455
self._simple_setup = self._init_simple_setup(local_path_state) # this needs state
@@ -114,6 +95,13 @@ def get_waypoints(self) -> List[HelperLatLon]:
11495

11596
return waypoints
11697

98+
def create_buffer_around_position(self: OMPLPath, position: XY) -> Polygon:
99+
"""Create a space around the given position. Position is the center of the space and
100+
is a tuple of x and y.
101+
"""
102+
space = Point(position.x, position.y).buffer(self._box_buffer, cap_style=3, join_style=2)
103+
return space
104+
117105
def update_objectives(self):
118106
"""Update the objectives on the basis of which the path is optimized.
119107
Raises:
@@ -122,21 +110,33 @@ def update_objectives(self):
122110
raise NotImplementedError
123111

124112
def _init_simple_setup(self, local_path_state) -> pyompl.SimpleSetup:
125-
"""Initialize and configure the OMPL SimpleSetup object.
126-
127-
Returns:
128-
og.SimpleSetup: Encapsulates the various objects necessary to solve a geometric or
129-
control query in OMPL.
130-
"""
131-
self.state = OMPLPathState(local_path_state, self._logger)
113+
self.state = local_path_state
114+
115+
# Create buffered spaces and extract their centers
116+
start_position_in_xy = cs.latlon_to_xy(self.state.reference_latlon, self.state.position)
117+
start_box = self.create_buffer_around_position(start_position_in_xy)
118+
start_x = start_position_in_xy.x
119+
start_y = start_position_in_xy.y
120+
121+
if not self.state.global_path:
122+
goal_polygon = self.create_buffer_around_position(cs.XY(0, 0))
123+
goal_x, goal_y = (0.0, 0.0)
124+
else:
125+
goal_position = self.state.global_path[-1]
126+
goal_position_in_xy = cs.latlon_to_xy(self.state.reference_latlon, goal_position)
127+
goal_polygon = self.create_buffer_around_position(goal_position_in_xy)
128+
goal_x, goal_y = goal_position_in_xy
132129

133130
# create an SE2 state space: rotation and translation in a plane
134131
space = pyompl.SE2StateSpace()
135132

136133
# set the bounds of the state space
137134
bounds = pyompl.RealVectorBounds(dim=2)
138-
x_min, x_max = self.state.state_domain
139-
y_min, y_max = self.state.state_range
135+
state_space = box(*MultiPolygon([start_box, goal_polygon]).bounds)
136+
x_min, y_min, x_max, y_max = state_space.bounds
137+
138+
if x_max <= x_min or y_max <= y_min:
139+
raise ValueError(f"Invalid bounds: x=[{x_min}, {x_max}], y=[{y_min}, {y_max}]")
140140
bounds.setLow(0, x_min)
141141
bounds.setLow(1, y_min)
142142
bounds.setHigh(0, x_max)
@@ -153,11 +153,8 @@ def _init_simple_setup(self, local_path_state) -> pyompl.SimpleSetup:
153153
simple_setup = pyompl.SimpleSetup(space)
154154
simple_setup.setStateValidityChecker(is_state_valid)
155155

156-
# set the goal and start states of the simple setup object
157156
start = pyompl.ScopedState(space)
158157
goal = pyompl.ScopedState(space)
159-
start_x, start_y = self.state.start_state
160-
goal_x, goal_y = self.state.goal_state
161158
start.setXY(start_x, start_y)
162159
goal.setXY(goal_x, goal_y)
163160
"""self._logger.debug(
@@ -170,6 +167,7 @@ def _init_simple_setup(self, local_path_state) -> pyompl.SimpleSetup:
170167
# Constructs a space information instance for this simple setup
171168
space_information = simple_setup.getSpaceInformation()
172169

170+
# figure this out
173171
self.state.planner = pyompl.RRTstar(space_information)
174172

175173
# set the optimization objective of the simple setup object
@@ -178,7 +176,8 @@ def _init_simple_setup(self, local_path_state) -> pyompl.SimpleSetup:
178176
objective = get_sailing_objective(
179177
space_information,
180178
simple_setup,
181-
self.state.heading_direction,
179+
# This too
180+
self.state.heading,
182181
self.state.wind_direction,
183182
self.state.wind_speed,
184183
)

src/local_pathfinding/test/test_ompl_path.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from custom_interfaces.msg import GPS, AISShips, Path, WindSensor
44
from rclpy.impl.rcutils_logger import RcutilsLogger
5+
from shapely.geometry import Point
56

67
import local_pathfinding.coord_systems as cs
78
import local_pathfinding.ompl_path as ompl_path
@@ -20,25 +21,6 @@
2021
)
2122

2223

23-
def test_OMPLPathState():
24-
local_path_state = LocalPathState(
25-
gps=GPS(),
26-
ais_ships=AISShips(),
27-
global_path=Path(),
28-
filtered_wind_sensor=WindSensor(),
29-
planner="rrtstar",
30-
)
31-
state = ompl_path.OMPLPathState(local_path_state, logger=RcutilsLogger())
32-
assert state.state_domain == (-1, 1), "incorrect value for attribute state_domain"
33-
assert state.state_range == (-1, 1), "incorrect value for attribute start_state"
34-
assert state.start_state == pytest.approx(
35-
(0.5, 0.4)
36-
), "incorrect value for attribute start_state"
37-
assert state.goal_state == pytest.approx(
38-
(0.5, -0.4)
39-
), "incorrect value for attribute goal_state"
40-
41-
4224
def test_OMPLPath___init__():
4325
assert PATH.solved
4426

@@ -50,8 +32,7 @@ def test_OMPLPath_get_cost():
5032

5133
def test_OMPLPath_get_waypoint():
5234
waypoints = PATH.get_waypoints()
53-
54-
waypoint_XY = cs.XY(*PATH.state.start_state)
35+
waypoint_XY = cs.XY(PATH.state.position.latitude, PATH.state.position.longitude)
5536
start_state_latlon = cs.xy_to_latlon(PATH.state.reference_latlon, waypoint_XY)
5637

5738
test_start = waypoints[0]
@@ -85,3 +66,21 @@ def test_is_state_valid(x: float, y: float, is_valid: bool):
8566
assert ompl_path.is_state_valid(state), "state should be valid"
8667
else:
8768
assert not ompl_path.is_state_valid(state), "state should not be valid"
69+
70+
71+
@pytest.mark.parametrize(
72+
"position,expected_area,expected_bounds",
73+
[
74+
(cs.XY(0.0, 0.0), pytest.approx(4, rel=1e-2), (-1, -1, 1, 1)),
75+
(cs.XY(100.0, 100.0), pytest.approx(4, rel=1e-2), (99, 99, 101, 101)),
76+
(cs.XY(-100.0, -100.0), pytest.approx(4, rel=1e-2), (-101, -101, -99, -99)),
77+
],
78+
)
79+
def test_create_space(position: cs.XY, expected_area, expected_bounds):
80+
"""Test creation of buffered space around positions"""
81+
# Given an OMPLPath instance
82+
space = PATH.create_buffer_around_position(position)
83+
84+
assert space.area == expected_area, "Space area should match buffer size"
85+
assert space.bounds == pytest.approx(expected_bounds, abs=1.0), "Bounds should match expected"
86+
assert space.contains(Point(position.x, position.y)), "Space should contain center point"

0 commit comments

Comments
 (0)