Skip to content

Commit 7c3a1ea

Browse files
authored
Feat: add superimpose debug app (#30)
* add superimpose debug app * fixup
1 parent 7e0d785 commit 7c3a1ea

File tree

4 files changed

+155
-15
lines changed

4 files changed

+155
-15
lines changed

wavey/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@
2020
from wavey.map import DEFAULT_ARROW_LENGTH, RESOLUTION, Map
2121
from wavey.nwfs import download_forecast, get_most_recent_forecast
2222

23-
# Force non-interactive backend to keep consistency between local and github actions
24-
matplotlib.rcParams["backend"] = "agg"
25-
2623
LOG = logging.getLogger(__name__)
2724

2825
# Location of Breakwater (aka San Carlos Beach)
@@ -99,6 +96,9 @@ def main(
9996
dither: Dithering increases image quality, but also increases storage size.
10097
"""
10198

99+
# Force non-interactive backend to keep consistency between local and github actions
100+
matplotlib.rcParams["backend"] = "agg"
101+
102102
if resolution != "f":
103103
LOG.warning("Not drawing full resolution coastlines. Use the flag '--resolution f'")
104104

wavey/apps/__init__.py

Whitespace-only changes.

wavey/apps/superimpose.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import datetime
2+
import logging
3+
from typing import Literal
4+
5+
import matplotlib.pyplot as plt
6+
import numpy as np
7+
import pygrib
8+
9+
from wavey.__main__ import (
10+
BREAKWATER_LAT_IDX,
11+
BREAKWATER_LON_IDX,
12+
DPI,
13+
MONASTERY_LAT_IDX,
14+
MONASTERY_LON_IDX,
15+
utc_to_pt,
16+
)
17+
from wavey.common import FEET_PER_METER, setup_logging
18+
from wavey.grib import NUM_DATA_POINTS, ForecastData, ForecastType, read_forecast_data
19+
from wavey.nwfs import download_forecast, get_all_forecasts
20+
21+
LOG = logging.getLogger(__name__)
22+
23+
24+
def main(
25+
time: Literal["00", "06"] = "06",
26+
) -> None:
27+
"""
28+
Superimpose forecasts from multiple times on the same graph. This is useful for visualizing how reliable forecasts
29+
are with different forecast horizons.
30+
31+
Args:
32+
time: Forecast time in UTC, either "00" or "06".
33+
"""
34+
35+
all_forecasts = get_all_forecasts(time=time)
36+
37+
# Download forecast data
38+
wave_height_forecasts: list[ForecastData] = []
39+
for forecast in all_forecasts:
40+
grib_path = download_forecast(forecast)
41+
42+
with pygrib.open(grib_path) as grbs:
43+
wave_height_forecasts.append(read_forecast_data(grbs, ForecastType.WaveHeight))
44+
45+
LOG.info("Plotting graph")
46+
_, ax = plt.subplots(1, 1, figsize=(8, 4), dpi=DPI)
47+
48+
for i, wave_height_forecast in enumerate(wave_height_forecasts):
49+
wave_height_ft = wave_height_forecast.data * FEET_PER_METER
50+
analysis_date_pacific = utc_to_pt(wave_height_forecast.analysis_date_utc)
51+
52+
offset = i * 24 # each forecast is 24 hours into the past
53+
54+
bw_wave_height_ft = wave_height_ft[offset:, BREAKWATER_LAT_IDX, BREAKWATER_LON_IDX]
55+
assert not np.ma.is_masked(bw_wave_height_ft), "Unexpected: Breakwater data contains masked points"
56+
57+
mon_wave_height_ft = wave_height_ft[offset:, MONASTERY_LAT_IDX, MONASTERY_LON_IDX]
58+
assert not np.ma.is_masked(mon_wave_height_ft), "Unexpected: Monastery data contains masked points"
59+
60+
# NOTE: need to erase timezone info for mlpd3 to plot local times correctly
61+
time0 = analysis_date_pacific.replace(tzinfo=None)
62+
times = [time0 + datetime.timedelta(hours=hour_i) for hour_i in range(offset, NUM_DATA_POINTS)]
63+
64+
# plot styles
65+
labels: tuple[str | None, str | None]
66+
if offset == 0:
67+
labels = ("Breakwater", "Monastery")
68+
linestyle = "-"
69+
alpha = 1.0
70+
else:
71+
labels = (None, None)
72+
linestyle = "--"
73+
alpha = 0.5 ** ((offset / 24) - 1)
74+
75+
ax.plot(times, bw_wave_height_ft, label=labels[0], color="blue", linestyle=linestyle, alpha=alpha) # type: ignore[arg-type]
76+
ax.plot(times, mon_wave_height_ft, label=labels[1], color="red", linestyle=linestyle, alpha=alpha) # type: ignore[arg-type]
77+
78+
ax.set_ylim(0)
79+
ax.set_ylabel("Significant wave height (ft)")
80+
ax.yaxis.label.set_fontsize(14)
81+
ax.xaxis.set_ticks_position("bottom")
82+
ax.legend(loc="upper right")
83+
ax.grid(True, linestyle=":", alpha=0.7)
84+
85+
plt.tight_layout()
86+
plt.show()
87+
88+
89+
if __name__ == "__main__":
90+
import tyro
91+
92+
setup_logging()
93+
tyro.cli(main)

wavey/nwfs.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def get_most_recent_forecast() -> str:
2323
2424
Returns:
2525
URL to the GRIB file.
26+
27+
Raises:
28+
HTTPError: If accessing website returns error.
2629
"""
2730

2831
# 1. List dates with forecasts
@@ -55,17 +58,7 @@ def get_most_recent_forecast() -> str:
5558
"Unexpected: could not find any forecasts for Monterey bay."
5659
)
5760

58-
# parse date and time
59-
date_match = re.search(r"\d{8}", most_recent_date)
60-
assert date_match, f"Unexpected date: {most_recent_date}"
61-
time_match = re.search(r"\d{2}", most_recent_time)
62-
assert time_match, f"Unexpected time: {most_recent_time}"
63-
64-
yyyymmdd = date_match.group(0)
65-
hh = time_match.group(0)
66-
filename = f"{_MTR}_nwps_{_CG3}_{yyyymmdd}_{hh}00.grib2"
67-
68-
url = os.path.join(_BASE_URL, most_recent_date, _MTR, most_recent_time, _CG3, filename)
61+
url = _get_url(date=most_recent_date, time=most_recent_time)
6962
LOG.info(f"Found most recent forecast: {url}")
7063
return url
7164

@@ -103,6 +96,9 @@ def _list_dates() -> list[str]:
10396
10497
Returns:
10598
List of strings like "wr.YYYYMMDD/"; sorted (most recent first).
99+
100+
Raises:
101+
HTTPError: If accessing website returns error.
106102
"""
107103

108104
url = _BASE_URL
@@ -121,7 +117,7 @@ def _list_times(date: str) -> list[str]:
121117
List of strings like "HH/"; sorted (most recent first).
122118
123119
Raises:
124-
HTTPError: If no forecasts for the given date.
120+
HTTPError: If no forecasts for Monterey on the given date.
125121
"""
126122

127123
url = os.path.join(_BASE_URL, date, _MTR)
@@ -146,6 +142,54 @@ def _check_time(date: str, time: str) -> bool:
146142
return r.ok
147143

148144

145+
def _get_url(date: str, time: str) -> str:
146+
"""
147+
Given date and time, get URL to the GRIB file.
148+
149+
Args:
150+
date: A string like "wr.YYYYMMDD/".
151+
time: A string like "HH/".
152+
153+
Returns:
154+
URL to the GRIB file.
155+
"""
156+
157+
date_match = re.search(r"\d{8}", date)
158+
assert date_match, f"Unexpected date: {date}"
159+
time_match = re.search(r"\d{2}", time)
160+
assert time_match, f"Unexpected time: {time}"
161+
162+
yyyymmdd = date_match.group(0)
163+
hh = time_match.group(0)
164+
filename = f"{_MTR}_nwps_{_CG3}_{yyyymmdd}_{hh}00.grib2"
165+
166+
return os.path.join(_BASE_URL, date, _MTR, time, _CG3, filename)
167+
168+
169+
def get_all_forecasts(time: str = "06") -> list[str]:
170+
"""
171+
Get all NWFS forecast data for Monterey bay.
172+
173+
Args:
174+
time: A string like "HH/".
175+
176+
Returns:
177+
List of URLS to GRIB files.
178+
179+
Raises:
180+
HTTPError: If accessing website returns error.
181+
"""
182+
183+
# 1. List dates with forecasts
184+
dates = _list_dates()
185+
LOG.info(f"Found NWFS forecasts: {dates}")
186+
187+
# 2. For each date, check for Monterey bay forecast on the given time
188+
good_dates = [date for date in dates if _check_time(date=date, time=time)]
189+
190+
return [_get_url(date=date, time=time) for date in good_dates]
191+
192+
149193
def download_forecast(url: str, dir: Path | None = None) -> Path:
150194
"""
151195
Download NWFS forecast data to disk.
@@ -157,6 +201,9 @@ def download_forecast(url: str, dir: Path | None = None) -> Path:
157201
158202
Returns:
159203
Path to the GRIB file.
204+
205+
Raises:
206+
HTTPError: If error encountered during download.
160207
"""
161208

162209
if dir is None:

0 commit comments

Comments
 (0)