diff --git a/README.rst b/README.rst index c240d0d..236e3be 100644 --- a/README.rst +++ b/README.rst @@ -97,6 +97,25 @@ This will download new datasets as necessary and load the new data into the ``da Example ------- +Plot data from the command line +``````````````````````````````` + +Running the following from the command line will plot the variable *significant wave height* +from the WAVEWATCH III *at_4m* grid. Note that the time of day (in this case, 15:00) is +separated from the date with a ``T`` (i.e. times can be given as ``YYYY-MM-DDTHH``) + +.. code:: bash + + $ ww3 plot --grid=at_4m --data-var=swh "2010-09-15T15" + +.. image:: https://raw.githubusercontent.com/csdms/bmi-wavewatch3/main/docs/source/_static/hurrican_julia.png + :width: 100% + :alt: Hurricane Julia + +Plot data from Python +````````````````````` + +This example is similar to the previous but uses the *bmi_wavewatch3* Python interface. .. code:: python diff --git a/bmi_wavewatch3/cli.py b/bmi_wavewatch3/cli.py index 2b45c16..2412f59 100644 --- a/bmi_wavewatch3/cli.py +++ b/bmi_wavewatch3/cli.py @@ -10,10 +10,12 @@ from multiprocessing import Pool, RLock import click +import matplotlib.pyplot as plt from tqdm.auto import tqdm from .downloader import WaveWatch3Downloader from .errors import ChoiceError, DateValueError from .source import SOURCES +from .wavewatch3 import WaveWatch3 out = partial(click.secho, bold=True, file=sys.stderr) @@ -26,7 +28,7 @@ def validate_date(ctx, param, value): source = SOURCES[ctx.parent.params["source"]] - for date_str in value: + for date_str in [value] if isinstance(value, str) else value: try: source.validate_date(date_str) except DateValueError as error: @@ -39,7 +41,7 @@ def validate_quantity(ctx, param, value): if not value: return sorted(source.QUANTITIES) - for quantity in value: + for quantity in [value] if isinstance(value, str) else value: try: source.validate_quantity(quantity) except ChoiceError as error: @@ -47,6 +49,30 @@ def validate_quantity(ctx, param, value): return value +def validate_data_var(ctx, param, value): + data_var_to_quantity = { + "dirpw": "dp", + "swh": "hs", + "perpw": "tp", + "u": "wind", + "v": "wind", + "swdir": "pdir", + "swell": "phs", + "swper": "ptp", + } + source = SOURCES[ctx.parent.params["source"]] + try: + quantity = data_var_to_quantity[value] + except KeyError: + raise click.BadParameter(ChoiceError(value, list(data_var_to_quantity))) + + try: + source.validate_quantity(quantity) + except ChoiceError as error: + raise click.BadParameter(error) + return value + + def validate_grid(ctx, param, value): source = SOURCES[ctx.parent.params["source"]] if not value: @@ -244,6 +270,53 @@ def clean(ctx, dry_run, cache_dir, yes): out(f"Removed {len(cache_files)} files ({total_bytes} bytes)") +@ww3.command() +@click.argument("date", callback=validate_date) +@click.option("--grid", default=None, help="Grid to download", callback=validate_grid) +@click.option( + "--data-var", + help="Data variable to plot", + default="swh", + callback=validate_data_var, +) +@click.pass_context +def plot(ctx, date, grid, data_var): + """Plot WAVEWATCH III data by date.""" + verbose = ctx.parent.params["verbose"] + silent = ctx.parent.params["silent"] + source = ctx.parent.params["source"] + + data_var_to_quantity = { + "dirpw": "dp", + "swh": "hs", + "perpw": "tp", + "u": "wind", + "v": "wind", + "swdir": "pdir", + "swell": "phs", + "swper": "ptp", + } + quantity = data_var_to_quantity[data_var] + + if not silent: + out(f"source: {source}") + out(f"grid: {grid}") + out(f"date: {date}") + out(f"data_var: {data_var} ({quantity})") + + ww3 = WaveWatch3(date, source=source, grid=grid) + if not silent and verbose: + [out(f"source file: {url}") for url in ww3._urls] + + ww3.data + if not silent and verbose: + [out(f"cache file: {ww3._cache / url.filename}") for url in ww3._urls] + + ww3.data[data_var][ww3.step, :, :].plot() + plt.gca().set_aspect(1) + plt.show() + + def _retreive_urls(urls, disable=False, force=False): tqdm.set_lock(RLock()) p = Pool(initializer=tqdm.set_lock, initargs=(tqdm.get_lock(),)) diff --git a/bmi_wavewatch3/source.py b/bmi_wavewatch3/source.py index 9063509..4da3ac6 100644 --- a/bmi_wavewatch3/source.py +++ b/bmi_wavewatch3/source.py @@ -121,7 +121,7 @@ def validate_date(cls, date): try: datetime.datetime.fromisoformat(date) except ValueError as error: - raise DateValueError(error) + raise DateValueError(str(error)) date_in_range_or_raise(date, lower=cls.MIN_DATE, upper=cls.MAX_DATE) return date diff --git a/docs/source/_static/hurricane_julia.png b/docs/source/_static/hurricane_julia.png new file mode 100644 index 0000000..a439378 Binary files /dev/null and b/docs/source/_static/hurricane_julia.png differ diff --git a/news/13.bugfix b/news/13.bugfix new file mode 100644 index 0000000..a045302 --- /dev/null +++ b/news/13.bugfix @@ -0,0 +1,3 @@ +Fixed a bug in the reporting of an error caused by an invalide datatime +string. + diff --git a/news/13.feature b/news/13.feature new file mode 100644 index 0000000..b66b45d --- /dev/null +++ b/news/13.feature @@ -0,0 +1,5 @@ +Added a new subcommand, *plot*, to the *ww3* command-line program. +``ww3 plot`` with download (if the data files are not already cached) and +create a plot of the requested data. + + diff --git a/pyproject.toml b/pyproject.toml index 63ffab8..55369f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "bmipy", "cfgrib", "click", + "matplotlib", "tomli", "tqdm", "xarray",