Skip to content

Commit 1734c03

Browse files
Raghumanimehta/735 refactorings (#739)
* initial changes * fixed tests * minor change in node_mock_wind_sensor's use of get_apparent_wind * removed import --------- Co-authored-by: Sean Donaghy <[email protected]>
1 parent b212574 commit 1734c03

File tree

8 files changed

+95
-99
lines changed

8 files changed

+95
-99
lines changed

src/local_pathfinding/local_pathfinding/coord_systems.py

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ def true_bearing_to_plotly_cartesian(true_bearing_deg: float) -> float:
5050
Returns:
5151
float: Angle where 0 is north and values increases clockwise.
5252
"""
53-
assert -180 < true_bearing_deg <= 180
54-
5553
plotly_cartesian = true_bearing_deg
5654
if -180 < true_bearing_deg < 0:
5755
plotly_cartesian += 360.0
@@ -81,41 +79,23 @@ def get_path_segment_true_bearing(s1: XY, s2: XY, rad: bool = False):
8179
return bound_to_180(math.degrees(segment_true_bearing_rad))
8280

8381

84-
def angle_to_vector_projections(vector_angle_rad: float, vector_magnitude: float) -> XY:
85-
"""Convert a polar vector (angle in radians, speed_knots) to east/north Cartesian components.
82+
def polar_to_cartesian(angle_rad: float, magnitude: float) -> XY:
83+
"""Convert polar coordinates to Cartesian coordinates.
8684
8785
Args:
88-
vector_angle_rad (float): Direction angle in radians. Range: (-π, π], where 0 rad = north,
89-
+π/2 = east.
90-
vector_magnitude (float): Vector speed (e.g., true wind speed in kmph).
86+
angle_rad (float): Direction angle in radians. Range: (-pi, pi], where 0 rad = north,
87+
+pi/2 = east.
88+
magnitude (float): Vector magnitude (e.g., speed in kmph).
9189
9290
Returns:
93-
XY: Decomposed vector components in the global (east, north) frame:
91+
XY: Cartesian vector components in the global (east, north) frame:
9492
- x → east component
9593
- y → north component
9694
"""
97-
# case 1: vector in quadrant I
98-
if 0 <= vector_angle_rad <= PI / 2:
99-
return XY(
100-
x=vector_magnitude * math.sin(vector_angle_rad),
101-
y=vector_magnitude * math.cos(vector_angle_rad),
102-
)
103-
# case 2: vector in quadrant IV
104-
elif PI / 2 < vector_angle_rad <= PI:
105-
alpha = vector_angle_rad - (PI / 2) # alpha is with respect to positive x-axis
106-
return XY(x=vector_magnitude * math.cos(alpha), y=vector_magnitude * -1 * math.sin(alpha))
107-
# case 3: vector in quadrant II
108-
elif -PI / 2 <= vector_angle_rad < 0:
109-
alpha = abs(vector_angle_rad) # vector_angle_rad is negative in quadrant II
110-
return XY(x=vector_magnitude * -1 * math.sin(alpha), y=vector_magnitude * math.cos(alpha))
111-
# case 4: vector in quadrant III
112-
elif -PI <= vector_angle_rad < -PI / 2:
113-
# vector_angle_rad is negative in quadrant III and alpha is with respect to negative x-axis
114-
alpha = abs(vector_angle_rad) - (PI / 2)
115-
return XY(
116-
x=vector_magnitude * -1 * math.cos(alpha), y=vector_magnitude * -1 * math.sin(alpha)
117-
)
118-
return XY(x=0.0, y=0.0) # place holder for invalid input
95+
return XY(
96+
x=magnitude * math.sin(angle_rad),
97+
y=magnitude * math.cos(angle_rad)
98+
)
11999

120100

121101
def meters_to_km(meters: float) -> float:

src/local_pathfinding/local_pathfinding/mock_nodes/node_mock_gps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def wind_sensor_callback(self, msg: ci.WindSensor):
136136
self._logger.debug(f"Received data from {self.__mock_wind_sensor_sub.topic}: {msg}")
137137
aw_speed_kmph: float = msg.speed.speed
138138
aw_direction_deg: float = msg.direction
139-
tw_direction_rad, tw_speed_kmph = wcs.get_true_wind(
139+
tw_dir_rad, tw_speed_kmph = wcs.get_true_wind(
140140
aw_direction_deg,
141141
aw_speed_kmph,
142142
self.__heading_deg.heading,
@@ -146,7 +146,7 @@ def wind_sensor_callback(self, msg: ci.WindSensor):
146146
speed=float(
147147
TimeObjective.get_sailbot_speed(
148148
math.radians(self.__heading_deg.heading),
149-
tw_direction_rad,
149+
tw_dir_rad,
150150
tw_speed_kmph,
151151
)
152152
)

src/local_pathfinding/local_pathfinding/mock_nodes/node_mock_wind_sensor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from typing import List
2626

2727
import custom_interfaces.msg as ci
28-
import numpy as np
2928
import rclpy
3029
from rcl_interfaces.msg import SetParametersResult
3130
from rclpy.node import Node
@@ -78,15 +77,16 @@ def __init__(self):
7877
self.add_on_set_parameters_callback(self._on_set_parameters)
7978

8079
def mock_wind_sensor_callback(self):
81-
aw_speed_kmph, aw_direction_rad = wcs.get_apparent_wind(
80+
aw_dir_deg, aw_speed_kmph = wcs.get_apparent_wind(
8281
self._tw_dir_deg,
8382
self._tw_speed_kmph,
8483
self._boat_heading_deg,
8584
self._boat_speed,
85+
ret_rad=False
8686
)
8787
aw_dir_boat_coord_deg = wcs.global_to_boat_coordinate(
8888
self._boat_heading_deg,
89-
np.degrees(aw_direction_rad),
89+
aw_dir_deg
9090
)
9191
msg = ci.WindSensor(
9292
speed=ci.HelperSpeed(speed=aw_speed_kmph),

src/local_pathfinding/local_pathfinding/ompl_objectives.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def get_sailing_objective(
223223
boat_heading_degrees, apparent_wind_direction_degrees
224224
)
225225

226-
true_wind_direction_radians, true_wind_speed_kmph = wcs.get_true_wind(
226+
tw_dir_rad, tw_speed_kmph = wcs.get_true_wind(
227227
apparent_wind_direction_degrees_global_coordinates,
228228
apparent_wind_speed_kmph,
229229
boat_heading_degrees,
@@ -234,13 +234,13 @@ def get_sailing_objective(
234234
multiObjective.addObjective(
235235
objective=WindObjective(
236236
space_information,
237-
true_wind_direction_radians,
237+
tw_dir_rad,
238238
),
239239
weight=WIND_OBJECTIVE_WEIGHT,
240240
)
241241
multiObjective.addObjective(
242242
objective=TimeObjective(
243-
space_information, true_wind_direction_radians, true_wind_speed_kmph
243+
space_information, tw_dir_rad, tw_speed_kmph
244244
),
245245
weight=TIME_OBJECTIVE_WEIGHT,
246246
)

src/local_pathfinding/local_pathfinding/visualizer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,16 @@ def __init__(self, msgs: deque[ci.LPathData]):
109109
aw_dir_global_rad = math.radians(aw_dir_global)
110110

111111
# Compute apparent wind vector (in global frame)
112-
self.aw_wind_vector = cs.angle_to_vector_projections(aw_dir_global_rad, aw_speed)
112+
self.aw_wind_vector = cs.polar_to_cartesian(aw_dir_global_rad, aw_speed)
113113

114114
# True wind from apparent
115-
true_wind_angle_rad, true_wind_mag = wcs.get_true_wind(
115+
tw_dir_rad, tw_speed_kmph = wcs.get_true_wind(
116116
aw_dir_global, aw_speed, boat_heading, boat_speed
117117
)
118-
self.true_wind_vector = cs.angle_to_vector_projections(true_wind_angle_rad, true_wind_mag)
118+
self.true_wind_vector = cs.polar_to_cartesian(tw_dir_rad, tw_speed_kmph)
119119
# Boat wind vector
120120
boat_wind_radians = math.radians(cs.bound_to_180(boat_heading + 180))
121-
self.boat_wind_vector = cs.angle_to_vector_projections(boat_wind_radians, boat_speed)
121+
self.boat_wind_vector = cs.polar_to_cartesian(boat_wind_radians, boat_speed)
122122

123123
def _validate_message(self, msg: ci.LPathData):
124124
"""Checks if the sailbot observer node received any messages.

src/local_pathfinding/local_pathfinding/wind_coord_systems.py

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,29 @@ def global_to_boat_coordinate(boat_heading: float, global_wind_direction: float)
4141

4242

4343
def get_true_wind(
44-
aw_direction: float,
45-
aw_speed: float,
46-
boat_heading: float,
47-
boat_speed: float,
44+
aw_dir_deg: float,
45+
aw_speed_kmph: float,
46+
boat_heading_deg: float,
47+
boat_speed_kmph: float,
48+
ret_rad: bool = True,
4849
) -> tuple[float, float]:
4950
"""Compute the true wind vector from apparent wind and boat motion.
5051
5152
Args:
52-
aw_direction (float): Apparent wind direction in degrees (-180, 180]. This is the wind
53+
aw_dir_deg (float): Apparent wind direction in degrees (-180, 180]. This is the wind
5354
as measured relative to the boat (sensor reading).
54-
aw_speed (float): Apparent wind speed (same units as boat_speed), e.g., km/h.
55-
boat_heading (float): Boat heading in degrees (-180, 180].
56-
boat_speed (float): Boat speed over ground (same units as aw_speed), e.g., km/h.
55+
aw_speed_kmph (float): Apparent wind speed in km/h.
56+
boat_heading_deg (float): Boat heading in degrees (-180, 180].
57+
boat_speed_kmph (float): Boat speed over ground in km/h.
58+
ret_rad (bool): If True, return wind direction in radians. If False, return in degrees.
5759
NOTE: All the angles are with respect to global true bearing. It is the
5860
responsibility of the caller to ensure this. Particularly, the apparent wind read by the
5961
sensor is in boat coordinates
6062
6163
Returns:
62-
tuple[float, float]: (tw_angle, tw_magnitude)
63-
- tw_angle: true wind direction in radians within (-pi, pi].
64-
- tw_magnitude: true wind speed
64+
tuple[float, float]: (tw_dir_rad, tw_speed_kmph)
65+
- tw_dir_rad: true wind direction in radians within (-pi, pi].
66+
- tw_speed_kmph: true wind speed in km/h
6567
If the resulting vector magnitude is effectively zero (<= FLOATING_POINT_ERROR_THRESHOLD),
6668
returns (0.0, 0.0). NOTE: The caller is responsible for handling this case, otherwise the
6769
calculations will break down.
@@ -70,65 +72,79 @@ def get_true_wind(
7072
The function computes vector components in an east/north frame, subtracting the boat
7173
motion from the apparent wind to obtain the true wind.
7274
"""
73-
wind_radians = math.radians(aw_direction)
75+
aw_dir_rad = math.radians(aw_dir_deg)
7476

7577
# boat wind is in the direction of the boat heading (reverse of boat heading)
76-
boat_wind_radians = math.radians(cs.bound_to_180(boat_heading + 180))
77-
apparent_wind_east = aw_speed * math.sin(wind_radians)
78-
apparent_wind_north = aw_speed * math.cos(wind_radians)
78+
bw_dir_rad = math.radians(cs.bound_to_180(boat_heading_deg + 180))
79+
aw_east_kmph = aw_speed_kmph * math.sin(aw_dir_rad)
80+
aw_north_kmph = aw_speed_kmph * math.cos(aw_dir_rad)
7981

80-
boat_wind_east = boat_speed * math.sin(boat_wind_radians)
81-
boat_wind_north = boat_speed * math.cos(boat_wind_radians)
82+
bw_east_kmph = boat_speed_kmph * math.sin(bw_dir_rad)
83+
bw_north_kmph = boat_speed_kmph * math.cos(bw_dir_rad)
8284

83-
true_east = apparent_wind_east - boat_wind_east
84-
true_north = apparent_wind_north - boat_wind_north
85+
tw_east_kmph = aw_east_kmph - bw_east_kmph
86+
tw_north_kmph = aw_north_kmph - bw_north_kmph
8587

86-
magnitude = math.hypot(true_east, true_north)
87-
angle = math.atan2(true_east, true_north)
88+
tw_speed_kmph = math.hypot(tw_east_kmph, tw_north_kmph)
89+
tw_dir_rad = math.atan2(tw_east_kmph, tw_north_kmph)
8890

89-
if magnitude > FLOATING_POINT_ERROR_THRESHOLD:
90-
return angle, magnitude
91+
if ret_rad:
92+
tw_dir = tw_dir_rad
93+
else:
94+
tw_dir = math.degrees(tw_dir_rad)
95+
96+
if tw_speed_kmph > FLOATING_POINT_ERROR_THRESHOLD:
97+
return tw_dir, tw_speed_kmph
9198
return ZERO_VECTOR_CONSTANT, 0.0
9299

93100

94-
def get_apparent_wind(tw_direction, tw_speed, boat_heading, boat_speed) -> tuple[float, float]:
101+
def get_apparent_wind(
102+
tw_dir_deg: float,
103+
tw_speed_kmph: float,
104+
boat_heading_deg: float,
105+
boat_speed_kmph: float,
106+
ret_rad: bool = True,
107+
) -> tuple[float, float]:
95108
"""Compute the apparent wind vector from true wind and boat motion.
96109
97110
Args:
98-
tw_direction (float): True wind direction in radians (measured as atan2(east, north) style)
99-
or degrees if caller uses degrees consistently. (This routine converts using math.radians # noqa
100-
in the original code; keep inputs consistent with usage.)
101-
tw_speed (float): True wind speed (same units as boat_speed), e.g., km/h.
102-
boat_heading (float): Boat heading in degrees (-180, 180].
103-
boat_speed (float): Boat speed over ground (same units as tw_speed), e.g., km/h.
111+
tw_dir_deg (float): True wind direction in degrees (-180, 180].
112+
tw_speed_kmph (float): True wind speed in km/h.
113+
boat_heading_deg (float): Boat heading in degrees (-180, 180].
114+
boat_speed_kmph (float): Boat speed over ground in km/h.
115+
ret_rad (bool): If True, return wind direction in radians. If False, return in degrees.
104116
NOTE: All the angles are with respect to global true bearing. It is the
105117
responsibility of the caller to ensure this.
106118
107119
Returns:
108-
tuple[float, float]: (aw_angle, aw_magnitude)
109-
- aw_angle: apparent wind direction in radians within (-pi, pi]
110-
- aw_magnitude: apparent wind speed (same units as inputs).
120+
tuple[float, float]: (aw_dir_rad, aw_speed_kmph)
121+
- aw_dir_rad: apparent wind direction in radians within (-pi, pi]
122+
- aw_speed_kmph: apparent wind speed in km/h.
111123
If the resulting vector magnitude is effectively zero (<= FLOATING_POINT_ERROR_THRESHOLD),
112124
returns (0.0, 0.0). NOTE: The caller is responsible for handling this case, otherwise the
113125
calculations will break down.
114126
"""
115-
tw_direction = math.radians(tw_direction)
127+
tw_dir_rad = math.radians(tw_dir_deg)
128+
129+
bw_speed_kmph = boat_speed_kmph
130+
bw_dir_rad = math.radians(cs.bound_to_180(boat_heading_deg + 180.0))
131+
bw_east_kmph = bw_speed_kmph * math.sin(bw_dir_rad)
132+
bw_north_kmph = bw_speed_kmph * math.cos(bw_dir_rad)
116133

117-
bw_speed = boat_speed
118-
bw_direction = math.radians(cs.bound_to_180(boat_heading + 180.0))
119-
# print(math.degrees(bw_direction))
120-
boat_wind_east = bw_speed * math.sin(bw_direction)
121-
boat_wind_north = bw_speed * math.cos(bw_direction)
134+
tw_east_kmph = tw_speed_kmph * math.sin(tw_dir_rad)
135+
tw_north_kmph = tw_speed_kmph * math.cos(tw_dir_rad)
122136

123-
tw_speed_east = tw_speed * math.sin(tw_direction)
124-
tw_speed_north = tw_speed * math.cos(tw_direction)
137+
aw_east_kmph = tw_east_kmph + bw_east_kmph
138+
aw_north_kmph = tw_north_kmph + bw_north_kmph
125139

126-
aw_speed_east = tw_speed_east + boat_wind_east
127-
aw_speed_north = tw_speed_north + boat_wind_north
140+
aw_speed_kmph = math.hypot(aw_east_kmph, aw_north_kmph)
141+
aw_dir_rad = math.atan2(aw_east_kmph, aw_north_kmph)
128142

129-
magnitude = math.hypot(aw_speed_east, aw_speed_north)
130-
angle = math.atan2(aw_speed_east, aw_speed_north)
143+
if ret_rad:
144+
aw_dir = aw_dir_rad
145+
else:
146+
aw_dir = math.degrees(aw_dir_rad)
131147

132-
if magnitude > FLOATING_POINT_ERROR_THRESHOLD:
133-
return angle, magnitude
148+
if aw_speed_kmph > FLOATING_POINT_ERROR_THRESHOLD:
149+
return aw_dir, aw_speed_kmph
134150
return ZERO_VECTOR_CONSTANT, 0.0

src/local_pathfinding/test/test_coord_systems.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def test_true_bearing_to_plotly_cartesian(true_bearing: float, plotly_cartesian:
119119
(NORTHEAST, 0.0, cs.XY(0.0, 0.0))
120120
],
121121
)
122-
def test_angle_to_vector_projections(angle_rad: float, speed: float, expected_xy: cs.XY):
123-
result = cs.angle_to_vector_projections(angle_rad, speed)
122+
def test_polar_to_cartesian(angle_rad: float, speed: float, expected_xy: cs.XY):
123+
result = cs.polar_to_cartesian(angle_rad, speed)
124124

125125
# Component-wise check
126126
assert (result.x, result.y) == pytest.approx(

src/local_pathfinding/test/test_wind_coord_systems.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,16 @@ def test_get_true_wind_direction(
6666
expected_direction: float,
6767
expected_speed: float,
6868
):
69-
true_wind_direction, true_wind_speed = wcs.get_true_wind(
69+
tw_dir_rad, tw_speed_kmph = wcs.get_true_wind(
7070
wind_direction_degrees, wind_speed, heading_degrees, speed
7171
)
7272

7373
# Convert radians to degrees for easier comparison
74-
true_wind_direction_degrees = math.degrees(true_wind_direction)
74+
tw_dir_deg = math.degrees(tw_dir_rad)
7575

76-
assert true_wind_direction_degrees == pytest.approx(
76+
assert tw_dir_deg == pytest.approx(
7777
expected=expected_direction, abs=1e-2
78-
) and true_wind_speed == pytest.approx(expected=expected_speed, abs=1e-2)
78+
) and tw_speed_kmph == pytest.approx(expected=expected_speed, abs=1e-2)
7979

8080

8181
@pytest.mark.parametrize(
@@ -96,14 +96,14 @@ def test_get_apparent_wind_direction(
9696
expected_direction: float,
9797
expected_speed: float,
9898
):
99-
aw_direction, aw_speed = wcs.get_apparent_wind(
99+
aw_dir_rad, aw_speed_kmph = wcs.get_apparent_wind(
100100
tw_direction_degrees,
101101
tw_speed, heading_degrees, speed
102102
)
103103

104104
# Convert radians to degrees for easier comparison
105-
aw_direction_deg = math.degrees(aw_direction)
105+
aw_dir_deg = math.degrees(aw_dir_rad)
106106

107-
assert aw_direction_deg == pytest.approx(
107+
assert aw_dir_deg == pytest.approx(
108108
expected=expected_direction, abs=1e-2
109-
) and aw_speed == pytest.approx(expected=expected_speed, abs=1e-2)
109+
) and aw_speed_kmph == pytest.approx(expected=expected_speed, abs=1e-2)

0 commit comments

Comments
 (0)