Skip to content

Commit 1f4f792

Browse files
khushalkottaruCopilotGui-FernandesBR
authored
BUG: Add wraparound logic for wind direction in environment plots (#939)
* chore: added personal toolkit files * update branch name in workflow * chore: update toolkit files * Fix: add wraparound logic for wind direction and related tests * style: fix ruff formatting * Remove unused import Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * refactor: move repetitive logic into helper method * fix: update test logic in test_environment * add changelog entry --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com>
1 parent df52c15 commit 1f4f792

3 files changed

Lines changed: 92 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Attention: The newest changes should be on top -->
7373

7474
### Fixed
7575

76+
- BUG: Add wraparound logic for wind direction in environment plots [#939](https://github.com/RocketPy-Team/RocketPy/pull/939)
7677
- BUG: Restore `Rocket.power_off_drag` and `Rocket.power_on_drag` as `Function` objects while preserving raw inputs in `power_off_drag_input` and `power_on_drag_input` [#941](https://github.com/RocketPy-Team/RocketPy/pull/941)
7778
- BUG: Add explicit timeouts to ThrustCurve API requests [#935](https://github.com/RocketPy-Team/RocketPy/pull/935)
7879
- BUG: Fix hard-coded radius value for parachute added mass calculation [#889](https://github.com/RocketPy-Team/RocketPy/pull/889)

rocketpy/plots/environment_plots.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@ def __init__(self, environment):
3333
self.grid = np.linspace(environment.elevation, environment.max_expected_height)
3434
self.environment = environment
3535

36+
def _break_direction_wraparound(self, directions, altitudes):
37+
"""Inserts NaN into direction and altitude arrays at 0°/360° wraparound
38+
points so matplotlib does not draw a horizontal line across the plot.
39+
40+
Parameters
41+
----------
42+
directions : numpy.ndarray
43+
Wind direction values in degrees, dtype float.
44+
altitudes : numpy.ndarray
45+
Altitude values corresponding to each direction, dtype float.
46+
47+
Returns
48+
-------
49+
directions : numpy.ndarray
50+
Direction array with NaN inserted at wraparound points.
51+
altitudes : numpy.ndarray
52+
Altitude array with NaN inserted at wraparound points.
53+
"""
54+
WRAP_THRESHOLD = 180 # degrees; half the full circle
55+
wrap_indices = np.where(np.abs(np.diff(directions)) > WRAP_THRESHOLD)[0] + 1
56+
directions = np.insert(directions, wrap_indices, np.nan)
57+
altitudes = np.insert(altitudes, wrap_indices, np.nan)
58+
return directions, altitudes
59+
3660
def __wind(self, ax):
3761
"""Adds wind speed and wind direction graphs to the same axis.
3862
@@ -55,9 +79,14 @@ def __wind(self, ax):
5579
ax.set_xlabel("Wind Speed (m/s)", color="#ff7f0e")
5680
ax.tick_params("x", colors="#ff7f0e")
5781
axup = ax.twiny()
82+
directions = np.array(
83+
[self.environment.wind_direction(i) for i in self.grid], dtype=float
84+
)
85+
altitudes = np.array(self.grid, dtype=float)
86+
directions, altitudes = self._break_direction_wraparound(directions, altitudes)
5887
axup.plot(
59-
[self.environment.wind_direction(i) for i in self.grid],
60-
self.grid,
88+
directions,
89+
altitudes,
6190
color="#1f77b4",
6291
label="Wind Direction",
6392
)
@@ -311,9 +340,14 @@ def ensemble_member_comparison(self, *, filename=None):
311340
ax8 = plt.subplot(324)
312341
for i in range(self.environment.num_ensemble_members):
313342
self.environment.select_ensemble_member(i)
343+
dirs = np.array(
344+
[self.environment.wind_direction(j) for j in self.grid], dtype=float
345+
)
346+
alts = np.array(self.grid, dtype=float)
347+
dirs, alts = self._break_direction_wraparound(dirs, alts)
314348
ax8.plot(
315-
[self.environment.wind_direction(i) for i in self.grid],
316-
self.grid,
349+
dirs,
350+
alts,
317351
label=i,
318352
)
319353
ax8.set_ylabel("Height Above Sea Level (m)")

tests/integration/environment/test_environment.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,59 @@ def test_standard_atmosphere(mock_show, example_plain_env): # pylint: disable=u
9292
assert example_plain_env.prints.print_earth_details() is None
9393

9494

95+
@patch("matplotlib.pyplot.show")
96+
def test_wind_plots_wrapping_direction(mock_show, example_plain_env): # pylint: disable=unused-argument
97+
"""Tests that wind direction plots handle 360°→0° wraparound without
98+
drawing a horizontal line across the graph.
99+
100+
Parameters
101+
----------
102+
mock_show : mock
103+
Mock object to replace matplotlib.pyplot.show() method.
104+
example_plain_env : rocketpy.Environment
105+
Example environment object to be tested.
106+
"""
107+
# Set a custom atmosphere where wind direction wraps from ~350° to ~10°
108+
# across the altitude range by choosing wind_u and wind_v to create a
109+
# direction near 350° at low altitude and ~10° at higher altitude.
110+
# wind_direction = (180 + atan2(wind_u, wind_v)) % 360
111+
# For direction ~350°: need atan2(wind_u, wind_v) ≈ 170° → wind_u>0, wind_v<0
112+
# For direction ~10°: need atan2(wind_u, wind_v) ≈ -170° → wind_u<0, wind_v<0
113+
example_plain_env.set_atmospheric_model(
114+
type="custom_atmosphere",
115+
pressure=None,
116+
temperature=300,
117+
wind_u=[(0, 1), (5000, -1)], # changes sign across altitude
118+
wind_v=[(0, -6), (5000, -6)], # stays negative → heading near 350°/10°
119+
)
120+
# Verify that the wind direction actually wraps through 0°/360° in this
121+
# atmosphere so the test exercises the wraparound code path.
122+
low_dir = example_plain_env.wind_direction(0)
123+
high_dir = example_plain_env.wind_direction(5000)
124+
assert abs(low_dir - high_dir) > 180, (
125+
"Test setup error: wind direction should cross 0°/360° boundary"
126+
)
127+
# Verify that the helper inserts NaN breaks into the direction and altitude
128+
# arrays at the wraparound point, which is the core of the fix.
129+
directions = np.array(
130+
[example_plain_env.wind_direction(i) for i in example_plain_env.plots.grid],
131+
dtype=float,
132+
)
133+
altitudes = np.array(example_plain_env.plots.grid, dtype=float)
134+
directions_broken, altitudes_broken = (
135+
example_plain_env.plots._break_direction_wraparound(directions, altitudes)
136+
)
137+
assert np.any(np.isnan(directions_broken)), (
138+
"Expected NaN breaks in direction array at 0°/360° wraparound"
139+
)
140+
assert np.any(np.isnan(altitudes_broken)), (
141+
"Expected NaN breaks in altitude array at 0°/360° wraparound"
142+
)
143+
# Verify info() and atmospheric_model() plots complete without error
144+
assert example_plain_env.info() is None
145+
assert example_plain_env.plots.atmospheric_model() is None
146+
147+
95148
@pytest.mark.parametrize(
96149
"model_name",
97150
[

0 commit comments

Comments
 (0)