From 82009bea1a2d281fb3105baa8b57888e32b0d859 Mon Sep 17 00:00:00 2001 From: zacharyburnett Date: Fri, 3 Jun 2022 17:32:18 -0400 Subject: [PATCH] add `pre-commit` configuration and fix HWM multi-storm bug (for Hurricane Laura, #40) --- .github/workflows/quick_test.yml | 6 +- .github/workflows/tests.yml | 6 +- .pre-commit-config.yaml | 44 ++ README.md | 8 +- docs/source/conf.py | 50 +- pyproject.toml | 4 +- stormevents/coops/__init__.py | 26 +- stormevents/coops/tidalstations.py | 324 ++++++----- stormevents/nhc/atcf.py | 349 ++++++------ stormevents/nhc/storms.py | 180 ++++--- stormevents/nhc/track.py | 503 ++++++++++-------- stormevents/stormevent.py | 135 +++-- stormevents/usgs/__init__.py | 14 +- stormevents/usgs/events.py | 235 ++++---- stormevents/usgs/highwatermarks.py | 189 +++---- stormevents/usgs/sensors.py | 24 +- stormevents/utilities.py | 7 +- tests/__init__.py | 25 +- .../test_usgs_flood_event/florence2018.csv | 4 +- .../test_vortex_track_no_internet/fort.22 | 2 +- .../test_coops_product_within_region/data.nc | Bin 20042 -> 20042 bytes .../stations.csv | 2 +- .../data/reference/test_nhc_storms/storms.csv | 2 +- .../florence2018_water_levels.nc | Bin 204426 -> 204426 bytes .../test_usgs_flood_storms/storms.csv | 58 +- tests/test_atcf.py | 30 +- tests/test_coops.py | 99 ++-- tests/test_nhc.py | 192 +++---- tests/test_stormevent.py | 97 ++-- tests/test_usgs.py | 63 ++- tests/test_utilities.py | 42 +- 31 files changed, 1512 insertions(+), 1208 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/quick_test.yml b/.github/workflows/quick_test.yml index 15a128b..e926947 100644 --- a/.github/workflows/quick_test.yml +++ b/.github/workflows/quick_test.yml @@ -25,15 +25,15 @@ jobs: path: ${{ env.pythonLocation }} key: lint-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }} - name: install linters - run: pip install flake8 oitnb + run: pip install flake8 black - name: lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: lint with oitnb - run: oitnb . --check + - name: lint with black + run: black . --check test: needs: lint name: quick test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 43a09bf..678e7e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,15 +28,15 @@ jobs: path: ${{ env.pythonLocation }} key: lint-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }} - name: install linters - run: pip install flake8 oitnb + run: pip install flake8 black - name: lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: lint with oitnb - run: oitnb . --check + - name: lint with black + run: black . --check test: needs: lint name: test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a4b83e4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,44 @@ +--- +default_language_version: + python: "python3" +fail_fast: true +ci: + skip: ["poetry-lock", "prospector", "mypy"] + +repos: + - repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: "v4.2.0" + hooks: + - id: "check-added-large-files" + - id: "check-ast" + - id: "check-byte-order-marker" + - id: "check-docstring-first" + - id: "check-executables-have-shebangs" + - id: "check-json" + - id: "check-symlinks" + - id: "check-merge-conflict" + - id: "check-vcs-permalinks" + - id: "check-xml" + - id: "check-yaml" + - id: "debug-statements" + - id: "end-of-file-fixer" + exclude: .+\.ipynb + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-noqa + - id: python-check-blanket-type-ignore + - id: python-use-type-annotations + + - repo: "https://github.com/asottile/reorder_python_imports" + rev: "v3.1.0" + hooks: + - id: "reorder-python-imports" + args: + - "--py39-plus" + + - repo: "https://github.com/psf/black" + rev: "22.3.0" + hooks: + - id: "black" diff --git a/README.md b/README.md index 259ee14..14ac24c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://codecov.io/gh/oceanmodeling/StormEvents/branch/main/graph/badge.svg?token=BQWB1QKJ3Q)](https://codecov.io/gh/oceanmodeling/StormEvents) [![version](https://img.shields.io/pypi/v/StormEvents)](https://pypi.org/project/StormEvents) [![license](https://img.shields.io/github/license/oceanmodeling/StormEvents)](https://opensource.org/licenses/gpl-license) -[![style](https://sourceforge.net/p/oitnb/code/ci/default/tree/_doc/_static/oitnb.svg?format=raw)](https://sourceforge.net/p/oitnb/code) +[![style](https://github.com/psf/black)](https://img.shields.io/badge/code%20style-black-000000.svg) `stormevents` provides Python interfaces for observational data surrounding named storm events. @@ -292,7 +292,7 @@ coops_stations_within_region(region=region) ``` nws_id name state removed geometry -nos_id +nos_id 8651370 DUKN7 Duck NC NaT POINT (-75.75000 36.18750) 8652587 ORIN7 Oregon Inlet Marina NC NaT POINT (-75.56250 35.78125) 8654467 HCGN7 USCG Station Hatteras NC NaT POINT (-75.68750 35.21875) @@ -488,7 +488,7 @@ StormEvent(name='HENRI', year=2021, start_date=Timestamp('2021-08-21 12:00:00'), from stormevents import StormEvent from datetime import timedelta -StormEvent('ida', 2021, end_date=timedelta(days=2)) # <- end 2 days after NHC start time +StormEvent('ida', 2021, end_date=timedelta(days=2)) # <- end 2 days after NHC start time ``` ``` @@ -537,7 +537,7 @@ flood.high_water_marks() ``` latitude longitude eventName ... siteZone peak_summary_id geometry -hwm_id ... +hwm_id ... 33496 37.298440 -80.007750 Florence Sep 2018 ... NaN NaN POINT (-80.00775 37.29844) 33497 33.699720 -78.936940 Florence Sep 2018 ... NaN NaN POINT (-78.93694 33.69972) 33498 33.758610 -78.792780 Florence Sep 2018 ... NaN NaN POINT (-78.79278 33.75861) diff --git a/docs/source/conf.py b/docs/source/conf.py index d3543f4..82b2c99 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,17 +3,15 @@ # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html - # -- Path setup -------------------------------------------------------------- - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os -from os import PathLike -from pathlib import Path import subprocess import sys +from os import PathLike +from pathlib import Path from dunamai import Version from setuptools import config @@ -26,7 +24,7 @@ def repository_root(path: PathLike = None) -> Path: path = Path(path) if path.is_file(): path = path.parent - if '.git' in (child.name for child in path.iterdir()) or path == path.parent: + if ".git" in (child.name for child in path.iterdir()) or path == path.parent: return path else: return repository_root(path.parent) @@ -35,35 +33,35 @@ def repository_root(path: PathLike = None) -> Path: sys.path.insert(0, str(repository_root())) subprocess.run( - f'{sys.executable} -m pip install -U pip', + f"{sys.executable} -m pip install -U pip", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # -- Project information ----------------------------------------------------- -metadata = config.read_configuration('../../setup.cfg')['metadata'] +metadata = config.read_configuration("../../setup.cfg")["metadata"] -project = metadata['name'] -author = metadata['author'] -copyright = f'2021, {author}' +project = metadata["name"] +author = metadata["author"] +copyright = f"2021, {author}" # The full version, including alpha/beta/rc tags try: release = Version.from_any_vcs().serialize() except RuntimeError: - release = os.environ.get('VERSION') + release = os.environ.get("VERSION") # -- General configuration --------------------------------------------------- -autoclass_content = 'both' # include both class docstring and __init__ +autoclass_content = "both" # include both class docstring and __init__ autodoc_default_options = { # Make sure that any autodoc declarations show the right members - 'members': True, - 'inherited-members': True, - 'private-members': True, - 'member-order': 'bysource', - 'exclude-members': '__weakref__', + "members": True, + "inherited-members": True, + "private-members": True, + "member-order": "bysource", + "exclude-members": "__weakref__", } autosummary_generate = True # Make _autosummary files and include them napoleon_numpy_docstring = False # Force consistency, leave only Google @@ -74,15 +72,15 @@ def repository_root(path: PathLike = None) -> Path: # ones. extensions = [ # Need the autodoc and autosummary packages to generate our docs. - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", # The Napoleon extension allows for nicer argument formatting. - 'sphinx.ext.napoleon', - 'm2r2', + "sphinx.ext.napoleon", + "m2r2", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -94,13 +92,13 @@ def repository_root(path: PathLike = None) -> Path: # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Extension configuration ------------------------------------------------- -source_suffix = ['.rst', '.md'] -bibtex_bibfiles = ['references.bib'] +source_suffix = [".rst", ".md"] +bibtex_bibfiles = ["references.bib"] diff --git a/pyproject.toml b/pyproject.toml index 4e1ac1b..ec20b4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ shapely = '>=1.8' typepigeon = '>=1.0.5' xarray = '*' isort = { version = '*', optional = true } -oitnb = { version = '*', optional = true } +black = { version = '*', optional = true } pytest = { version = '*', optional = true } pytest-cov = { version = '*', optional = true } pytest-socket = { version = '*', optional = true } @@ -43,5 +43,5 @@ sphinx-rtd-theme = { version = '*', optional = true } [tool.poetry.extras] testing = ['pytest', 'pytest-cov', 'pytest-socket', 'pytest-xdist'] -development = ['isort', 'oitnb'] +development = ['isort', 'black'] documentation = ['m2r2', 'sphinx', 'sphinx-rtd-theme'] diff --git a/stormevents/coops/__init__.py b/stormevents/coops/__init__.py index 3b891eb..0cde654 100644 --- a/stormevents/coops/__init__.py +++ b/stormevents/coops/__init__.py @@ -1,14 +1,12 @@ -from stormevents.coops.tidalstations import ( - COOPS_Interval, - COOPS_Product, - coops_product_within_region, - COOPS_Query, - COOPS_Station, - coops_stations, - coops_stations_within_region, - COOPS_StationStatus, - COOPS_TidalDatum, - COOPS_TimeZone, - COOPS_Units, - COOPS_VelocityType, -) +from stormevents.coops.tidalstations import COOPS_Interval +from stormevents.coops.tidalstations import COOPS_Product +from stormevents.coops.tidalstations import coops_product_within_region +from stormevents.coops.tidalstations import COOPS_Query +from stormevents.coops.tidalstations import COOPS_Station +from stormevents.coops.tidalstations import coops_stations +from stormevents.coops.tidalstations import coops_stations_within_region +from stormevents.coops.tidalstations import COOPS_StationStatus +from stormevents.coops.tidalstations import COOPS_TidalDatum +from stormevents.coops.tidalstations import COOPS_TimeZone +from stormevents.coops.tidalstations import COOPS_Units +from stormevents.coops.tidalstations import COOPS_VelocityType diff --git a/stormevents/coops/tidalstations.py b/stormevents/coops/tidalstations.py index 006659e..ec89d62 100644 --- a/stormevents/coops/tidalstations.py +++ b/stormevents/coops/tidalstations.py @@ -2,96 +2,109 @@ interface with the NOAA Center for Operational Oceanographic Products and Services (CO-OPS) API https://api.tidesandcurrents.noaa.gov/api/prod/ """ - +import json from datetime import datetime from enum import Enum from functools import lru_cache -import json from pathlib import Path from typing import Union -from bs4 import BeautifulSoup, element import geopandas -from geopandas import GeoDataFrame import numpy import pandas -from pandas import DataFrame, Series import requests -from shapely.geometry import box, MultiPolygon, Polygon import typepigeon import xarray +from bs4 import BeautifulSoup +from bs4 import element +from geopandas import GeoDataFrame +from pandas import DataFrame +from pandas import Series +from shapely.geometry import box +from shapely.geometry import MultiPolygon +from shapely.geometry import Polygon from xarray import Dataset class COOPS_Product(Enum): WATER_LEVEL = ( - 'water_level' + "water_level" # Preliminary or verified water levels, depending on availability. ) - AIR_TEMPERATURE = 'air_temperature' # Air temperature as measured at the station. - WATER_TEMPERATURE = 'water_temperature' # Water temperature as measured at the station. - WIND = 'wind' # Wind speed, direction, and gusts as measured at the station. - AIR_PRESSURE = 'air_pressure' # Barometric pressure as measured at the station. - AIR_GAP = 'air_gap' # Air Gap (distance between a bridge and the water's surface) at the station. - CONDUCTIVITY = 'conductivity' # The water's conductivity as measured at the station. - VISIBILITY = 'visibility' # Visibility from the station's visibility sensor. A measure of atmospheric clarity. - HUMIDITY = 'humidity' # Relative humidity as measured at the station. - SALINITY = 'salinity' # Salinity and specific gravity data for the station. - HOURLY_HEIGHT = 'hourly_height' # Verified hourly height water level data for the station. - HIGH_LOW = 'high_low' # Verified high/low water level data for the station. - DAILY_MEAN = 'daily_mean' # Verified daily mean water level data for the station. - MONTHLY_MEAN = 'monthly_mean' # Verified monthly mean water level data for the station. + AIR_TEMPERATURE = "air_temperature" # Air temperature as measured at the station. + WATER_TEMPERATURE = ( + "water_temperature" # Water temperature as measured at the station. + ) + WIND = "wind" # Wind speed, direction, and gusts as measured at the station. + AIR_PRESSURE = "air_pressure" # Barometric pressure as measured at the station. + AIR_GAP = "air_gap" # Air Gap (distance between a bridge and the water's surface) at the station. + CONDUCTIVITY = ( + "conductivity" # The water's conductivity as measured at the station. + ) + VISIBILITY = "visibility" # Visibility from the station's visibility sensor. A measure of atmospheric clarity. + HUMIDITY = "humidity" # Relative humidity as measured at the station. + SALINITY = "salinity" # Salinity and specific gravity data for the station. + HOURLY_HEIGHT = ( + "hourly_height" # Verified hourly height water level data for the station. + ) + HIGH_LOW = "high_low" # Verified high/low water level data for the station. + DAILY_MEAN = "daily_mean" # Verified daily mean water level data for the station. + MONTHLY_MEAN = ( + "monthly_mean" # Verified monthly mean water level data for the station. + ) ONE_MINUTE_WATER_LEVEL = ( - 'one_minute_water_level' + "one_minute_water_level" # One minute water level data for the station. ) - PREDICTIONS = 'predictions' # 6 minute predictions water level data for the station.* - DATUMS = 'datums' # datums data for the stations. - CURRENTS = 'currents' # Currents data for currents stations. + PREDICTIONS = ( + "predictions" # 6 minute predictions water level data for the station.* + ) + DATUMS = "datums" # datums data for the stations. + CURRENTS = "currents" # Currents data for currents stations. CURRENTS_PREDICTIONS = ( - 'currents_predictions' + "currents_predictions" # Currents predictions data for currents predictions stations. ) class COOPS_TidalDatum(Enum): - CRD = 'CRD' # Columbia River Datum - IGLD = 'IGLD' # International Great Lakes Datum - LWD = 'LWD' # Great Lakes Low Water Datum (Chart Datum) - MHHW = 'MHHW' # Mean Higher High Water - MHW = 'MHW' # Mean High Water - MTL = 'MTL' # Mean Tide Level - MSL = 'MSL' # Mean Sea Level - MLW = 'MLW' # Mean Low Water - MLLW = 'MLLW' # Mean Lower Low Water - NAVD = 'NAVD' # North American Vertical Datum - STND = 'STND' # Station Datum + CRD = "CRD" # Columbia River Datum + IGLD = "IGLD" # International Great Lakes Datum + LWD = "LWD" # Great Lakes Low Water Datum (Chart Datum) + MHHW = "MHHW" # Mean Higher High Water + MHW = "MHW" # Mean High Water + MTL = "MTL" # Mean Tide Level + MSL = "MSL" # Mean Sea Level + MLW = "MLW" # Mean Low Water + MLLW = "MLLW" # Mean Lower Low Water + NAVD = "NAVD" # North American Vertical Datum + STND = "STND" # Station Datum class COOPS_VelocityType(Enum): - SPEED_DIR = 'speed_dir' # Return results for speed and dirction - DEFAULT = 'default' # Return results for velocity major, mean flood direction and mean ebb dirction + SPEED_DIR = "speed_dir" # Return results for speed and dirction + DEFAULT = "default" # Return results for velocity major, mean flood direction and mean ebb dirction class COOPS_Units(Enum): - METRIC = 'metric' - ENGLISH = 'english' + METRIC = "metric" + ENGLISH = "english" class COOPS_TimeZone(Enum): - GMT = 'gmt' # Greenwich Mean Time - LST = 'lst' # Local Standard Time. The time local to the requested station. - LST_LDT = 'lst_ldt' # Local Standard/Local Daylight Time. The time local to the requested station. + GMT = "gmt" # Greenwich Mean Time + LST = "lst" # Local Standard Time. The time local to the requested station. + LST_LDT = "lst_ldt" # Local Standard/Local Daylight Time. The time local to the requested station. class COOPS_Interval(Enum): - H = 'h' # Hourly Met data and harmonic predictions will be returned - HILO = 'hilo' # High/Low tide predictions for all stations. + H = "h" # Hourly Met data and harmonic predictions will be returned + HILO = "hilo" # High/Low tide predictions for all stations. class COOPS_StationStatus(Enum): - ACTIVE = 'active' - DISCONTINUED = 'discontinued' + ACTIVE = "active" + DISCONTINUED = "discontinued" class COOPS_Station: @@ -122,17 +135,17 @@ def __init__(self, id: Union[int, str]): stations = coops_stations() if id in stations.index: metadata = stations.loc[id] - elif id in stations['nws_id'].values: - metadata = stations[stations['nws_id'] == id] - elif id in stations['name'].values: - metadata = stations[stations['name'] == id] + elif id in stations["nws_id"].values: + metadata = stations[stations["nws_id"] == id] + elif id in stations["name"].values: + metadata = stations[stations["name"] == id] else: metadata = None if metadata is None or len(metadata) == 0: raise ValueError(f'station with "{id}" not found') - removed = metadata['removed'] + removed = metadata["removed"] if isinstance(removed, Series): self.__active = pandas.isna(removed).any() removed = pandas.unique(removed.dropna().sort_values(ascending=False)) @@ -144,10 +157,10 @@ def __init__(self, id: Union[int, str]): metadata = metadata.iloc[0] self.nos_id = metadata.name - self.nws_id = metadata['nws_id'] + self.nws_id = metadata["nws_id"] self.location = metadata.geometry - self.state = metadata['state'] - self.name = metadata['name'] + self.state = metadata["state"] + self.name = metadata["name"] self.__query = None @@ -166,30 +179,32 @@ def constituents(self) -> DataFrame: :return: table of tidal constituents for the current station """ - url = f'https://tidesandcurrents.noaa.gov/harcon.html?id={self.nos_id}' + url = f"https://tidesandcurrents.noaa.gov/harcon.html?id={self.nos_id}" response = requests.get(url) - soup = BeautifulSoup(response.content, features='html.parser') - table = soup.find_all('table', {'class': 'table table-striped'}) + soup = BeautifulSoup(response.content, features="html.parser") + table = soup.find_all("table", {"class": "table table-striped"}) if len(table) > 0: table = table[0] - columns = [field.text for field in table.find('thead').find('tr').find_all('th')] + columns = [ + field.text for field in table.find("thead").find("tr").find_all("th") + ] constituents = [] - for row in table.find_all('tr')[1:]: - constituents.append([entry.text for entry in row.find_all('td')]) + for row in table.find_all("tr")[1:]: + constituents.append([entry.text for entry in row.find_all("td")]) constituents = DataFrame(constituents, columns=columns) - constituents.rename(columns={'Constituent #': '#'}, inplace=True) + constituents.rename(columns={"Constituent #": "#"}, inplace=True) constituents = constituents.astype( { - '#': numpy.int32, - 'Amplitude': numpy.float64, - 'Phase': numpy.float64, - 'Speed': numpy.float64, + "#": numpy.int32, + "Amplitude": numpy.float64, + "Phase": numpy.float64, + "Speed": numpy.float64, } ) else: - constituents = DataFrame(columns=['#', 'Amplitude', 'Phase', 'Speed']) + constituents = DataFrame(columns=["#", "Amplitude", "Phase", "Speed"]) - constituents.set_index('#', inplace=True) + constituents.set_index("#", inplace=True) return constituents def product( @@ -260,29 +275,33 @@ def product( data = self.__query.data - data['nos_id'] = self.nos_id - data.set_index(['nos_id', data.index], inplace=True) + data["nos_id"] = self.nos_id + data.set_index(["nos_id", data.index], inplace=True) data = data.to_xarray() - if len(data['t']) > 0: + if len(data["t"]) > 0: data = data.assign_coords( - nws_id=('nos_id', [self.nws_id]), - x=('nos_id', [self.location.x]), - y=('nos_id', [self.location.y]), + nws_id=("nos_id", [self.nws_id]), + x=("nos_id", [self.location.x]), + y=("nos_id", [self.location.y]), ) else: data = data.assign_coords( - nws_id=('nos_id', []), x=('nos_id', []), y=('nos_id', []), + nws_id=("nos_id", []), + x=("nos_id", []), + y=("nos_id", []), ) return data def __str__(self) -> str: - return f'{self.__class__.__name__} - {self.nos_id} ({self.name}) - {self.location}' + return ( + f"{self.__class__.__name__} - {self.nos_id} ({self.name}) - {self.location}" + ) def __repr__(self) -> str: - return f'{self.__class__.__name__}({self.nos_id})' + return f"{self.__class__.__name__}({self.nos_id})" class COOPS_Query: @@ -291,7 +310,7 @@ class COOPS_Query: https://api.tidesandcurrents.noaa.gov/api/prod/ """ - URL = 'https://tidesandcurrents.noaa.gov/api/datagetter?' + URL = "https://tidesandcurrents.noaa.gov/api/datagetter?" def __init__( self, @@ -411,7 +430,7 @@ def query(self): product = product.value start_date = self.start_date if start_date is not None and not isinstance(start_date, str): - start_date = f'{self.start_date:%Y%m%d %H:%M}' + start_date = f"{self.start_date:%Y%m%d %H:%M}" datum = self.datum if isinstance(datum, Enum): datum = datum.value @@ -426,16 +445,16 @@ def query(self): interval = interval.value return { - 'station': self.station, - 'product': product, - 'begin_date': start_date, - 'end_date': f'{self.end_date:%Y%m%d %H:%M}', - 'datum': datum, - 'units': units, - 'time_zone': time_zone, - 'interval': interval, - 'format': 'json', - 'application': 'noaa/nos/csdl/stormevents', + "station": self.station, + "product": product, + "begin_date": start_date, + "end_date": f"{self.end_date:%Y%m%d %H:%M}", + "datum": datum, + "units": units, + "time_zone": time_zone, + "interval": interval, + "format": "json", + "application": "noaa/nos/csdl/stormevents", } @property @@ -464,21 +483,26 @@ def data(self) -> DataFrame: if self.__previous_query is None or self.query != self.__previous_query: response = requests.get(self.URL, params=self.query) data = response.json() - fields = ['t', 'v', 's', 'f', 'q'] - if 'error' in data or 'data' not in data: - if 'error' in data: - self.__error = data['error']['message'] + fields = ["t", "v", "s", "f", "q"] + if "error" in data or "data" not in data: + if "error" in data: + self.__error = data["error"]["message"] data = DataFrame(columns=fields) else: - data = DataFrame(data['data'], columns=fields) - data[data == ''] = numpy.nan + data = DataFrame(data["data"], columns=fields) + data[data == ""] = numpy.nan data = data.astype( - {'v': numpy.float32, 's': numpy.float32, 'f': 'string', 'q': 'string'}, - errors='ignore', + { + "v": numpy.float32, + "s": numpy.float32, + "f": "string", + "q": "string", + }, + errors="ignore", ) - data['t'] = pandas.to_datetime(data['t']) + data["t"] = pandas.to_datetime(data["t"]) - data.set_index('t', inplace=True) + data.set_index("t", inplace=True) self.__data = data return self.__data @@ -489,10 +513,10 @@ def __repr__(self) -> str: @lru_cache(maxsize=None) def __coops_stations_html_tables() -> element.ResultSet: response = requests.get( - 'https://access.co-ops.nos.noaa.gov/nwsproducts.html?type=current', + "https://access.co-ops.nos.noaa.gov/nwsproducts.html?type=current", ) - soup = BeautifulSoup(response.content, features='html.parser') - return soup.find_all('div', {'class': 'table-responsive'}) + soup = BeautifulSoup(response.content, features="html.parser") + return soup.find_all("div", {"class": "table-responsive"}) @lru_cache(maxsize=None) @@ -553,82 +577,84 @@ def coops_stations(station_status: COOPS_StationStatus = None) -> GeoDataFrame: tables = __coops_stations_html_tables() status_tables = { - COOPS_StationStatus.ACTIVE: (0, 'NWSTable'), - COOPS_StationStatus.DISCONTINUED: (1, 'HistNWSTable'), + COOPS_StationStatus.ACTIVE: (0, "NWSTable"), + COOPS_StationStatus.DISCONTINUED: (1, "HistNWSTable"), } dataframes = {} for status, (table_index, table_id) in status_tables.items(): - table = tables[table_index].find('table', {'id': table_id}).find_all('tr') + table = tables[table_index].find("table", {"id": table_id}).find_all("tr") - stations_columns = [field.text for field in table[0].find_all('th')] + stations_columns = [field.text for field in table[0].find_all("th")] stations = DataFrame( [ - [value.text.strip() for value in station.find_all('td')] + [value.text.strip() for value in station.find_all("td")] for station in table[1:] ], columns=stations_columns, ) stations.rename( columns={ - 'NOS ID': 'nos_id', - 'NWS ID': 'nws_id', - 'Latitude': 'y', - 'Longitude': 'x', - 'State': 'state', - 'Station Name': 'name', + "NOS ID": "nos_id", + "NWS ID": "nws_id", + "Latitude": "y", + "Longitude": "x", + "State": "state", + "Station Name": "name", }, inplace=True, ) stations = stations.astype( { - 'nos_id': numpy.int32, - 'nws_id': 'string', - 'x': numpy.float16, - 'y': numpy.float16, - 'state': 'string', - 'name': 'string', + "nos_id": numpy.int32, + "nws_id": "string", + "x": numpy.float16, + "y": numpy.float16, + "state": "string", + "name": "string", }, copy=False, ) - stations.set_index('nos_id', inplace=True) + stations.set_index("nos_id", inplace=True) if status == COOPS_StationStatus.DISCONTINUED: - with open(Path(__file__).parent / 'us_states.json') as us_states_file: + with open(Path(__file__).parent / "us_states.json") as us_states_file: us_states = json.load(us_states_file) for name, abbreviation in us_states.items(): - stations.loc[stations['state'] == name, 'state'] = abbreviation + stations.loc[stations["state"] == name, "state"] = abbreviation - stations.rename(columns={'Removed Date/Time': 'removed'}, inplace=True) + stations.rename(columns={"Removed Date/Time": "removed"}, inplace=True) - stations['removed'] = pandas.to_datetime(stations['removed']).astype('string') + stations["removed"] = pandas.to_datetime(stations["removed"]).astype( + "string" + ) stations = ( - stations[~pandas.isna(stations['removed'])] - .sort_values('removed', ascending=False) - .groupby('nos_id') + stations[~pandas.isna(stations["removed"])] + .sort_values("removed", ascending=False) + .groupby("nos_id") .aggregate( { - 'nws_id': 'first', - 'removed': ','.join, - 'y': 'first', - 'x': 'first', - 'state': 'first', - 'name': 'first', + "nws_id": "first", + "removed": ",".join, + "y": "first", + "x": "first", + "state": "first", + "name": "first", } ) ) else: - stations['removed'] = pandas.NA + stations["removed"] = pandas.NA - stations['status'] = status.value + stations["status"] = status.value dataframes[status] = stations active_stations = dataframes[COOPS_StationStatus.ACTIVE] discontinued_stations = dataframes[COOPS_StationStatus.DISCONTINUED] discontinued_stations.loc[ - discontinued_stations.index.isin(active_stations.index), 'status' + discontinued_stations.index.isin(active_stations.index), "status" ] = COOPS_StationStatus.ACTIVE.value stations = pandas.concat( @@ -637,22 +663,25 @@ def coops_stations(station_status: COOPS_StationStatus = None) -> GeoDataFrame: discontinued_stations, ) ) - stations.loc[pandas.isna(stations['status']), 'status'] = COOPS_StationStatus.ACTIVE.value - stations.sort_values(['status', 'removed'], na_position='first', inplace=True) + stations.loc[ + pandas.isna(stations["status"]), "status" + ] = COOPS_StationStatus.ACTIVE.value + stations.sort_values(["status", "removed"], na_position="first", inplace=True) if station_status is not None: if isinstance(station_status, COOPS_StationStatus): station_status = station_status.value - stations = stations[stations['status'] == station_status] + stations = stations[stations["status"] == station_status] return GeoDataFrame( - stations[['nws_id', 'name', 'state', 'status', 'removed']], - geometry=geopandas.points_from_xy(stations['x'], stations['y']), + stations[["nws_id", "name", "state", "status", "removed"]], + geometry=geopandas.points_from_xy(stations["x"], stations["y"]), ) def coops_stations_within_region( - region: Polygon, station_status: COOPS_StationStatus = None, + region: Polygon, + station_status: COOPS_StationStatus = None, ) -> GeoDataFrame: """ retrieve all stations within the specified region of interest @@ -692,7 +721,8 @@ def coops_stations_within_bounds( station_status: COOPS_StationStatus = None, ) -> GeoDataFrame: return coops_stations_within_region( - region=box(minx=minx, miny=miny, maxx=maxx, maxy=maxy), station_status=station_status + region=box(minx=minx, miny=miny, maxx=maxx, maxy=maxy), + station_status=station_status, ) @@ -742,7 +772,9 @@ def coops_product_within_region( q (nos_id, t) object 'p' 'p' 'p' 'p' 'p' 'p' ... 'p' 'p' 'p' 'p' 'p' """ - stations = coops_stations_within_region(region=region, station_status=station_status) + stations = coops_stations_within_region( + region=region, station_status=station_status + ) station_data = [ COOPS_Station(station).product( product=product, @@ -755,5 +787,5 @@ def coops_product_within_region( ) for station in stations.index ] - station_data = [station for station in station_data if len(station['t']) > 0] - return xarray.combine_nested(station_data, concat_dim='nos_id') + station_data = [station for station in station_data if len(station["t"]) > 0] + return xarray.combine_nested(station_data, concat_dim="nos_id") diff --git a/stormevents/nhc/atcf.py b/stormevents/nhc/atcf.py index 2a4e52c..1a040a3 100644 --- a/stormevents/nhc/atcf.py +++ b/stormevents/nhc/atcf.py @@ -1,17 +1,21 @@ -from datetime import datetime -from enum import Enum import ftplib import io import itertools +from collections.abc import Iterable +from datetime import datetime +from enum import Enum from os import PathLike from pathlib import Path -from typing import Iterable, List, TextIO, Union +from typing import List +from typing import TextIO +from typing import Union import geopandas -from geopandas import GeoDataFrame import pandas -from pandas import DataFrame, Series import typepigeon +from geopandas import GeoDataFrame +from pandas import DataFrame +from pandas import Series from stormevents.nhc.storms import nhc_storms @@ -23,116 +27,116 @@ # https://www.nrlmry.navy.mil/atcf_web/docs/database/new/abrdeck.html ATCF_FIELDS = { # BASIN - basin, e.g. WP, IO, SH, CP, EP, AL, SL - 'BASIN': 'basin', + "BASIN": "basin", # CY - annual cyclone number: 1 through 99 - 'CY': 'storm_number', + "CY": "storm_number", # YYYYMMDDHH - Warning Date-Time-Group: 0000010100 through 9999123123. (note, 4 digit year) - 'YYYYMMDDHH': 'datetime', + "YYYYMMDDHH": "datetime", # TECHNUM/MIN - objective technique sorting number, minutes for best track: 00 - 99 - 'TECHNUM/MIN': 'advisory_number', + "TECHNUM/MIN": "advisory_number", # TECH - acronym for each objective technique or CARQ or WRNG, BEST for best track. - 'TECH': 'advisory', + "TECH": "advisory", # TAU - forecast period: -24 through 240 hours, 0 for best-track, negative taus used for CARQ and WRNG records. - 'TAU': 'forecast_hours', + "TAU": "forecast_hours", # LatN/S - Latitude (tenths of degrees) for the DTG: 0 through 900, N/S is the hemispheric index. - 'LatN/S': 'latitude', + "LatN/S": "latitude", # LonE/W - Longitude (tenths of degrees) for the DTG: 0 through 1800, E/W is the hemispheric index. - 'LonE/W': 'longitude', + "LonE/W": "longitude", # VMAX - Maximum sustained wind speed in knots: 0 through 300. - 'VMAX': 'max_sustained_wind_speed', + "VMAX": "max_sustained_wind_speed", # MSLP - Minimum sea level pressure, 1 through 1100 MB. - 'MSLP': 'central_pressure', + "MSLP": "central_pressure", # TY - Level of tc development: DB - disturbance, TD - tropical depression, TS - tropical storm, TY - typhoon, ST - super typhoon, TC - tropical cyclone, HU - hurricane, SD - subtropical depression, SS - subtropical storm, EX - extratropical systems, IN - inland, DS - dissipating, LO - low, WV - tropical wave, ET - extrapolated, XX - unknown. - 'TY': 'development_level', + "TY": "development_level", # RAD - Wind intensity (kts) for the radii defined in this record: 34, 50, 64. - 'RAD': 'isotach_radius', + "RAD": "isotach_radius", # WINDCODE - Radius code: AAA - full circle, QQQ - quadrant (NNQ, NEQ, EEQ, SEQ, SSQ, SWQ, WWQ, NWQ) - 'WINDCODE': 'isotach_quadrant_code', + "WINDCODE": "isotach_quadrant_code", # RAD1 - If full circle, radius of specified wind intensity, If semicircle or quadrant, radius of specified wind intensity of circle portion specified in radius code. 0 - 1200 nm. - 'RAD1': 'isotach_radius_for_NEQ', + "RAD1": "isotach_radius_for_NEQ", # RAD2 - If full circle this field not used, If semicicle, radius (nm) of specified wind intensity for semicircle not specified in radius code, If quadrant, radius (nm) of specified wind intensity for 2nd quadrant (counting clockwise from quadrant specified in radius code). 0 through 1200 nm. - 'RAD2': 'isotach_radius_for_SEQ', + "RAD2": "isotach_radius_for_SEQ", # RAD3 - If full circle or semicircle this field not used, If quadrant, radius (nm) of specified wind intensity for 3rd quadrant (counting clockwise from quadrant specified in radius code). 0 through 1200 nm. - 'RAD3': 'isotach_radius_for_NWQ', + "RAD3": "isotach_radius_for_NWQ", # RAD4 - If full circle or semicircle this field not used, If quadrant, radius (nm) of specified wind intensity for 4th quadrant (counting clockwise from quadrant specified in radius code). 0 through 1200 nm. - 'RAD4': 'isotach_radius_for_SWQ', + "RAD4": "isotach_radius_for_SWQ", # RADP - pressure in millibars of the last closed isobar, 900 - 1050 mb. - 'RADP': 'background_pressure', + "RADP": "background_pressure", # RRP - radius of the last closed isobar in nm, 0 - 9999 nm. - 'RRP': 'radius_of_last_closed_isobar', + "RRP": "radius_of_last_closed_isobar", # MRD - radius of max winds, 0 - 999 nm. - 'MRD': 'radius_of_maximum_winds', + "MRD": "radius_of_maximum_winds", # GUSTS - gusts, 0 through 995 kts. - 'GUSTS': 'gust_speed', + "GUSTS": "gust_speed", # EYE - eye diameter, 0 through 999 nm. - 'EYE': 'eye_diameter', + "EYE": "eye_diameter", # SUBREGION - subregion code: A - Arabian Sea, B - Bay of Bengal, C - Central Pacific, E - Eastern Pacific, L - Atlantic, P - South Pacific (135E - 120W), Q - South Atlantic, S - South IO (20E - 135E), W - Western Pacific - 'SUBREGION': 'subregion_code', + "SUBREGION": "subregion_code", # MAXSEAS - max seas: 0 through 999 ft. - 'MAXSEAS': 'maximum_wave_height', + "MAXSEAS": "maximum_wave_height", # INITIALS - Forecaster's initials, used for tau 0 WRNG, up to 3 chars. - 'INITIALS': 'forecaster_initials', + "INITIALS": "forecaster_initials", # DIR - storm direction in compass coordinates, 0 - 359 degrees. - 'DIR': 'direction', + "DIR": "direction", # SPEED - storm speed, 0 - 999 kts. - 'SPEED': 'speed', + "SPEED": "speed", # STORMNAME - literal storm name, NONAME or INVEST. TCcyx used pre-1999, where: cy = Annual cyclone number 01 through 99, x = Subregion code: W, A, B, S, P, C, E, L, Q. - 'STORMNAME': 'name', + "STORMNAME": "name", # user data section as indicated by USERDEFINED parameter. - 'USERDEFINED': 'extra_values', + "USERDEFINED": "extra_values", } FORT_22_FIELDS = { # Time Record number in column 29. There can be multiple lines for a given time record depending on the number of isotachs reported in the ATCF File - 'RECORD': 'record_number', + "RECORD": "record_number", # number of isotachs reported in the ATCF file for the corresponding Time record. - 'ISOTACHS': 'num_isotachs', + "ISOTACHS": "num_isotachs", # Columns 31-34 indicate the selection of radii for that particular isotach. 0 indicates do not use this radius, and 1 indicates use this radius and corresponding wind speed. - 'ISOTACHSEL1': 'isotach_select_1', - 'ISOTACHSEL2': 'isotach_select_2', - 'ISOTACHSEL3': 'isotach_select_3', - 'ISOTACHSEL4': 'isotach_select_4', + "ISOTACHSEL1": "isotach_select_1", + "ISOTACHSEL2": "isotach_select_2", + "ISOTACHSEL3": "isotach_select_3", + "ISOTACHSEL4": "isotach_select_4", # Columns 35-38 are the designated Rmax values computed for each of the quadrants selected for each particular isotach. - 'RMAXQUADRANT1': 'radius_of_maximum_winds_quadrant_1', - 'RMAXQUADRANT2': 'radius_of_maximum_winds_quadrant_2', - 'RMAXQUADRANT3': 'radius_of_maximum_winds_quadrant_3', - 'RMAXQUADRANT4': 'radius_of_maximum_winds_quadrant_4', + "RMAXQUADRANT1": "radius_of_maximum_winds_quadrant_1", + "RMAXQUADRANT2": "radius_of_maximum_winds_quadrant_2", + "RMAXQUADRANT3": "radius_of_maximum_winds_quadrant_3", + "RMAXQUADRANT4": "radius_of_maximum_winds_quadrant_4", # Column 39 is the Holland B parameter computed using the formulas outlines in the Holland paper, and implemented using the aswip program. - 'HOLLANDB': 'holland_b', + "HOLLANDB": "holland_b", # Column 40-43 is the quadrant-varying Holland B parameter - 'HOLLANDB1': 'holland_b_quadrant_1', - 'HOLLANDB2': 'holland_b_quadrant_2', - 'HOLLANDB3': 'holland_b_quadrant_3', - 'HOLLANDB4': 'holland_b_quadrant_4', + "HOLLANDB1": "holland_b_quadrant_1", + "HOLLANDB2": "holland_b_quadrant_2", + "HOLLANDB3": "holland_b_quadrant_3", + "HOLLANDB4": "holland_b_quadrant_4", # Column 44-47 are the quadrant-varying Vmax calculated at the top of the planetary boundary (a wind reduction factor is applied to reduce the wind speed at the boundary to the 10-m surface) - 'VMAX1': 'max_sustained_wind_speed_1', - 'VMAX2': 'max_sustained_wind_speed_2', - 'VMAX3': 'max_sustained_wind_speed_3', - 'VMAX4': 'max_sustained_wind_speed_4', + "VMAX1": "max_sustained_wind_speed_1", + "VMAX2": "max_sustained_wind_speed_2", + "VMAX3": "max_sustained_wind_speed_3", + "VMAX4": "max_sustained_wind_speed_4", } EXTRA_ATCF_FIELDS = { # DEPTH - system depth, D-deep, M-medium, S-shallow, X-unknown - 'DEPTH': 'depth_code', + "DEPTH": "depth_code", # SEAS - Wave height for radii defined in SEAS1-SEAS4, 0-99 ft. - 'SEAS': 'isowave', + "SEAS": "isowave", # SEASCODE - Radius code: AAA - full circle, QQQ - quadrant (NNQ, NEQ, EEQ, SEQ, SSQ, SWQ, WWQ, NWQ) - 'SEASCODE': 'isowave_quadrant_code', + "SEASCODE": "isowave_quadrant_code", # SEAS1 - first quadrant seas radius as defined by SEASCODE, 0 through 999 nm. - 'SEAS1': 'isowave_radius_for_NEQ', + "SEAS1": "isowave_radius_for_NEQ", # SEAS2 - second quadrant seas radius as defined by SEASCODE, 0 through 999 nm. - 'SEAS2': 'isowave_radius_for_SEQ', + "SEAS2": "isowave_radius_for_SEQ", # SEAS3 - third quadrant seas radius as defined by SEASCODE, 0 through 999 nm. - 'SEAS3': 'isowave_radius_for_NWQ', + "SEAS3": "isowave_radius_for_NWQ", # SEAS4 - fourth quadrant seas radius as defined by SEASCODE, 0 through 999 nm. - 'SEAS4': 'isowave_radius_for_SWQ', + "SEAS4": "isowave_radius_for_SWQ", # user data section as indicated by USERDEFINED parameter. - 'USERDEFINED': 'extra_values', + "USERDEFINED": "extra_values", } def atcf_files( - file_deck: 'ATCF_FileDeck' = None, mode: 'ATCF_Mode' = None, year: int = None + file_deck: "ATCF_FileDeck" = None, mode: "ATCF_Mode" = None, year: int = None ) -> List[str]: if file_deck is None: return list( @@ -165,18 +169,21 @@ def atcf_files( year = range(ATCF_RECORD_START_YEAR, datetime.today().year + 1) return list( itertools.chain( - *(atcf_files(file_deck=file_deck, mode=mode, year=entry) for entry in year) + *( + atcf_files(file_deck=file_deck, mode=mode, year=entry) + for entry in year + ) ) ) url = atcf_url(file_deck=file_deck, mode=mode, year=year) - hostname, directory = url.split('/', 3)[2:] - ftp = ftplib.FTP(hostname.replace('ftp://', ''), 'anonymous', '') + hostname, directory = url.split("/", 3)[2:] + ftp = ftplib.FTP(hostname.replace("ftp://", ""), "anonymous", "") filenames = [ filename for filename, metadata in ftp.mlsd(directory) - if metadata['type'] == 'file' and filename[0] == file_deck.value + if metadata["type"] == "file" and filename[0] == file_deck.value ] filenames = sorted((filename for filename in filenames), reverse=True) @@ -193,67 +200,75 @@ class ATCF_FileDeck(Enum): The contents of each type of data file is described at http://hurricanes.ral.ucar.edu/realtime/ """ - ADVISORY = 'a' - BEST = 'b' # "best track" - FIXED = 'f' # https://www.nrlmry.navy.mil/atcf_web/docs/database/new/newfdeck.txt + ADVISORY = "a" + BEST = "b" # "best track" + FIXED = "f" # https://www.nrlmry.navy.mil/atcf_web/docs/database/new/newfdeck.txt class ATCF_Mode(Enum): - HISTORICAL = 'ARCHIVE' - REALTIME = 'aid_public' + HISTORICAL = "ARCHIVE" + REALTIME = "aid_public" class ATCF_Advisory(Enum): - BEST = 'BEST' - OFCL = 'OFCL' - OFCP = 'OFCP' - HMON = 'HMON' - CARQ = 'CARQ' - HWRF = 'HWRF' + BEST = "BEST" + OFCL = "OFCL" + OFCP = "OFCP" + HMON = "HMON" + CARQ = "CARQ" + HWRF = "HWRF" def get_atcf_entry( - year: int, basin: str = None, storm_number: int = None, storm_name: str = None, + year: int, + basin: str = None, + storm_number: int = None, + storm_name: str = None, ) -> Series: storms = nhc_storms(year=year) if storm_name is None and (basin is None and storm_number is None): - raise ValueError('need either storm name + year OR basin + storm number + year') + raise ValueError("need either storm name + year OR basin + storm number + year") if basin is not None: - storms = storms[storms['basin'].str.contains(basin.upper())] + storms = storms[storms["basin"].str.contains(basin.upper())] if storm_number is not None: - storms = storms[storms['number'] == storm_number] + storms = storms[storms["number"] == storm_number] if storm_name is not None: - storms = storms[storms['name'].str.contains(storm_name.upper())] + storms = storms[storms["name"].str.contains(storm_name.upper())] if len(storms) > 0: - storms['name'] = storms['name'].str.strip() - storms['class'] = storms['class'].str.strip() - storms['basin'] = storms['basin'].str.strip() - storms['source'] = storms['source'].str.strip() + storms["name"] = storms["name"].str.strip() + storms["class"] = storms["class"].str.strip() + storms["basin"] = storms["basin"].str.strip() + storms["source"] = storms["source"].str.strip() return storms.iloc[0] else: - message = f'no storms with given info' + message = f"no storms with given info" if storm_name is not None: message = f'{message} ("{storm_name}")' else: message = f'{message} ("{basin}{storm_number}")' - message = f'{message} found in {year}' + message = f"{message} found in {year}" raise ValueError(message) def atcf_url( - nhc_code: str = None, file_deck: ATCF_FileDeck = None, mode: ATCF_Mode = None, year=None, + nhc_code: str = None, + file_deck: ATCF_FileDeck = None, + mode: ATCF_Mode = None, + year=None, ) -> str: if nhc_code is not None: year = int(nhc_code[4:]) if mode is None: if nhc_code is None: - raise ValueError('NHC storm code not given') - entry = get_atcf_entry(basin=nhc_code[:2], storm_number=int(nhc_code[2:4]), year=year) - if entry['source'] == 'ARCHIVE': + raise ValueError("NHC storm code not given") + entry = get_atcf_entry( + basin=nhc_code[:2], storm_number=int(nhc_code[2:4]), year=year + ) + if entry["source"] == "ARCHIVE": mode = ATCF_Mode.HISTORICAL else: mode = ATCF_Mode.REALTIME @@ -274,26 +289,26 @@ def atcf_url( if mode == ATCF_Mode.HISTORICAL: if year is None: - raise ValueError('NHC storm code not given') - nhc_dir = f'archive/{year}' - suffix = '.dat.gz' + raise ValueError("NHC storm code not given") + nhc_dir = f"archive/{year}" + suffix = ".dat.gz" else: if file_deck == ATCF_FileDeck.ADVISORY: - nhc_dir = 'aid_public' - suffix = '.dat.gz' + nhc_dir = "aid_public" + suffix = ".dat.gz" elif file_deck == ATCF_FileDeck.BEST: - nhc_dir = 'btk' - suffix = '.dat' + nhc_dir = "btk" + suffix = ".dat" elif file_deck == ATCF_FileDeck.FIXED: - nhc_dir = 'fix' - suffix = '.dat' + nhc_dir = "fix" + suffix = ".dat" else: raise NotImplementedError(f'filedeck "{file_deck}" is not implemented') - url = f'ftp://ftp.nhc.noaa.gov/atcf/{nhc_dir}/' + url = f"ftp://ftp.nhc.noaa.gov/atcf/{nhc_dir}/" if nhc_code is not None: - url += f'{file_deck.value}{nhc_code.lower()}{suffix}' + url += f"{file_deck.value}{nhc_code.lower()}{suffix}" return url @@ -313,16 +328,18 @@ def read_atcf( """ if advisories is not None: - advisories = [typepigeon.convert_value(advisory, str) for advisory in advisories] + advisories = [ + typepigeon.convert_value(advisory, str) for advisory in advisories + ] if isinstance(atcf, (str, PathLike, Path)): atcf = open(atcf) - lines = (str(line, 'UTF-8') if isinstance(line, bytes) else line for line in atcf) + lines = (str(line, "UTF-8") if isinstance(line, bytes) else line for line in atcf) lines = ( ( entry.strip() - for entry in line.split(',', maxsplit=len(ATCF_FIELDS) - 1) + for entry in line.split(",", maxsplit=len(ATCF_FIELDS) - 1) if ~pandas.isna(line) ) for line in lines @@ -337,96 +354,114 @@ def read_atcf( if column not in data.columns: data[column] = pandas.NA data.astype( - {field: 'string' for field in data.columns}, copy=False, + {field: "string" for field in data.columns}, + copy=False, ) - if 'USERDEFINED' in data and data['USERDEFINED'].str.contains(',').any(): + if "USERDEFINED" in data and data["USERDEFINED"].str.contains(",").any(): if fort_22: extra_fields = FORT_22_FIELDS else: extra_fields = EXTRA_ATCF_FIELDS try: - lines = (str(line, 'UTF-8') if isinstance(line, bytes) else line for line in atcf) + lines = ( + str(line, "UTF-8") if isinstance(line, bytes) else line for line in atcf + ) lines = ( ( entry.strip() - for entry in line.split(',', maxsplit=len(extra_fields) - 1) + for entry in line.split(",", maxsplit=len(extra_fields) - 1) if ~pandas.isna(line) ) for line in lines ) - extra_data = DataFrame.from_records(lines, columns=list(extra_fields),).astype( - {field: 'string' for field in extra_fields} - ) + extra_data = DataFrame.from_records( + lines, + columns=list(extra_fields), + ).astype({field: "string" for field in extra_fields}) data = pandas.concat([data.iloc[:, :-1], extra_data], axis=1) except ValueError: pass if advisories is not None and len(advisories) > 0: - data = data[data['TECH'].isin(advisories)] + data = data[data["TECH"].isin(advisories)] if len(data) == 0: raise ValueError(f'no ATCF records found matching "{advisories}"') - best_track_records = (data['TECH'] == 'BEST') & ( - data.loc[data['TECH'] == 'BEST', 'TECHNUM/MIN'].str.strip().str.len() > 0 + best_track_records = (data["TECH"] == "BEST") & ( + data.loc[data["TECH"] == "BEST", "TECHNUM/MIN"].str.strip().str.len() > 0 ) - data.loc[best_track_records, 'YYYYMMDDHH'] += data.loc[best_track_records, 'TECHNUM/MIN'] - data.loc[~best_track_records, 'YYYYMMDDHH'] += '00' - data['YYYYMMDDHH'] = pandas.to_datetime(data['YYYYMMDDHH'], format='%Y%m%d%H%M') - - data.loc[data['LatN/S'].str.endswith('N'), 'LatN/S'] = data['LatN/S'].str.strip('N') - data.loc[data['LatN/S'].str.endswith('S'), 'LatN/S'] = '-' + data['LatN/S'].str.strip('S') - - data.loc[data['LonE/W'].str.endswith('E'), 'LonE/W'] = data['LonE/W'].str.strip('E') - data.loc[data['LonE/W'].str.endswith('W'), 'LonE/W'] = '-' + data['LonE/W'].str.strip('W') - - data[['LatN/S', 'LonE/W']] = ( - data[['LatN/S', 'LonE/W']].astype({'LatN/S': float, 'LonE/W': float}, copy=False) / 10 + data.loc[best_track_records, "YYYYMMDDHH"] += data.loc[ + best_track_records, "TECHNUM/MIN" + ] + data.loc[~best_track_records, "YYYYMMDDHH"] += "00" + data["YYYYMMDDHH"] = pandas.to_datetime(data["YYYYMMDDHH"], format="%Y%m%d%H%M") + + data.loc[data["LatN/S"].str.endswith("N"), "LatN/S"] = data["LatN/S"].str.strip("N") + data.loc[data["LatN/S"].str.endswith("S"), "LatN/S"] = "-" + data[ + "LatN/S" + ].str.strip("S") + + data.loc[data["LonE/W"].str.endswith("E"), "LonE/W"] = data["LonE/W"].str.strip("E") + data.loc[data["LonE/W"].str.endswith("W"), "LonE/W"] = "-" + data[ + "LonE/W" + ].str.strip("W") + + data[["LatN/S", "LonE/W"]] = ( + data[["LatN/S", "LonE/W"]].astype( + {"LatN/S": float, "LonE/W": float}, copy=False + ) + / 10 ) - if pandas.isna(data['RAD']).any(): + if pandas.isna(data["RAD"]).any(): raise ValueError( - 'Error: No radial wind information for this storm; ' - 'parametric wind model cannot be built.' + "Error: No radial wind information for this storm; " + "parametric wind model cannot be built." ) float_fields = [ field for field in ( - 'VMAX', - 'MSLP', - 'RAD', - 'RAD1', - 'RAD2', - 'RAD3', - 'RAD4', - 'RADP', - 'RRP', - 'MRD', - 'GUSTS', - 'EYE', - 'MAXSEAS', - 'DIR', - 'SPEED', - 'SEAS', - 'SEAS1', - 'SEAS2', - 'SEAS3', - 'SEAS4', + "VMAX", + "MSLP", + "RAD", + "RAD1", + "RAD2", + "RAD3", + "RAD4", + "RADP", + "RRP", + "MRD", + "GUSTS", + "EYE", + "MAXSEAS", + "DIR", + "SPEED", + "SEAS", + "SEAS1", + "SEAS2", + "SEAS3", + "SEAS4", ) if field in data.columns ] for float_field in float_fields: data.loc[ - (data[float_field].str.len() == 0) | pandas.isna(data[float_field]), float_field - ] = 'NaN' + (data[float_field].str.len() == 0) | pandas.isna(data[float_field]), + float_field, + ] = "NaN" data[float_fields] = data[float_fields].astype(float, copy=False) data.rename( columns={ - **{field: value for field, value in ATCF_FIELDS.items() if field in data.columns}, + **{ + field: value + for field, value in ATCF_FIELDS.items() + if field in data.columns + }, **{ field: value for field, value in EXTRA_ATCF_FIELDS.items() @@ -442,5 +477,9 @@ def read_atcf( ) return GeoDataFrame( - data, geometry=geopandas.points_from_xy(data['longitude'], data['latitude'],) + data, + geometry=geopandas.points_from_xy( + data["longitude"], + data["latitude"], + ), ) diff --git a/stormevents/nhc/storms.py b/stormevents/nhc/storms.py index 9aaeba0..29e8896 100644 --- a/stormevents/nhc/storms.py +++ b/stormevents/nhc/storms.py @@ -1,12 +1,12 @@ +import re +from collections.abc import Iterable from datetime import datetime from functools import lru_cache -import re -from typing import Iterable -from bs4 import BeautifulSoup import numpy import pandas import requests +from bs4 import BeautifulSoup NHC_GIS_ARCHIVE_START_YEAR = 2008 @@ -36,145 +36,146 @@ def nhc_storms(year: int = None) -> pandas.DataFrame: [2714 rows x 8 columns] """ - url = 'https://ftp.nhc.noaa.gov/atcf/index/storm_list.txt' + url = "https://ftp.nhc.noaa.gov/atcf/index/storm_list.txt" columns = [ - 'name', - 'basin', + "name", + "basin", 2, 3, 4, 5, 6, - 'number', - 'year', - 'class', + "number", + "year", + "class", 10, - 'start_date', - 'end_date', + "start_date", + "end_date", 13, 14, 15, 16, 17, - 'source', + "source", 19, - 'nhc_code', + "nhc_code", ] storms = pandas.read_csv( url, header=0, names=columns, - parse_dates=['start_date', 'end_date'], - date_parser=lambda x: pandas.to_datetime(x.strip(), format='%Y%m%d%H') - if x.strip() != '9999999999' + parse_dates=["start_date", "end_date"], + date_parser=lambda x: pandas.to_datetime(x.strip(), format="%Y%m%d%H") + if x.strip() != "9999999999" else numpy.nan, ) storms = storms.astype( - {'start_date': 'datetime64[s]', 'end_date': 'datetime64[s]'}, copy=False, + {"start_date": "datetime64[s]", "end_date": "datetime64[s]"}, + copy=False, ) storms = storms[ [ - 'nhc_code', - 'name', - 'class', - 'year', - 'basin', - 'number', - 'source', - 'start_date', - 'end_date', + "nhc_code", + "name", + "class", + "year", + "basin", + "number", + "source", + "start_date", + "end_date", ] ] if year is not None: if isinstance(year, Iterable) and not isinstance(year, str): - storms = storms[storms['year'].isin(year)] + storms = storms[storms["year"].isin(year)] else: - storms = storms[storms['year'] == int(year)] + storms = storms[storms["year"] == int(year)] - storms['nhc_code'] = storms['nhc_code'].str.strip() - storms.set_index('nhc_code', inplace=True) + storms["nhc_code"] = storms["nhc_code"].str.strip() + storms.set_index("nhc_code", inplace=True) gis_archive_storms = nhc_storms_gis_archive(year=year) gis_archive_storms = gis_archive_storms.drop( gis_archive_storms[gis_archive_storms.index.isin(storms.index)].index ) if len(gis_archive_storms) > 0: - gis_archive_storms[['start_date', 'end_date']] = pandas.to_datetime(numpy.nan) + gis_archive_storms[["start_date", "end_date"]] = pandas.to_datetime(numpy.nan) storms = pandas.concat([storms, gis_archive_storms[storms.columns]]) - for string_column in ['name', 'class', 'source']: + for string_column in ["name", "class", "source"]: storms.loc[storms[string_column].str.len() == 0, string_column] = pandas.NA storms[string_column] = storms[string_column].str.strip() - storms[string_column] = storms[string_column].astype('string') + storms[string_column] = storms[string_column].astype("string") - storms.sort_values(['year', 'number', 'basin'], inplace=True) + storms.sort_values(["year", "number", "basin"], inplace=True) return storms @lru_cache(maxsize=None) def nhc_storms_archive(year: int = None) -> pandas.DataFrame: - url = 'https://ftp.nhc.noaa.gov/atcf/archive/storm.table' + url = "https://ftp.nhc.noaa.gov/atcf/archive/storm.table" columns = [ - 'name', - 'basin', + "name", + "basin", 2, 3, 4, 5, 6, - 'number', - 'year', - 'class', + "number", + "year", + "class", 10, - 'start_date', - 'end_date', + "start_date", + "end_date", 13, 14, 15, 16, 17, - 'source', + "source", 19, - 'nhc_code', + "nhc_code", ] storms = pandas.read_csv(url, header=0, names=columns) storms = storms[ [ - 'nhc_code', - 'name', - 'class', - 'year', - 'basin', - 'number', - 'source', - 'start_date', - 'end_date', + "nhc_code", + "name", + "class", + "year", + "basin", + "number", + "source", + "start_date", + "end_date", ] ] if year is not None: if isinstance(year, Iterable) and not isinstance(year, str): - storms = storms[storms['year'].isin(year)] + storms = storms[storms["year"].isin(year)] else: - storms = storms[storms['year'] == int(year)] + storms = storms[storms["year"] == int(year)] - storms['nhc_code'] = storms['nhc_code'].str.strip() - storms.set_index('nhc_code', inplace=True) + storms["nhc_code"] = storms["nhc_code"].str.strip() + storms.set_index("nhc_code", inplace=True) - storms.sort_values(['year', 'number', 'basin'], inplace=True) + storms.sort_values(["year", "number", "basin"], inplace=True) - for string_column in ['name', 'class', 'source']: + for string_column in ["name", "class", "source"]: storms.loc[storms[string_column].str.len() == 0, string_column] = pandas.NA storms[string_column] = storms[string_column].str.strip() - storms[string_column] = storms[string_column].astype('string') + storms[string_column] = storms[string_column].astype("string") return storms @@ -219,49 +220,52 @@ def nhc_storms_gis_archive(year: int = None) -> pandas.DataFrame: elif not isinstance(year, int): year = int(year) - url = 'http://www.nhc.noaa.gov/gis/archive_wsurge.php' - response = requests.get(url, params={'year': year}) + url = "http://www.nhc.noaa.gov/gis/archive_wsurge.php" + response = requests.get(url, params={"year": year}) if not response.ok: response.raise_for_status() - soup = BeautifulSoup(response.content, features='html.parser') - table = soup.find('table') + soup = BeautifulSoup(response.content, features="html.parser") + table = soup.find("table") rows = [] - for row in table.find_all('tr')[1:]: - identifier, long_name = (entry.text for entry in row.find_all('td')) + for row in table.find_all("tr")[1:]: + identifier, long_name = (entry.text for entry in row.find_all("td")) short_name = long_name.split()[-1] - rows.append((f'{identifier}{year}', short_name, long_name, year)) + rows.append((f"{identifier}{year}", short_name, long_name, year)) - storms = pandas.DataFrame(rows, columns=['nhc_code', 'name', 'long_name', 'year']) - storms['nhc_code'] = storms['nhc_code'].str.upper() - storms.set_index('nhc_code', inplace=True) + storms = pandas.DataFrame(rows, columns=["nhc_code", "name", "long_name", "year"]) + storms["nhc_code"] = storms["nhc_code"].str.upper() + storms.set_index("nhc_code", inplace=True) - storms['number'] = storms.index.str.slice(2, 4).astype(int) - storms['basin'] = storms.index.str.slice(0, 2) + storms["number"] = storms.index.str.slice(2, 4).astype(int) + storms["basin"] = storms.index.str.slice(0, 2) - storms['class'] = None + storms["class"] = None storms.loc[ - storms['long_name'].str.contains('Tropical Cyclone', flags=re.IGNORECASE) - | storms['long_name'].str.contains('Hurricane', flags=re.IGNORECASE), - 'class', - ] = 'HU' + storms["long_name"].str.contains("Tropical Cyclone", flags=re.IGNORECASE) + | storms["long_name"].str.contains("Hurricane", flags=re.IGNORECASE), + "class", + ] = "HU" storms.loc[ - storms['long_name'].str.contains('Tropical Storm', flags=re.IGNORECASE), 'class', - ] = 'TS' + storms["long_name"].str.contains("Tropical Storm", flags=re.IGNORECASE), + "class", + ] = "TS" storms.loc[ - storms['long_name'].str.contains('Tropical Depression', flags=re.IGNORECASE), 'class', - ] = 'TD' + storms["long_name"].str.contains("Tropical Depression", flags=re.IGNORECASE), + "class", + ] = "TD" storms.loc[ - storms['long_name'].str.contains('Subtropical', flags=re.IGNORECASE), 'class', - ] = 'ST' + storms["long_name"].str.contains("Subtropical", flags=re.IGNORECASE), + "class", + ] = "ST" - storms['source'] = 'GIS_ARCHIVE' + storms["source"] = "GIS_ARCHIVE" - storms.sort_values(['year', 'basin', 'number'], inplace=True) + storms.sort_values(["year", "basin", "number"], inplace=True) - for string_column in ['name', 'class', 'source']: + for string_column in ["name", "class", "source"]: storms.loc[storms[string_column].str.len() == 0, string_column] = pandas.NA storms[string_column] = storms[string_column].str.strip() - storms[string_column] = storms[string_column].astype('string') + storms[string_column] = storms[string_column].astype("string") - return storms[['name', 'class', 'year', 'basin', 'number', 'source']] + return storms[["name", "class", "year", "basin", "number", "source"]] diff --git a/stormevents/nhc/track.py b/stormevents/nhc/track.py index 63039e8..d10fbcd 100644 --- a/stormevents/nhc/track.py +++ b/stormevents/nhc/track.py @@ -1,32 +1,36 @@ -from datetime import datetime, timedelta -from functools import partial import gzip import io import logging -from os import PathLike import pathlib import re -from typing import Any, Dict, List, Union +from datetime import datetime +from datetime import timedelta +from functools import partial +from os import PathLike +from typing import Any +from typing import Dict +from typing import List +from typing import Union from urllib.error import URLError from urllib.request import urlopen import numpy import pandas +import typepigeon from pandas import DataFrame from pyproj import Geod from shapely import ops -from shapely.geometry import LineString, MultiPolygon, Polygon -import typepigeon - -from stormevents.nhc.atcf import ( - ATCF_Advisory, - ATCF_FileDeck, - ATCF_Mode, - atcf_url, - EXTRA_ATCF_FIELDS, - get_atcf_entry, - read_atcf, -) +from shapely.geometry import LineString +from shapely.geometry import MultiPolygon +from shapely.geometry import Polygon + +from stormevents.nhc.atcf import ATCF_Advisory +from stormevents.nhc.atcf import ATCF_FileDeck +from stormevents.nhc.atcf import ATCF_Mode +from stormevents.nhc.atcf import atcf_url +from stormevents.nhc.atcf import EXTRA_ATCF_FIELDS +from stormevents.nhc.atcf import get_atcf_entry +from stormevents.nhc.atcf import read_atcf from stormevents.nhc.storms import nhc_storms from stormevents.utilities import subset_time_interval @@ -112,7 +116,7 @@ def from_storm_name( end_date: datetime = None, file_deck: ATCF_FileDeck = None, advisories: [ATCF_Advisory] = None, - ) -> 'VortexTrack': + ) -> "VortexTrack": """ :param name: storm name :param year: storm year @@ -138,8 +142,11 @@ def from_storm_name( @classmethod def from_file( - cls, path: PathLike, start_date: datetime = None, end_date: datetime = None, - ) -> 'VortexTrack': + cls, + path: PathLike, + start_date: datetime = None, + end_date: datetime = None, + ) -> "VortexTrack": """ :param path: file path to ATCF data :param start_date: start date of track @@ -170,17 +177,17 @@ def name(self) -> str: if self.__name is None: # get the most frequently-used storm name in the data - names = self.data['name'].value_counts() + names = self.data["name"].value_counts() if len(names) > 0: name = names.index[0] else: - name = '' + name = "" - if name.strip() == '': + if name.strip() == "": storms = nhc_storms(year=self.year) if self.nhc_code.upper() in storms.index: storm = storms.loc[self.nhc_code.upper()] - name = storm['name'].lower() + name = storm["name"].lower() self.__name = name @@ -196,7 +203,7 @@ def basin(self) -> str: 'AL' """ - return self.data['basin'].iloc[0] + return self.data["basin"].iloc[0] @property def storm_number(self) -> str: @@ -208,7 +215,7 @@ def storm_number(self) -> str: 11 """ - return self.data['storm_number'].iloc[0] + return self.data["storm_number"].iloc[0] @property def year(self) -> int: @@ -220,7 +227,7 @@ def year(self) -> int: 2017 """ - return self.data['datetime'].iloc[0].year + return self.data["datetime"].iloc[0].year @property def nhc_code(self) -> str: @@ -244,8 +251,8 @@ def nhc_code(self) -> str: except ValueError: try: nhc_code = get_atcf_entry( - storm_name=self.__unfiltered_data['name'].tolist()[-1], - year=self.__unfiltered_data['datetime'].tolist()[-1].year, + storm_name=self.__unfiltered_data["name"].tolist()[-1], + year=self.__unfiltered_data["datetime"].tolist()[-1].year, ).name self.nhc_code = nhc_code except ValueError: @@ -263,7 +270,7 @@ def nhc_code(self, nhc_code: str): storm_name=nhc_code[:-4], year=int(nhc_code[-4:]) ).name if atcf_nhc_code is None: - raise ValueError(f'No storm with id: {nhc_code}') + raise ValueError(f"No storm with id: {nhc_code}") nhc_code = atcf_nhc_code self.__nhc_code = nhc_code @@ -291,15 +298,17 @@ def start_date(self) -> pandas.Timestamp: @start_date.setter def start_date(self, start_date: datetime): - data_start = self.unfiltered_data['datetime'].iloc[0] + data_start = self.unfiltered_data["datetime"].iloc[0] if start_date is None: start_date = data_start else: # interpret timedelta as a temporal movement around start / end - data_end = self.unfiltered_data['datetime'].iloc[-1] + data_end = self.unfiltered_data["datetime"].iloc[-1] start_date, _ = subset_time_interval( - start=data_start, end=data_end, subset_start=start_date, + start=data_start, + end=data_end, + subset_start=start_date, ) if not isinstance(start_date, pandas.Timestamp): start_date = pandas.to_datetime(start_date) @@ -330,15 +339,17 @@ def end_date(self) -> pandas.Timestamp: @end_date.setter def end_date(self, end_date: datetime): - data_end = self.unfiltered_data['datetime'].iloc[-1] + data_end = self.unfiltered_data["datetime"].iloc[-1] if end_date is None: end_date = data_end else: # interpret timedelta as a temporal movement around start / end - data_start = self.unfiltered_data['datetime'].iloc[0] + data_start = self.unfiltered_data["datetime"].iloc[0] _, end_date = subset_time_interval( - start=data_start, end=data_end, subset_end=end_date, + start=data_start, + end=data_end, + subset_end=end_date, ) if not isinstance(end_date, pandas.Timestamp): end_date = pandas.to_datetime(end_date) @@ -405,7 +416,9 @@ def __valid_advisories(self) -> List[ATCF_Advisory]: elif self.file_deck == ATCF_FileDeck.FIXED: valid_advisories = [entry.value for entry in ATCF_Advisory] else: - raise NotImplementedError(f'file deck {self.file_deck.value} not implemented') + raise NotImplementedError( + f"file deck {self.file_deck.value} not implemented" + ) return valid_advisories @@ -462,11 +475,13 @@ def data(self) -> DataFrame: """ return self.unfiltered_data.loc[ - (self.unfiltered_data['datetime'] >= self.start_date) - & (self.unfiltered_data['datetime'] <= self.end_date) + (self.unfiltered_data["datetime"] >= self.start_date) + & (self.unfiltered_data["datetime"] <= self.end_date) ] - def to_file(self, path: PathLike, advisory: ATCF_Advisory = None, overwrite: bool = False): + def to_file( + self, path: PathLike, advisory: ATCF_Advisory = None, overwrite: bool = False + ): """ write track to file path @@ -479,14 +494,14 @@ def to_file(self, path: PathLike, advisory: ATCF_Advisory = None, overwrite: boo path = pathlib.Path(path) if overwrite or not path.exists(): - if path.suffix == '.dat': + if path.suffix == ".dat": data = self.atcf(advisory=advisory) data.to_csv(path, index=False, header=False) - elif path.suffix == '.22': + elif path.suffix == ".22": data = self.fort_22(advisory=advisory) data.to_csv(path, index=False, header=False) else: - raise NotImplementedError(f'writing to `*{path.suffix}` not supported') + raise NotImplementedError(f"writing to `*{path.suffix}` not supported") else: logging.warning(f'skipping existing file "{path}"') @@ -501,123 +516,131 @@ def atcf(self, advisory: ATCF_Advisory = None) -> DataFrame: """ atcf = self.data.copy(deep=True) - atcf.loc[atcf['advisory'] != 'BEST', 'datetime'] = atcf.loc[ - atcf['advisory'] != 'BEST', 'track_start_time' + atcf.loc[atcf["advisory"] != "BEST", "datetime"] = atcf.loc[ + atcf["advisory"] != "BEST", "track_start_time" ] - atcf.drop(columns=['geometry', 'track_start_time'], inplace=True) + atcf.drop(columns=["geometry", "track_start_time"], inplace=True) if advisory is not None: if isinstance(advisory, ATCF_Advisory): advisory = advisory.value - atcf = atcf[atcf['advisory'] == advisory] + atcf = atcf[atcf["advisory"] == advisory] - atcf.loc[:, ['longitude', 'latitude']] = atcf.loc[:, ['longitude', 'latitude']] * 10 + atcf.loc[:, ["longitude", "latitude"]] = ( + atcf.loc[:, ["longitude", "latitude"]] * 10 + ) - float_columns = atcf.select_dtypes(include=['float']).columns + float_columns = atcf.select_dtypes(include=["float"]).columns integer_na_value = -99999 for column in float_columns: atcf.loc[pandas.isna(atcf[column]), column] = integer_na_value atcf.loc[:, column] = atcf.loc[:, column].round(0).astype(int) - atcf['basin'] = atcf['basin'].str.pad(2) - atcf['storm_number'] = atcf['storm_number'].astype('string').str.pad(3) - atcf['datetime'] = atcf['datetime'].dt.strftime('%Y%m%d%H').str.pad(11) - atcf['advisory_number'] = atcf['advisory_number'].str.pad(3) - atcf['advisory'] = atcf['advisory'].str.pad(5) - atcf['forecast_hours'] = atcf['forecast_hours'].astype('string').str.pad(4) + atcf["basin"] = atcf["basin"].str.pad(2) + atcf["storm_number"] = atcf["storm_number"].astype("string").str.pad(3) + atcf["datetime"] = atcf["datetime"].dt.strftime("%Y%m%d%H").str.pad(11) + atcf["advisory_number"] = atcf["advisory_number"].str.pad(3) + atcf["advisory"] = atcf["advisory"].str.pad(5) + atcf["forecast_hours"] = atcf["forecast_hours"].astype("string").str.pad(4) - atcf['latitude'] = atcf['latitude'].astype('string') - atcf.loc[~atcf['latitude'].str.contains('-'), 'latitude'] = ( - atcf.loc[~atcf['latitude'].str.contains('-'), 'latitude'] + 'N' + atcf["latitude"] = atcf["latitude"].astype("string") + atcf.loc[~atcf["latitude"].str.contains("-"), "latitude"] = ( + atcf.loc[~atcf["latitude"].str.contains("-"), "latitude"] + "N" ) - atcf.loc[atcf['latitude'].str.contains('-'), 'latitude'] = ( - atcf.loc[atcf['latitude'].str.contains('-'), 'latitude'] + 'S' + atcf.loc[atcf["latitude"].str.contains("-"), "latitude"] = ( + atcf.loc[atcf["latitude"].str.contains("-"), "latitude"] + "S" ) - atcf['latitude'] = atcf['latitude'].str.strip('-').str.pad(5) + atcf["latitude"] = atcf["latitude"].str.strip("-").str.pad(5) - atcf['longitude'] = atcf['longitude'].astype('string') - atcf.loc[~atcf['longitude'].str.contains('-'), 'longitude'] = ( - atcf.loc[~atcf['longitude'].str.contains('-'), 'longitude'] + 'E' + atcf["longitude"] = atcf["longitude"].astype("string") + atcf.loc[~atcf["longitude"].str.contains("-"), "longitude"] = ( + atcf.loc[~atcf["longitude"].str.contains("-"), "longitude"] + "E" ) - atcf.loc[atcf['longitude'].str.contains('-'), 'longitude'] = ( - atcf.loc[atcf['longitude'].str.contains('-'), 'longitude'] + 'W' + atcf.loc[atcf["longitude"].str.contains("-"), "longitude"] = ( + atcf.loc[atcf["longitude"].str.contains("-"), "longitude"] + "W" ) - atcf['longitude'] = atcf['longitude'].str.strip('-').str.pad(6) + atcf["longitude"] = atcf["longitude"].str.strip("-").str.pad(6) - atcf['max_sustained_wind_speed'] = ( - atcf['max_sustained_wind_speed'].astype('string').str.pad(5) + atcf["max_sustained_wind_speed"] = ( + atcf["max_sustained_wind_speed"].astype("string").str.pad(5) ) - atcf['central_pressure'] = atcf['central_pressure'].astype('string').str.pad(5) - atcf['development_level'] = atcf['development_level'].str.pad(3) - atcf['isotach_radius'] = atcf['isotach_radius'].astype('string').str.pad(4) - atcf['isotach_quadrant_code'] = atcf['isotach_quadrant_code'].str.pad(4) - atcf['isotach_radius_for_NEQ'] = ( - atcf['isotach_radius_for_NEQ'].astype('string').str.pad(5) + atcf["central_pressure"] = atcf["central_pressure"].astype("string").str.pad(5) + atcf["development_level"] = atcf["development_level"].str.pad(3) + atcf["isotach_radius"] = atcf["isotach_radius"].astype("string").str.pad(4) + atcf["isotach_quadrant_code"] = atcf["isotach_quadrant_code"].str.pad(4) + atcf["isotach_radius_for_NEQ"] = ( + atcf["isotach_radius_for_NEQ"].astype("string").str.pad(5) ) - atcf['isotach_radius_for_SEQ'] = ( - atcf['isotach_radius_for_SEQ'].astype('string').str.pad(5) + atcf["isotach_radius_for_SEQ"] = ( + atcf["isotach_radius_for_SEQ"].astype("string").str.pad(5) ) - atcf['isotach_radius_for_NWQ'] = ( - atcf['isotach_radius_for_NWQ'].astype('string').str.pad(5) + atcf["isotach_radius_for_NWQ"] = ( + atcf["isotach_radius_for_NWQ"].astype("string").str.pad(5) ) - atcf['isotach_radius_for_SWQ'] = ( - atcf['isotach_radius_for_SWQ'].astype('string').str.pad(5) + atcf["isotach_radius_for_SWQ"] = ( + atcf["isotach_radius_for_SWQ"].astype("string").str.pad(5) ) - atcf['background_pressure'].fillna(method='ffill', inplace=True) + atcf["background_pressure"].fillna(method="ffill", inplace=True) atcf.loc[ - ~pandas.isna(self.data['central_pressure']) - & (self.data['background_pressure'] <= self.data['central_pressure']) - & (self.data['central_pressure'] < 1013), - 'background_pressure', - ] = '1013' + ~pandas.isna(self.data["central_pressure"]) + & (self.data["background_pressure"] <= self.data["central_pressure"]) + & (self.data["central_pressure"] < 1013), + "background_pressure", + ] = "1013" atcf.loc[ - ~pandas.isna(self.data['central_pressure']) - & (self.data['background_pressure'] <= self.data['central_pressure']) - & (self.data['central_pressure'] < 1013), - 'background_pressure', - ] = (self.data['central_pressure'] + 1) - atcf['background_pressure'] = ( - atcf['background_pressure'].astype(int).astype('string').str.pad(5) + ~pandas.isna(self.data["central_pressure"]) + & (self.data["background_pressure"] <= self.data["central_pressure"]) + & (self.data["central_pressure"] < 1013), + "background_pressure", + ] = ( + self.data["central_pressure"] + 1 + ) + atcf["background_pressure"] = ( + atcf["background_pressure"].astype(int).astype("string").str.pad(5) ) - atcf['radius_of_last_closed_isobar'] = ( - atcf['radius_of_last_closed_isobar'].astype('string').str.pad(5) + atcf["radius_of_last_closed_isobar"] = ( + atcf["radius_of_last_closed_isobar"].astype("string").str.pad(5) + ) + atcf["radius_of_maximum_winds"] = ( + atcf["radius_of_maximum_winds"].astype("string").str.pad(4) ) - atcf['radius_of_maximum_winds'] = ( - atcf['radius_of_maximum_winds'].astype('string').str.pad(4) + atcf["gust_speed"] = atcf["gust_speed"].astype("string").str.pad(4) + atcf["eye_diameter"] = atcf["eye_diameter"].astype("string").str.pad(4) + atcf["subregion_code"] = atcf["subregion_code"].str.pad(4) + atcf["maximum_wave_height"] = ( + atcf["maximum_wave_height"].astype("string").str.pad(4) ) - atcf['gust_speed'] = atcf['gust_speed'].astype('string').str.pad(4) - atcf['eye_diameter'] = atcf['eye_diameter'].astype('string').str.pad(4) - atcf['subregion_code'] = atcf['subregion_code'].str.pad(4) - atcf['maximum_wave_height'] = atcf['maximum_wave_height'].astype('string').str.pad(4) - atcf['forecaster_initials'] = atcf['forecaster_initials'].str.pad(4) - - atcf['direction'] = atcf['direction'].astype('string').str.pad(4) - atcf['speed'] = atcf['speed'].astype('string').str.pad(4) - atcf['name'] = atcf['name'].astype('string').str.pad(11) - - if 'depth_code' in atcf.columns: - atcf['depth_code'] = atcf['depth_code'].astype('string').str.pad(2) - atcf['isowave'] = atcf['isowave'].astype('string').str.pad(3) - atcf['isowave_quadrant_code'] = ( - atcf['isowave_quadrant_code'].astype('string').str.pad(4) + atcf["forecaster_initials"] = atcf["forecaster_initials"].str.pad(4) + + atcf["direction"] = atcf["direction"].astype("string").str.pad(4) + atcf["speed"] = atcf["speed"].astype("string").str.pad(4) + atcf["name"] = atcf["name"].astype("string").str.pad(11) + + if "depth_code" in atcf.columns: + atcf["depth_code"] = atcf["depth_code"].astype("string").str.pad(2) + atcf["isowave"] = atcf["isowave"].astype("string").str.pad(3) + atcf["isowave_quadrant_code"] = ( + atcf["isowave_quadrant_code"].astype("string").str.pad(4) ) - atcf['isowave_radius_for_NEQ'] = ( - atcf['isowave_radius_for_NEQ'].astype('string').str.pad(5) + atcf["isowave_radius_for_NEQ"] = ( + atcf["isowave_radius_for_NEQ"].astype("string").str.pad(5) ) - atcf['isowave_radius_for_SEQ'] = ( - atcf['isowave_radius_for_SEQ'].astype('string').str.pad(5) + atcf["isowave_radius_for_SEQ"] = ( + atcf["isowave_radius_for_SEQ"].astype("string").str.pad(5) ) - atcf['isowave_radius_for_NWQ'] = ( - atcf['isowave_radius_for_NWQ'].astype('string').str.pad(5) + atcf["isowave_radius_for_NWQ"] = ( + atcf["isowave_radius_for_NWQ"].astype("string").str.pad(5) ) - atcf['isowave_radius_for_SWQ'] = ( - atcf['isowave_radius_for_SWQ'].astype('string').str.pad(5) + atcf["isowave_radius_for_SWQ"] = ( + atcf["isowave_radius_for_SWQ"].astype("string").str.pad(5) ) - for column in atcf.select_dtypes(include=['string']).columns: - atcf[column] = atcf[column].str.replace(re.compile(str(integer_na_value)), '') + for column in atcf.select_dtypes(include=["string"]).columns: + atcf[column] = atcf[column].str.replace( + re.compile(str(integer_na_value)), "" + ) return atcf @@ -632,19 +655,21 @@ def fort_22(self, advisory: ATCF_Advisory = None) -> DataFrame: fort22 = self.atcf(advisory=advisory) fort22.drop( - columns=[field for field in EXTRA_ATCF_FIELDS.values() if field in fort22.columns], + columns=[ + field for field in EXTRA_ATCF_FIELDS.values() if field in fort22.columns + ], inplace=True, ) - fort22['longitude'] = fort22['longitude'].str.strip().str.pad(4) - fort22['latitude'] = fort22['latitude'].str.strip().str.pad(5) - fort22['gust_speed'] = fort22['gust_speed'].str.strip().str.pad(5) - fort22['direction'] = fort22['direction'].str.strip().str.pad(3) - fort22['name'] = fort22['name'].str.strip().str.pad(12) - fort22.loc[fort22['name'] == '', 'name'] = self.name + fort22["longitude"] = fort22["longitude"].str.strip().str.pad(4) + fort22["latitude"] = fort22["latitude"].str.strip().str.pad(5) + fort22["gust_speed"] = fort22["gust_speed"].str.strip().str.pad(5) + fort22["direction"] = fort22["direction"].str.strip().str.pad(3) + fort22["name"] = fort22["name"].str.strip().str.pad(12) + fort22.loc[fort22["name"] == "", "name"] = self.name - fort22['record_number'] = ( - (self.data.groupby(['datetime']).ngroup() + 1).astype('string').str.pad(4) + fort22["record_number"] = ( + (self.data.groupby(["datetime"]).ngroup() + 1).astype("string").str.pad(4) ) return fort22 @@ -669,7 +694,7 @@ def linestrings(self) -> Dict[str, Dict[str, LineString]]: for advisory, advisory_tracks in tracks.items(): linestrings[advisory] = {} for track_start_time, track in advisory_tracks.items(): - geometries = track['geometry'] + geometries = track["geometry"] if len(geometries) > 1: geometries = geometries.drop_duplicates() if len(geometries) > 1: @@ -695,7 +720,7 @@ def distances(self) -> Dict[str, Dict[str, float]]: or len(self.__distances) == 0 or configuration != self.__previous_configuration ): - geodetic = Geod(ellps='WGS84') + geodetic = Geod(ellps="WGS84") linestrings = self.linestrings @@ -704,7 +729,12 @@ def distances(self) -> Dict[str, Dict[str, float]]: distances[advisory] = {} for track_start_time, linestring in advisory_tracks.items(): x, y = linestring.xy - _, _, track_distances = geodetic.inv(x[:-1], y[:-1], x[1:], y[1:],) + _, _, track_distances = geodetic.inv( + x[:-1], + y[:-1], + x[1:], + y[1:], + ) distances[advisory][track_start_time] = numpy.sum(track_distances) self.__distances = distances @@ -725,23 +755,23 @@ def isotachs( valid_isotach_values = [34, 50, 64] assert ( wind_speed in valid_isotach_values - ), f'isotach must be one of {valid_isotach_values}' + ), f"isotach must be one of {valid_isotach_values}" # collect the attributes needed from the forcing to generate swath - data = self.data[self.data['isotach_radius'] == wind_speed] + data = self.data[self.data["isotach_radius"] == wind_speed] # enumerate quadrants quadrant_names = [ - 'isotach_radius_for_NEQ', - 'isotach_radius_for_NWQ', - 'isotach_radius_for_SWQ', - 'isotach_radius_for_SEQ', + "isotach_radius_for_NEQ", + "isotach_radius_for_NWQ", + "isotach_radius_for_SWQ", + "isotach_radius_for_SEQ", ] # convert quadrant radii from nautical miles to meters data[quadrant_names] *= 1852.0 - geodetic = Geod(ellps='WGS84') + geodetic = Geod(ellps="WGS84") tracks = separate_tracks(data) @@ -753,7 +783,7 @@ def isotachs( track_isotachs = {} for index, row in track_data.iterrows(): # get the starting angle range for NEQ based on storm direction - rotation_angle = 360 - row['direction'] + rotation_angle = 360 - row["direction"] start_angle = 0 + rotation_angle end_angle = 90 + rotation_angle @@ -773,20 +803,22 @@ def isotachs( vectorized_forward_geodetic = numpy.vectorize( partial( geodetic.fwd, - lons=row['longitude'], - lats=row['latitude'], + lons=row["longitude"], + lats=row["latitude"], dist=row[quadrant_name], ) ) - x, y, reverse_azimuth = vectorized_forward_geodetic(az=theta) + x, y, reverse_azimuth = vectorized_forward_geodetic( + az=theta + ) vertices = numpy.stack([x, y], axis=1) # insert center point at beginning and end of list vertices = numpy.concatenate( [ - row[['longitude', 'latitude']].values[None, :], + row[["longitude", "latitude"]].values[None, :], vertices, - row[['longitude', 'latitude']].values[None, :], + row[["longitude", "latitude"]].values[None, :], ], axis=0, ) @@ -836,7 +868,9 @@ def wind_swaths( if len(convex_hulls) > 0: # get the union of polygons - advisory_wind_swaths[track_start_time] = ops.unary_union(convex_hulls) + advisory_wind_swaths[track_start_time] = ops.unary_union( + convex_hulls + ) if len(advisory_isotachs) > 0: wind_swaths[advisory] = advisory_wind_swaths @@ -855,7 +889,7 @@ def duration(self) -> pandas.Timedelta: :return: duration of current track """ - return self.data['datetime'].diff().sum() + return self.data["datetime"].diff().sum() @property def unfiltered_data(self) -> DataFrame: @@ -872,57 +906,67 @@ def unfiltered_data(self) -> DataFrame: or configuration != self.__previous_configuration ): advisories = self.advisories - if configuration['filename'] is not None: - atcf_file = configuration['filename'] + if configuration["filename"] is not None: + atcf_file = configuration["filename"] else: url = atcf_url(self.nhc_code, self.file_deck) try: response = urlopen(url) except URLError: - url = atcf_url(self.nhc_code, self.file_deck, mode=ATCF_Mode.HISTORICAL) + url = atcf_url( + self.nhc_code, self.file_deck, mode=ATCF_Mode.HISTORICAL + ) try: response = urlopen(url) except URLError: - raise ConnectionError(f'could not connect to {url}') + raise ConnectionError(f"could not connect to {url}") atcf_file = io.BytesIO() atcf_file.write(response.read()) atcf_file.seek(0) - if url.endswith('.gz'): - atcf_file = gzip.GzipFile(fileobj=atcf_file, mode='rb') + if url.endswith(".gz"): + atcf_file = gzip.GzipFile(fileobj=atcf_file, mode="rb") - if 'OFCL' in advisories and 'CARQ' not in advisories: + if "OFCL" in advisories and "CARQ" not in advisories: self.__advisories_to_remove.append(ATCF_Advisory.CARQ) dataframe = read_atcf( atcf_file, advisories=advisories + self.__advisories_to_remove ) - dataframe.sort_values(['datetime', 'advisory'], inplace=True) + dataframe.sort_values(["datetime", "advisory"], inplace=True) dataframe.reset_index(inplace=True, drop=True) - dataframe['track_start_time'] = dataframe['datetime'].copy() + dataframe["track_start_time"] = dataframe["datetime"].copy() if ATCF_Advisory.BEST in self.advisories: - dataframe.loc[dataframe['advisory'] == 'BEST', 'track_start_time'] = ( - dataframe.loc[dataframe['advisory'] == 'BEST', 'datetime'] + dataframe.loc[dataframe["advisory"] == "BEST", "track_start_time"] = ( + dataframe.loc[dataframe["advisory"] == "BEST", "datetime"] .sort_values() .iloc[0] ) - dataframe.loc[dataframe['advisory'] != 'BEST', 'datetime'] += pandas.to_timedelta( - dataframe.loc[dataframe['advisory'] != 'BEST', 'forecast_hours'].astype(int), - unit='hours', + dataframe.loc[ + dataframe["advisory"] != "BEST", "datetime" + ] += pandas.to_timedelta( + dataframe.loc[dataframe["advisory"] != "BEST", "forecast_hours"].astype( + int + ), + unit="hours", ) self.unfiltered_data = dataframe self.__previous_configuration = configuration # if location values have changed, recompute velocity - location_hash = pandas.util.hash_pandas_object(self.__unfiltered_data['geometry']) + location_hash = pandas.util.hash_pandas_object( + self.__unfiltered_data["geometry"] + ) - if self.__location_hash is None or len(location_hash) != len(self.__location_hash): + if self.__location_hash is None or len(location_hash) != len( + self.__location_hash + ): updated_locations = ~self.__unfiltered_data.index.isnull() else: updated_locations = location_hash != self.__location_hash - updated_locations |= pandas.isna(self.__unfiltered_data['speed']) + updated_locations |= pandas.isna(self.__unfiltered_data["speed"]) if updated_locations.any(): self.__unfiltered_data.loc[updated_locations] = self.__compute_velocity( @@ -935,12 +979,12 @@ def unfiltered_data(self) -> DataFrame: @unfiltered_data.setter def unfiltered_data(self, dataframe: DataFrame): # fill missing values of MRD and MSLP in the OFCL advisory - if 'OFCL' in self.advisories: + if "OFCL" in self.advisories: tracks = separate_tracks(dataframe) - if 'OFCL' in tracks: - ofcl_tracks = tracks['OFCL'] - carq_tracks = tracks['CARQ'] + if "OFCL" in tracks: + ofcl_tracks = tracks["OFCL"] + carq_tracks = tracks["CARQ"] for initial_time, forecast in ofcl_tracks.items(): if initial_time in carq_tracks: @@ -950,40 +994,46 @@ def unfiltered_data(self, dataframe: DataFrame): relation = HollandBRelation() holland_b = relation.holland_b( - max_sustained_wind_speed=carq_forecast['max_sustained_wind_speed'], - background_pressure=carq_forecast['background_pressure'], - central_pressure=carq_forecast['central_pressure'], + max_sustained_wind_speed=carq_forecast[ + "max_sustained_wind_speed" + ], + background_pressure=carq_forecast["background_pressure"], + central_pressure=carq_forecast["central_pressure"], ) holland_b[holland_b == numpy.inf] = numpy.nan holland_b = numpy.nanmean(holland_b) - mrd_missing = pandas.isna(forecast['radius_of_maximum_winds']) - mslp_missing = pandas.isna(forecast['central_pressure']) - radp_missing = pandas.isna(forecast['background_pressure']) + mrd_missing = pandas.isna(forecast["radius_of_maximum_winds"]) + mslp_missing = pandas.isna(forecast["central_pressure"]) + radp_missing = pandas.isna(forecast["background_pressure"]) # fill OFCL maximum wind radius with the first entry from the CARQ advisory - forecast.loc[mrd_missing, 'radius_of_maximum_winds'] = carq_forecast[ - 'radius_of_maximum_winds' - ].iloc[0] + forecast.loc[ + mrd_missing, "radius_of_maximum_winds" + ] = carq_forecast["radius_of_maximum_winds"].iloc[0] # fill OFCL background pressure with the first entry from the CARQ advisory central pressure (at sea level) - forecast.loc[radp_missing, 'background_pressure'] = carq_forecast[ - 'central_pressure' + forecast.loc[radp_missing, "background_pressure"] = carq_forecast[ + "central_pressure" ].iloc[0] # fill OFCL central pressure (at sea level) with the 3rd hour entry, preserving Holland B - forecast.loc[mslp_missing, 'central_pressure'] = relation.central_pressure( + forecast.loc[ + mslp_missing, "central_pressure" + ] = relation.central_pressure( max_sustained_wind_speed=forecast.loc[ - mslp_missing, 'max_sustained_wind_speed' + mslp_missing, "max_sustained_wind_speed" + ], + background_pressure=forecast.loc[ + mslp_missing, "background_pressure" ], - background_pressure=forecast.loc[mslp_missing, 'background_pressure'], holland_b=holland_b, ) if len(self.__advisories_to_remove) > 0: dataframe = dataframe[ - ~dataframe['advisory'].isin( + ~dataframe["advisory"].isin( [value.value for value in self.__advisories_to_remove] ) ] @@ -994,23 +1044,23 @@ def unfiltered_data(self, dataframe: DataFrame): @property def __configuration(self) -> Dict[str, Any]: return { - 'id': self.nhc_code, - 'file_deck': self.file_deck, - 'advisories': self.advisories, - 'filename': self.filename, + "id": self.nhc_code, + "file_deck": self.file_deck, + "advisories": self.advisories, + "filename": self.filename, } @staticmethod def __compute_velocity(data: DataFrame) -> DataFrame: - geodetic = Geod(ellps='WGS84') + geodetic = Geod(ellps="WGS84") - for advisory in pandas.unique(data['advisory']): - advisory_data = data.loc[data['advisory'] == advisory] + for advisory in pandas.unique(data["advisory"]): + advisory_data = data.loc[data["advisory"] == advisory] indices = numpy.array( [ - numpy.where(advisory_data['datetime'] == unique_datetime)[0][0] - for unique_datetime in pandas.unique(advisory_data['datetime']) + numpy.where(advisory_data["datetime"] == unique_datetime)[0][0] + for unique_datetime in pandas.unique(advisory_data["datetime"]) ] ) shifted_indices = numpy.roll(indices, 1) @@ -1020,32 +1070,32 @@ def __compute_velocity(data: DataFrame) -> DataFrame: shifted_indices = advisory_data.index[shifted_indices] _, inverse_azimuths, distances = geodetic.inv( - advisory_data.loc[indices, 'longitude'], - advisory_data.loc[indices, 'latitude'], - advisory_data.loc[shifted_indices, 'longitude'], - advisory_data.loc[shifted_indices, 'latitude'], + advisory_data.loc[indices, "longitude"], + advisory_data.loc[indices, "latitude"], + advisory_data.loc[shifted_indices, "longitude"], + advisory_data.loc[shifted_indices, "latitude"], ) - intervals = advisory_data.loc[indices, 'datetime'].diff() - speeds = distances / (intervals / pandas.to_timedelta(1, 's')) + intervals = advisory_data.loc[indices, "datetime"].diff() + speeds = distances / (intervals / pandas.to_timedelta(1, "s")) bearings = pandas.Series(inverse_azimuths % 360, index=speeds.index) for index in indices: cluster_index = ( - advisory_data['datetime'] == advisory_data.loc[index, 'datetime'] + advisory_data["datetime"] == advisory_data.loc[index, "datetime"] ) - advisory_data.loc[cluster_index, 'speed'] = speeds[index] - advisory_data.loc[cluster_index, 'direction'] = bearings[index] + advisory_data.loc[cluster_index, "speed"] = speeds[index] + advisory_data.loc[cluster_index, "direction"] = bearings[index] - data.loc[data['advisory'] == advisory] = advisory_data + data.loc[data["advisory"] == advisory] = advisory_data - data.loc[pandas.isna(data['speed']), 'speed'] = 0 + data.loc[pandas.isna(data["speed"]), "speed"] = 0 return data @property def __file_end_date(self): - unique_dates = numpy.unique(self.unfiltered_data['datetime']) + unique_dates = numpy.unique(self.unfiltered_data["datetime"]) for date in unique_dates: if date >= numpy.datetime64(self.end_date): return date @@ -1053,7 +1103,7 @@ def __file_end_date(self): def __len__(self) -> int: return len(self.data) - def __copy__(self) -> 'VortexTrack': + def __copy__(self) -> "VortexTrack": instance = self.__class__( storm=self.unfiltered_data.copy(), start_date=self.start_date, @@ -1065,7 +1115,7 @@ def __copy__(self) -> 'VortexTrack': instance.filename = self.filename return instance - def __eq__(self, other: 'VortexTrack') -> bool: + def __eq__(self, other: "VortexTrack") -> bool: return self.data.equals(other.data) def __str__(self) -> str: @@ -1087,22 +1137,30 @@ def holland_b( background_pressure: float, central_pressure: float, ) -> float: - return ((max_sustained_wind_speed ** 2) * self.rho * numpy.exp(1)) / ( + return ((max_sustained_wind_speed**2) * self.rho * numpy.exp(1)) / ( background_pressure - central_pressure ) def central_pressure( - self, max_sustained_wind_speed: float, background_pressure: float, holland_b: float, + self, + max_sustained_wind_speed: float, + background_pressure: float, + holland_b: float, ) -> float: return ( - -(max_sustained_wind_speed ** 2) * self.rho * numpy.exp(1) + -(max_sustained_wind_speed**2) * self.rho * numpy.exp(1) ) / holland_b + background_pressure def max_sustained_wind_speed( - self, holland_b: float, background_pressure: float, central_pressure: float, + self, + holland_b: float, + background_pressure: float, + central_pressure: float, ) -> float: return numpy.sqrt( - holland_b * (background_pressure - central_pressure) / (self.rho * numpy.exp(1)) + holland_b + * (background_pressure - central_pressure) + / (self.rho * numpy.exp(1)) ) @@ -1115,25 +1173,26 @@ def separate_tracks(data: DataFrame) -> Dict[str, Dict[str, DataFrame]]: """ tracks = {} - for advisory in pandas.unique(data['advisory']): - advisory_data = data[data['advisory'] == advisory] + for advisory in pandas.unique(data["advisory"]): + advisory_data = data[data["advisory"] == advisory] - if advisory == 'BEST': - advisory_data = advisory_data.sort_values('datetime') + if advisory == "BEST": + advisory_data = advisory_data.sort_values("datetime") - track_start_times = pandas.unique(advisory_data['track_start_time']) + track_start_times = pandas.unique(advisory_data["track_start_time"]) tracks[advisory] = {} for track_start_time in track_start_times: - if advisory == 'BEST': + if advisory == "BEST": track_data = advisory_data else: track_data = advisory_data[ - advisory_data['track_start_time'] == pandas.to_datetime(track_start_time) - ].sort_values('forecast_hours') + advisory_data["track_start_time"] + == pandas.to_datetime(track_start_time) + ].sort_values("forecast_hours") tracks[advisory][ - f'{pandas.to_datetime(track_start_time):%Y%m%dT%H%M%S}' + f"{pandas.to_datetime(track_start_time):%Y%m%dT%H%M%S}" ] = track_data return tracks diff --git a/stormevents/stormevent.py b/stormevents/stormevent.py index a0d9c99..6dc538e 100644 --- a/stormevents/stormevent.py +++ b/stormevents/stormevent.py @@ -4,27 +4,31 @@ from typing import List import pandas +import xarray from shapely import ops -from shapely.geometry import MultiPolygon, Polygon +from shapely.geometry import MultiPolygon +from shapely.geometry import Polygon from shapely.geometry.base import BaseGeometry from shapely.ops import shape as shapely_shape -import xarray from xarray import Dataset -from stormevents.coops.tidalstations import ( - COOPS_Interval, - COOPS_Product, - COOPS_Station, - coops_stations_within_region, - COOPS_StationStatus, - COOPS_TidalDatum, - COOPS_TimeZone, - COOPS_Units, -) -from stormevents.nhc import nhc_storms, VortexTrack -from stormevents.nhc.atcf import ATCF_Advisory, ATCF_FileDeck, ATCF_Mode -from stormevents.usgs import usgs_flood_storms, USGS_StormEvent -from stormevents.utilities import relative_to_time_interval, subset_time_interval +from stormevents.coops.tidalstations import COOPS_Interval +from stormevents.coops.tidalstations import COOPS_Product +from stormevents.coops.tidalstations import COOPS_Station +from stormevents.coops.tidalstations import coops_stations_within_region +from stormevents.coops.tidalstations import COOPS_StationStatus +from stormevents.coops.tidalstations import COOPS_TidalDatum +from stormevents.coops.tidalstations import COOPS_TimeZone +from stormevents.coops.tidalstations import COOPS_Units +from stormevents.nhc import nhc_storms +from stormevents.nhc import VortexTrack +from stormevents.nhc.atcf import ATCF_Advisory +from stormevents.nhc.atcf import ATCF_FileDeck +from stormevents.nhc.atcf import ATCF_Mode +from stormevents.usgs import usgs_flood_storms +from stormevents.usgs import USGS_StormEvent +from stormevents.utilities import relative_to_time_interval +from stormevents.utilities import subset_time_interval class StormEvent: @@ -34,7 +38,11 @@ class StormEvent: """ def __init__( - self, name: str, year: int, start_date: datetime = None, end_date: datetime = None + self, + name: str, + year: int, + start_date: datetime = None, + end_date: datetime = None, ): """ :param name: storm name @@ -59,7 +67,7 @@ def __init__( """ storms = nhc_storms(year=year) - storms = storms[storms['name'].str.contains(name.upper())] + storms = storms[storms["name"].str.contains(name.upper())] if len(storms) > 0: self.__entry = storms.iloc[0] else: @@ -68,7 +76,7 @@ def __init__( self.__usgs_id = None self.__is_usgs_flood_event = True self.__high_water_marks = None - self.__previous_configuration = {'name': self.name, 'year': self.year} + self.__previous_configuration = {"name": self.name, "year": self.year} self.start_date = start_date self.end_date = end_date @@ -76,7 +84,7 @@ def __init__( @classmethod def from_nhc_code( cls, nhc_code: str, start_date: datetime = None, end_date: datetime = None - ) -> 'StormEvent': + ) -> "StormEvent": """ retrieve storm information from the NHC code :param nhc_code: NHC code @@ -100,7 +108,10 @@ def from_nhc_code( storm = storms.loc[nhc_code] return cls( - name=storm['name'], year=storm['year'], start_date=start_date, end_date=end_date + name=storm["name"], + year=storm["year"], + start_date=start_date, + end_date=end_date, ) @classmethod @@ -110,7 +121,7 @@ def from_usgs_id( year: int = None, start_date: datetime = None, end_date: datetime = None, - ) -> 'StormEvent': + ) -> "StormEvent": """ retrieve storm information from the USGS flood event ID :param usgs_id: USGS flood event ID @@ -122,17 +133,17 @@ def from_usgs_id( StormEvent(name='HENRI', year=2021, start_date=Timestamp('2021-08-20 18:00:00'), end_date=Timestamp('2021-08-24 12:00:00')) """ - flood_events = usgs_flood_storms(year=year) + storms = usgs_flood_storms(year=year) - if usgs_id in flood_events.index: - flood_event = flood_events.loc[usgs_id] + if usgs_id in storms["usgs_id"].values: + flood_event = storms.loc[storms["usgs_id"] == usgs_id].iloc[0] if start_date is None: - start_date = flood_event['start_date'] + start_date = flood_event["start_date"] if end_date is None: - end_date = flood_event['end_date'] + end_date = flood_event["end_date"] storm = cls( - name=flood_event['nhc_name'], - year=flood_event['year'], + name=flood_event["nhc_name"], + year=flood_event["year"], start_date=start_date, end_date=end_date, ) @@ -156,24 +167,22 @@ def usgs_id(self) -> int: """ if self.__usgs_id is None and self.__is_usgs_flood_event: - usgs_storm_events = usgs_flood_storms(year=self.year) + storms = usgs_flood_storms(year=self.year) - if self.nhc_code in usgs_storm_events['nhc_code'].values: - usgs_storm_event = usgs_storm_events.loc[ - usgs_storm_events['nhc_code'] == self.nhc_code - ] - self.__usgs_id = usgs_storm_event.index[0] + if self.nhc_code in storms.index.values: + usgs_storm_event = storms.loc[self.nhc_code] + self.__usgs_id = usgs_storm_event["usgs_id"] else: self.__is_usgs_flood_event = False return self.__usgs_id @property def name(self) -> str: - return self.__entry['name'].strip() + return self.__entry["name"].strip() @property def year(self) -> int: - return self.__entry['year'] + return self.__entry["year"] @property def basin(self) -> str: @@ -181,7 +190,7 @@ def basin(self) -> str: :return: basin in which storm occurred """ - return self.__entry['basin'].strip() + return self.__entry["basin"].strip() @property def number(self) -> int: @@ -189,7 +198,7 @@ def number(self) -> int: :return: ordinal number of storm in the year """ - return self.__entry['number'] + return self.__entry["number"] @property def start_date(self) -> datetime: @@ -202,14 +211,16 @@ def start_date(self, start_date: datetime): else: # interpret timedelta as a temporal movement around start / end start_date, _ = subset_time_interval( - start=self.__data_start, end=self.__data_end, subset_start=start_date, + start=self.__data_start, + end=self.__data_end, + subset_start=start_date, ) self.__start_date = start_date @property @lru_cache(maxsize=None) def __data_start(self) -> datetime: - data_start = self.__entry['start_date'] + data_start = self.__entry["start_date"] if pandas.isna(data_start): data_start = VortexTrack.from_storm_name(self.name, self.year).start_date return data_start @@ -225,14 +236,16 @@ def end_date(self, end_date: datetime): else: # interpret timedelta as a temporal movement around start / end _, end_date = subset_time_interval( - start=self.__data_start, end=self.__data_end, subset_end=end_date, + start=self.__data_start, + end=self.__data_end, + subset_end=end_date, ) self.__end_date = end_date @property @lru_cache(maxsize=None) def __data_end(self) -> datetime: - data_end = self.__entry['end_date'] + data_end = self.__entry["end_date"] if pandas.isna(data_end): data_end = VortexTrack.from_storm_name(self.name, self.year).end_date return data_end @@ -287,7 +300,7 @@ def flood_event(self) -> USGS_StormEvent: >>> flood = storm.flood_event >>> flood.high_water_marks() latitude longitude eventName ... siteZone peak_summary_id geometry - hwm_id ... + hwm_id ... 33496 37.298440 -80.007750 Florence Sep 2018 ... NaN NaN POINT (-80.00775 37.29844) 33497 33.699720 -78.936940 Florence Sep 2018 ... NaN NaN POINT (-78.93694 33.69972) 33498 33.758610 -78.792780 Florence Sep 2018 ... NaN NaN POINT (-78.79278 33.75861) @@ -302,8 +315,11 @@ def flood_event(self) -> USGS_StormEvent: [644 rows x 53 columns] """ - configuration = {'name': self.name, 'year': self.year} - if self.__high_water_marks is None or configuration != self.__previous_configuration: + configuration = {"name": self.name, "year": self.year} + if ( + self.__high_water_marks is None + or configuration != self.__previous_configuration + ): self.__high_water_marks = USGS_StormEvent(name=self.name, year=self.year) return self.__high_water_marks @@ -360,7 +376,10 @@ def coops_product_within_isotach( track.advisories = advisories else: track = self.track( - start_date=start_date, end_date=end_date, filename=track, advisories=advisories + start_date=start_date, + end_date=end_date, + filename=track, + advisories=advisories, ) polygons = [] @@ -442,7 +461,9 @@ def coops_product_within_region( start=self.start_date, end=self.end_date, relative=end_date ) - stations = coops_stations_within_region(region=region, station_status=station_type) + stations = coops_stations_within_region( + region=region, station_status=station_type + ) if len(stations) > 0: stations_data = [] @@ -456,22 +477,22 @@ def coops_product_within_region( time_zone=time_zone, interval=interval, ) - if len(station_data['t']) > 0: + if len(station_data["t"]) > 0: stations_data.append(station_data) - stations_data = xarray.combine_nested(stations_data, concat_dim='nos_id') + stations_data = xarray.combine_nested(stations_data, concat_dim="nos_id") else: stations_data = Dataset( - coords={'t': None, 'nos_id': None, 'nws_id': None, 'x': None, 'y': None} + coords={"t": None, "nos_id": None, "nws_id": None, "x": None, "y": None} ) return stations_data def __repr__(self) -> str: return ( - f'{self.__class__.__name__}(' - f'name={repr(self.name)}, ' - f'year={repr(self.year)}, ' - f'start_date={repr(self.start_date)}, ' - f'end_date={repr(self.end_date)}' - f')' + f"{self.__class__.__name__}(" + f"name={repr(self.name)}, " + f"year={repr(self.year)}, " + f"start_date={repr(self.start_date)}, " + f"end_date={repr(self.end_date)}" + f")" ) diff --git a/stormevents/usgs/__init__.py b/stormevents/usgs/__init__.py index f5a752c..caf9022 100644 --- a/stormevents/usgs/__init__.py +++ b/stormevents/usgs/__init__.py @@ -1,7 +1,7 @@ -from stormevents.usgs.events import ( - USGS_Event, - usgs_flood_events, - usgs_flood_storms, - USGS_StormEvent, -) -from stormevents.usgs.sensors import USGS_File, usgs_files, usgs_sensors +from stormevents.usgs.events import USGS_Event +from stormevents.usgs.events import usgs_flood_events +from stormevents.usgs.events import usgs_flood_storms +from stormevents.usgs.events import USGS_StormEvent +from stormevents.usgs.sensors import USGS_File +from stormevents.usgs.sensors import usgs_files +from stormevents.usgs.sensors import usgs_sensors diff --git a/stormevents/usgs/events.py b/stormevents/usgs/events.py index 47ecf47..edb089b 100644 --- a/stormevents/usgs/events.py +++ b/stormevents/usgs/events.py @@ -1,28 +1,30 @@ +import re from datetime import datetime from functools import lru_cache from os import PathLike -import re from typing import List -from geopandas import GeoDataFrame import pandas -from pandas import DataFrame import typepigeon +from geopandas import GeoDataFrame +from pandas import DataFrame from stormevents.nhc import nhc_storms -from stormevents.usgs.base import EventStatus, EventType -from stormevents.usgs.highwatermarks import ( - HighWaterMarkEnvironment, - HighWaterMarkQuality, - HighWaterMarksQuery, - HighWaterMarkType, -) -from stormevents.usgs.sensors import usgs_files, usgs_sensors +from stormevents.usgs.base import EventStatus +from stormevents.usgs.base import EventType +from stormevents.usgs.highwatermarks import HighWaterMarkEnvironment +from stormevents.usgs.highwatermarks import HighWaterMarkQuality +from stormevents.usgs.highwatermarks import HighWaterMarksQuery +from stormevents.usgs.highwatermarks import HighWaterMarkType +from stormevents.usgs.sensors import usgs_files +from stormevents.usgs.sensors import usgs_sensors @lru_cache(maxsize=None) def usgs_flood_events( - year: int = None, event_type: EventType = None, event_status: EventStatus = None, + year: int = None, + event_type: EventType = None, + event_status: EventStatus = None, ) -> DataFrame: """ this function collects all USGS flood events of the given type and status that have high-water mark data @@ -58,54 +60,56 @@ def usgs_flood_events( [293 rows x 11 columns] """ - events = pandas.read_json('https://stn.wim.usgs.gov/STNServices/Events.json') + events = pandas.read_json("https://stn.wim.usgs.gov/STNServices/Events.json") events.rename( columns={ - 'event_id': 'usgs_id', - 'event_name': 'name', - 'event_start_date': 'start_date', - 'event_end_date': 'end_date', - 'event_description': 'description', - 'event_coordinator': 'coordinator', + "event_id": "usgs_id", + "event_name": "name", + "event_start_date": "start_date", + "event_end_date": "end_date", + "event_description": "description", + "event_coordinator": "coordinator", }, inplace=True, ) - events.set_index('usgs_id', inplace=True) - events['start_date'] = pandas.to_datetime(events['start_date']) - events['end_date'] = pandas.to_datetime(events['end_date']) - events['last_updated'] = pandas.to_datetime(events['last_updated']) - events['event_type'] = events['event_type_id'].apply(lambda value: EventType(value).name) - events['event_status'] = events['event_status_id'].apply( + events.set_index("usgs_id", inplace=True) + events["start_date"] = pandas.to_datetime(events["start_date"]) + events["end_date"] = pandas.to_datetime(events["end_date"]) + events["last_updated"] = pandas.to_datetime(events["last_updated"]) + events["event_type"] = events["event_type_id"].apply( + lambda value: EventType(value).name + ) + events["event_status"] = events["event_status_id"].apply( lambda value: EventStatus(value).name ) - events['year'] = events['start_date'].dt.year + events["year"] = events["start_date"].dt.year events = events[ [ - 'name', - 'year', - 'description', - 'event_type', - 'event_status', - 'coordinator', - 'instruments', - 'last_updated', - 'last_updated_by', - 'start_date', - 'end_date', + "name", + "year", + "description", + "event_type", + "event_status", + "coordinator", + "instruments", + "last_updated", + "last_updated_by", + "start_date", + "end_date", ] ] if event_type is not None: event_type = typepigeon.convert_value(event_type, [str]) - events = events[events['event_type'].isin(event_type)] + events = events[events["event_type"].isin(event_type)] if event_status is not None: event_status = typepigeon.convert_value(event_status, [str]) - events = events[events['event_status'].isin(event_status)] + events = events[events["event_status"].isin(event_status)] if year is not None: year = typepigeon.convert_value(year, [int]) - events = events[events['year'].isin(year)] + events = events[events["year"].isin(year)] return events @@ -121,57 +125,94 @@ def usgs_flood_storms(year: int = None) -> DataFrame: :return: table of USGS flood events with NHC storm names >>> usgs_flood_storms() - usgs_name year nhc_name ... last_updated_by start_date end_date - usgs_id ... - 8 Wilma 2005 WILMA ... NaN 2005-10-20 00:00:00 2005-10-31 00:00:00 - 18 Isaac Aug 2012 2012 ISAAC ... 35.0 2012-08-27 05:00:00 2012-09-02 05:00:00 - 19 Rita 2005 RITA ... NaN 2005-09-23 04:00:00 2005-09-25 04:00:00 - 23 Irene 2011 IRENE ... NaN 2011-08-26 04:00:00 2011-08-29 04:00:00 - 24 Sandy 2012 SANDY ... NaN 2012-10-21 04:00:00 2012-10-30 04:00:00 - ... ... ... ... ... ... ... ... - 303 2020 TS Marco - Hurricane Laura 2020 MARCO ... 864.0 2020-08-22 05:00:00 2020-08-30 05:00:00 - 304 2020 Hurricane Sally 2020 SALLY ... 864.0 2020-09-13 05:00:00 2020-09-20 05:00:00 - 305 2020 Hurricane Delta 2020 DELTA ... 864.0 2020-10-06 05:00:00 2020-10-13 05:00:00 - 310 2021 Tropical Cyclone Henri 2021 HENRI ... 864.0 2021-08-20 05:00:00 2021-09-03 05:00:00 - 312 2021 Tropical Cyclone Ida 2021 IDA ... 864.0 2021-08-27 05:00:00 2021-09-03 05:00:00 - [30 rows x 13 columns] + usgs_id usgs_name year nhc_name ... last_updated last_updated_by start_date end_date + nhc_code ... + AL252005 8 Wilma 2005 WILMA ... NaT NaN 2005-10-20 00:00:00 2005-10-31 00:00:00 + AL092012 18 Isaac Aug 2012 2012 ISAAC ... 2018-09-10 14:14:55.378445 35.0 2012-08-27 05:00:00 2012-09-02 05:00:00 + AL182005 19 Rita 2005 RITA ... NaT NaN 2005-09-23 04:00:00 2005-09-25 04:00:00 + AL092011 23 Irene 2011 IRENE ... NaT NaN 2011-08-26 04:00:00 2011-08-29 04:00:00 + AL092011 23 Irene 2011 IRENE ... NaT NaN 2011-08-26 04:00:00 2011-08-29 04:00:00 + AL182012 24 Sandy 2012 SANDY ... NaT NaN 2012-10-21 04:00:00 2012-10-30 04:00:00 + AL072008 25 Gustav 2008 GUSTAV ... NaT NaN 2008-08-31 04:00:00 2008-09-03 04:00:00 + AL092008 26 Ike 2008 IKE ... NaT NaN 2008-09-11 04:00:00 2008-09-12 04:00:00 + AL112015 119 Joaquin 2015 JOAQUIN ... NaT NaN 2015-10-01 04:00:00 2015-10-08 04:00:00 + AL092016 131 Hermine 2016 HERMINE ... NaT NaN 2016-09-01 04:00:00 2016-09-07 04:00:00 + AL132003 133 Isabel September 2003 2003 ISABEL ... NaT NaN 2003-09-06 05:00:00 2003-09-30 05:00:00 + AL142016 135 Matthew October 2016 2016 MATTHEW ... NaT NaN 2016-10-03 04:00:00 2016-10-30 04:00:00 + AL092017 180 Harvey Aug 2017 2017 HARVEY ... NaT NaN 2017-08-24 05:00:00 2017-09-24 05:00:00 + AL112017 182 Irma September 2017 2017 IRMA ... 2018-09-07 15:07:25.174430 35.0 2017-09-03 05:00:00 2017-09-30 05:00:00 + AL152017 189 Maria September 2017 2017 MARIA ... 2020-10-05 19:41:33.520407 1.0 2017-09-17 04:00:00 2017-10-02 04:00:00 + AL122017 190 Jose September 2017 2017 JOSE ... 2020-10-05 19:40:26.866120 1.0 2017-09-18 04:00:00 2017-09-25 04:00:00 + AL162017 196 Nate October 2017 2017 NATE ... 2018-09-07 15:06:46.782674 35.0 2017-10-05 05:00:00 2017-10-14 05:00:00 + EP142018 281 Lane August 2018 2018 LANE ... 2018-08-22 02:38:28.664132 35.0 2018-08-22 05:00:00 2018-09-15 05:00:00 + AL072018 282 Gordon Sep 2018 2018 GORDON ... 2018-09-04 14:10:53.247893 35.0 2018-09-04 05:00:00 2018-10-04 05:00:00 + AL072018 282 Gordon Sep 2018 2018 GORDON ... 2018-09-04 14:10:53.247893 35.0 2018-09-04 05:00:00 2018-10-04 05:00:00 + AL062018 283 Florence Sep 2018 2018 FLORENCE ... 2018-09-07 15:05:56.149271 35.0 2018-09-07 05:00:00 2018-10-07 05:00:00 + AL092018 284 Isaac Sep 2018 2018 ISAAC ... 2018-12-06 16:10:38.342516 3.0 2018-09-11 04:00:00 2018-09-18 04:00:00 + AL142018 287 Michael Oct 2018 2018 MICHAEL ... 2018-12-06 16:24:40.504991 3.0 2018-10-08 04:00:00 2018-10-15 04:00:00 + AL052019 291 2019 Hurricane Dorian 2019 DORIAN ... 2019-08-31 18:00:27.314745 36.0 2019-08-28 04:00:00 2019-09-20 04:00:00 + AL052019 291 2019 Hurricane Dorian 2019 DORIAN ... 2019-08-31 18:00:27.314745 36.0 2019-08-28 04:00:00 2019-09-20 04:00:00 + AL111995 299 1995 South Carolina August Tropical Storm Jerry 1995 JERRY ... 2020-06-24 18:11:28.492197 864.0 1995-08-01 05:00:00 1995-08-31 05:00:00 + AL081999 300 1999 South Carolina September Hurricane Floyd 1999 FLOYD ... 2020-06-24 18:12:51.380357 864.0 1999-09-01 05:00:00 1999-09-30 05:00:00 + AL092020 301 2020 Hurricane Isaias 2020 ISAIAS ... 2020-07-31 11:47:44.480931 864.0 2020-07-31 05:00:00 2020-08-07 05:00:00 + AL142020 303 2020 TS Marco - Hurricane Laura 2020 MARCO ... 2020-08-24 18:31:59.388708 864.0 2020-08-22 05:00:00 2020-08-30 05:00:00 + AL132020 303 2020 TS Marco - Hurricane Laura 2020 LAURA ... 2020-08-24 18:31:59.388708 864.0 2020-08-22 05:00:00 2020-08-30 05:00:00 + AL192020 304 2020 Hurricane Sally 2020 SALLY ... 2020-09-13 13:15:24.843513 864.0 2020-09-13 05:00:00 2020-09-20 05:00:00 + AL262020 305 2020 Hurricane Delta 2020 DELTA ... 2020-10-06 12:58:46.905258 864.0 2020-10-06 05:00:00 2020-10-13 05:00:00 + AL082021 310 2021 Tropical Cyclone Henri 2021 HENRI ... 2021-08-21 15:56:08.379065 864.0 2021-08-20 05:00:00 2021-09-03 05:00:00 + AL092021 312 2021 Tropical Cyclone Ida 2021 IDA ... 2021-08-27 13:00:47.886713 864.0 2021-08-27 05:00:00 2021-09-03 05:00:00 """ - events = usgs_flood_events(year=year, event_type=EventType.HURRICANE) + events = usgs_flood_events(year=year, event_type=EventType.HURRICANE).copy() - events.rename(columns={'name': 'usgs_name'}, inplace=True) - events['nhc_name'] = None - events['nhc_code'] = None + events.rename(columns={"name": "usgs_name"}, inplace=True) + events["nhc_name"] = None + events["nhc_code"] = None - storms = nhc_storms(tuple(pandas.unique(events['year']))) + events.reset_index(inplace=True) - storm_names = sorted(pandas.unique(storms['name'].str.strip())) + storms = nhc_storms(tuple(pandas.unique(events["year"]))) + + storm_names = sorted(pandas.unique(storms["name"].str.strip())) for storm_name in storm_names: event_storms = events[ - events['usgs_name'].str.contains(storm_name, flags=re.IGNORECASE) + events["usgs_name"].str.contains(storm_name, flags=re.IGNORECASE) ] - for event_id, event in event_storms.iterrows(): + for _, event in event_storms.iterrows(): storms_matching = storms[ - storms['name'].str.contains(storm_name, flags=re.IGNORECASE) - & (storms['year'] == event['year']) + storms["name"].str.contains(storm_name, flags=re.IGNORECASE) + & (storms["year"] == event["year"]) ] for nhc_code, storm in storms_matching.iterrows(): - events.at[event_id, 'nhc_name'] = storm['name'] - events.at[event_id, 'nhc_code'] = storm.name - - return events.loc[ - ~pandas.isna(events['nhc_code']), - ['usgs_name', 'year', 'nhc_name', 'nhc_code', *events.columns[2:-2],], + matching_event = events.loc[events["usgs_id"] == event["usgs_id"]].iloc[ + 0 + ] + if matching_event["nhc_code"] is None: + events.at[matching_event.name, "nhc_name"] = storm["name"] + events.at[matching_event.name, "nhc_code"] = storm.name + else: + matching_event["nhc_name"] = storm["name"] + matching_event["nhc_code"] = storm.name + events = events.append(matching_event) + + events = events.loc[ + ~pandas.isna(events["nhc_code"]), + ["usgs_id", "usgs_name", "year", "nhc_name", "nhc_code", *events.columns[3:-2]], ] + events.sort_values("usgs_id", inplace=True) + events.set_index("nhc_code", inplace=True) + + return events + class USGS_Event: """ representation of an arbitrary flood event as defined by the USGS """ - URL = 'https://stn.wim.usgs.gov/STNServices/HWMs/FilteredHWMs.json' + URL = "https://stn.wim.usgs.gov/STNServices/HWMs/FilteredHWMs.json" def __init__(self, id: int): """ @@ -217,7 +258,11 @@ def __init__(self, id: int): self.__error = None @classmethod - def from_name(cls, name: str, year: int = None,) -> 'USGS_Event': + def from_name( + cls, + name: str, + year: int = None, + ) -> "USGS_Event": """ retrieve high-water mark info from the USGS flood event name @@ -227,20 +272,20 @@ def from_name(cls, name: str, year: int = None,) -> 'USGS_Event': """ events = usgs_flood_events(year=year) - events = events[events['name'] == name] + events = events[events["name"] == name] if len(events) == 0: raise ValueError(f'no event with name "{name}" found') if year is not None: - events = events[events['year'] == year] + events = events[events["year"] == year] event = events.iloc[0] return cls(id=event.name) @classmethod - def from_csv(cls, filename: PathLike) -> 'USGS_Event': + def from_csv(cls, filename: PathLike) -> "USGS_Event": """ read a CSV file with high-water mark data @@ -248,11 +293,11 @@ def from_csv(cls, filename: PathLike) -> 'USGS_Event': :return: flood event object """ - data = pandas.read_csv(filename, index_col='hwm_id') + data = pandas.read_csv(filename, index_col="hwm_id") try: - instance = cls(id=int(data['event_id'].iloc[0])) + instance = cls(id=int(data["event_id"].iloc[0])) except KeyError: - instance = cls.from_name(data['eventName'].iloc[0]) + instance = cls.from_name(data["eventName"].iloc[0]) instance.__data = data return instance @@ -267,47 +312,47 @@ def id(self, id: int): @property def name(self) -> str: - return self.__metadata['name'] + return self.__metadata["name"] @property def year(self) -> int: - return self.__metadata['year'] + return self.__metadata["year"] @property def description(self) -> str: - return self.__metadata['description'] + return self.__metadata["description"] @property def event_type(self) -> EventType: - return typepigeon.convert_value(self.__metadata['event_type'], EventType) + return typepigeon.convert_value(self.__metadata["event_type"], EventType) @property def event_status(self) -> EventStatus: - return typepigeon.convert_value(self.__metadata['event_status'], EventStatus) + return typepigeon.convert_value(self.__metadata["event_status"], EventStatus) @property def coordinator(self) -> str: - return self.__metadata['coordinator'] + return self.__metadata["coordinator"] @property def instruments(self) -> str: - return self.__metadata['instruments'] + return self.__metadata["instruments"] @property def last_updated(self) -> datetime: - return self.__metadata['last_updated'] + return self.__metadata["last_updated"] @property def last_updated_by(self) -> str: - return self.__metadata['last_updated_by'] + return self.__metadata["last_updated_by"] @property def start_date(self) -> datetime: - return typepigeon.convert_value(self.__metadata['start_date'], datetime) + return typepigeon.convert_value(self.__metadata["start_date"], datetime) @property def end_date(self) -> datetime: - return typepigeon.convert_value(self.__metadata['end_date'], datetime) + return typepigeon.convert_value(self.__metadata["end_date"], datetime) @property def files(self) -> DataFrame: @@ -364,7 +409,7 @@ def sensors(self) -> DataFrame: return usgs_sensors(event_id=self.id) def retrieve_file(self, id: int, path: PathLike): - 'https://stn.wim.usgs.gov/STNServices/Files/{id}/item' + "https://stn.wim.usgs.gov/STNServices/Files/{id}/item" def high_water_marks( self, @@ -443,7 +488,7 @@ def high_water_marks( return self.__query.data - def __eq__(self, other: 'USGS_Event') -> bool: + def __eq__(self, other: "USGS_Event") -> bool: return ( self.__query is not None and other.__query is not None @@ -451,7 +496,7 @@ def __eq__(self, other: 'USGS_Event') -> bool: ) def __repr__(self) -> str: - return f'{self.__class__.__name__}(id={repr(self.id)})' + return f"{self.__class__.__name__}(id={repr(self.id)})" class USGS_StormEvent(USGS_Event): @@ -466,9 +511,11 @@ def __init__(self, name: str, year: int): """ storms = usgs_flood_storms(year=year) - storm = storms[(storms['nhc_name'] == name.upper().strip()) & (storms['year'] == year)] + storm = storms[ + (storms["nhc_name"] == name.upper().strip()) & (storms["year"] == year) + ] if len(storm) == 0: raise ValueError(f'storm "{name} {year}" not found in USGS HWM database') - super().__init__(id=storm.index[0]) + super().__init__(id=storm.iloc[0, 0]) diff --git a/stormevents/usgs/highwatermarks.py b/stormevents/usgs/highwatermarks.py index d5b9246..5b95965 100644 --- a/stormevents/usgs/highwatermarks.py +++ b/stormevents/usgs/highwatermarks.py @@ -1,14 +1,17 @@ from enum import Enum -from typing import Any, Dict, List +from typing import Any +from typing import Dict +from typing import List import geopandas -from geopandas import GeoDataFrame import pandas -from pandas import DataFrame import requests import typepigeon +from geopandas import GeoDataFrame +from pandas import DataFrame -from stormevents.usgs.base import EventStatus, EventType +from stormevents.usgs.base import EventStatus +from stormevents.usgs.base import EventType class HighWaterMarkType(Enum): @@ -40,8 +43,8 @@ class HighWaterMarkQuality(Enum): class HighWaterMarkEnvironment(Enum): - COASTAL = 'Coastal' - RIVERINE = 'Riverine' + COASTAL = "Coastal" + RIVERINE = "Riverine" class HighWaterMarksQuery: @@ -50,7 +53,7 @@ class HighWaterMarksQuery: https://stn.wim.usgs.gov/STNServices/Documentation/home """ - URL = 'https://stn.wim.usgs.gov/STNServices/HWMs/FilteredHWMs.json' + URL = "https://stn.wim.usgs.gov/STNServices/HWMs/FilteredHWMs.json" def __init__( self, @@ -180,30 +183,31 @@ def environment(self, environment: HighWaterMarkEnvironment): @property def query(self) -> Dict[str, Any]: query = { - 'Event': self.event_id, - 'EventType': self.event_type, - 'EventStatus': self.event_status, - 'States': self.us_states, - 'County': self.us_counties, - 'HWMType': self.hwm_type, - 'HWMQuality': self.quality, - 'HWMEnvironment': self.environment, - 'SurveyComplete': self.survey_completed, - 'StillWater': self.still_water, + "Event": self.event_id, + "EventType": self.event_type, + "EventStatus": self.event_status, + "States": self.us_states, + "County": self.us_counties, + "HWMType": self.hwm_type, + "HWMQuality": self.quality, + "HWMEnvironment": self.environment, + "SurveyComplete": self.survey_completed, + "StillWater": self.still_water, } for key, value in query.items(): - if key not in ['SurveyComplete', 'StillWater']: + if key not in ["SurveyComplete", "StillWater"]: if isinstance(value, Enum): value = value.value elif isinstance(value, List): value = [ - entry.value if isinstance(entry, Enum) else entry for entry in value + entry.value if isinstance(entry, Enum) else entry + for entry in value ] value = typepigeon.convert_value(value, [str]) if len(value) > 0: - value = ','.join(value) + value = ",".join(value) else: value = None @@ -240,11 +244,11 @@ def data(self) -> GeoDataFrame: if any( value is not None for key, value in query.items() - if key not in ['SurveyComplete', 'StillWater'] + if key not in ["SurveyComplete", "StillWater"] ): url = self.URL else: - url = 'https://stn.wim.usgs.gov/STNServices/HWMs.json' + url = "https://stn.wim.usgs.gov/STNServices/HWMs.json" response = requests.get(url, params=query) @@ -252,70 +256,75 @@ def data(self) -> GeoDataFrame: data = DataFrame(response.json()) self.__error = None else: - self.__error = f'{response.reason} - {response.request.url}' + self.__error = f"{response.reason} - {response.request.url}" raise ValueError(self.__error) if len(data) > 0: - data['survey_date'] = pandas.to_datetime(data['survey_date'], errors='coerce') - data['flag_date'] = pandas.to_datetime(data['flag_date'], errors='coerce') - data.loc[data['markerName'].str.len() == 0, 'markerName'] = None + data["survey_date"] = pandas.to_datetime( + data["survey_date"], errors="coerce" + ) + data["flag_date"] = pandas.to_datetime( + data["flag_date"], errors="coerce" + ) + data.loc[data["markerName"].str.len() == 0, "markerName"] = None else: data = DataFrame( columns=[ - 'latitude', - 'longitude', - 'eventName', - 'hwmTypeName', - 'hwmQualityName', - 'verticalDatumName', - 'verticalMethodName', - 'approvalMember', - 'markerName', - 'horizontalMethodName', - 'horizontalDatumName', - 'flagMemberName', - 'surveyMemberName', - 'site_no', - 'siteDescription', - 'sitePriorityName', - 'networkNames', - 'stateName', - 'countyName', - 'siteZone', - 'sitePermHousing', - 'site_latitude', - 'site_longitude', - 'hwm_id', - 'waterbody', - 'site_id', - 'event_id', - 'hwm_type_id', - 'hwm_quality_id', - 'latitude_dd', - 'longitude_dd', - 'survey_date', - 'elev_ft', - 'vdatum_id', - 'vcollect_method_id', - 'bank', - 'marker_id', - 'hcollect_method_id', - 'hwm_notes', - 'hwm_environment', - 'flag_date', - 'stillwater', - 'hdatum_id', - 'hwm_label', - 'files', - 'height_above_gnd', - 'hwm_locationdescription', - 'flag_member_id', - 'survey_member_id', + "latitude", + "longitude", + "eventName", + "hwmTypeName", + "hwmQualityName", + "verticalDatumName", + "verticalMethodName", + "approvalMember", + "markerName", + "horizontalMethodName", + "horizontalDatumName", + "flagMemberName", + "surveyMemberName", + "site_no", + "siteDescription", + "sitePriorityName", + "networkNames", + "stateName", + "countyName", + "siteZone", + "sitePermHousing", + "site_latitude", + "site_longitude", + "hwm_id", + "waterbody", + "site_id", + "event_id", + "hwm_type_id", + "hwm_quality_id", + "latitude_dd", + "longitude_dd", + "survey_date", + "elev_ft", + "vdatum_id", + "vcollect_method_id", + "bank", + "marker_id", + "hcollect_method_id", + "hwm_notes", + "hwm_environment", + "flag_date", + "stillwater", + "hdatum_id", + "hwm_label", + "files", + "height_above_gnd", + "hwm_locationdescription", + "flag_member_id", + "survey_member_id", ], ) - data.set_index('hwm_id', inplace=True) + data.set_index("hwm_id", inplace=True) self.__data = GeoDataFrame( - data, geometry=geopandas.points_from_xy(data['longitude'], data['latitude']), + data, + geometry=geopandas.points_from_xy(data["longitude"], data["latitude"]), ) self.__previous_query = query elif self.__error is not None: @@ -323,21 +332,21 @@ def data(self) -> GeoDataFrame: return self.__data - def __eq__(self, other: 'HighWaterMarksQuery') -> bool: + def __eq__(self, other: "HighWaterMarksQuery") -> bool: return self.query == other.query def __repr__(self) -> str: return ( - f'{self.__class__.__name__}(' - f'event_id={self.event_id}, ' - f'event_type={self.event_type}, ' - f'event_status={self.event_status}, ' - f'us_states={self.us_states}, ' - f'us_counties={self.us_counties}, ' - f'hwm_type={self.hwm_type}, ' - f'quality={self.quality}, ' - f'environment={self.environment}, ' - f'survey_completed={self.survey_completed}, ' - f'still_water={self.still_water}' - f')' + f"{self.__class__.__name__}(" + f"event_id={self.event_id}, " + f"event_type={self.event_type}, " + f"event_status={self.event_status}, " + f"us_states={self.us_states}, " + f"us_counties={self.us_counties}, " + f"hwm_type={self.hwm_type}, " + f"quality={self.quality}, " + f"environment={self.environment}, " + f"survey_completed={self.survey_completed}, " + f"still_water={self.still_water}" + f")" ) diff --git a/stormevents/usgs/sensors.py b/stormevents/usgs/sensors.py index 012eacd..82ca5bf 100644 --- a/stormevents/usgs/sensors.py +++ b/stormevents/usgs/sensors.py @@ -2,8 +2,8 @@ from os import PathLike import pandas -from pandas import DataFrame import requests +from pandas import DataFrame class SensorType(Enum): @@ -64,11 +64,11 @@ def __init__(self, id: int): @property def url(self) -> str: - return f'https://stn.wim.usgs.gov/STNServices/Files/{id}/item' + return f"https://stn.wim.usgs.gov/STNServices/Files/{id}/item" def to_file(self, path: PathLike): response = requests.get(self.url, stream=True) - with open(path, 'wb') as output_file: + with open(path, "wb") as output_file: for chunk in response.iter_content(chunk_size=1024): output_file.write(chunk) @@ -101,18 +101,18 @@ def usgs_files(file_type: FileType = None, event_id: int = None) -> DataFrame: """ if event_id is None: - url = 'https://stn.wim.usgs.gov/STNServices/Files.json' + url = "https://stn.wim.usgs.gov/STNServices/Files.json" else: - url = f'https://stn.wim.usgs.gov/STNServices/Events/{event_id}/Files.json' + url = f"https://stn.wim.usgs.gov/STNServices/Events/{event_id}/Files.json" files = pandas.read_json(url) - files.set_index('file_id', inplace=True) + files.set_index("file_id", inplace=True) if file_type is not None: if isinstance(file_type, FileType): file_type = file_type.value - files = files[files['filetype_id'] == file_type] + files = files[files["filetype_id"] == file_type] return files @@ -149,21 +149,21 @@ def usgs_sensors( """ if event_id is None: - url = 'https://stn.wim.usgs.gov/STNServices/Instruments.json' + url = "https://stn.wim.usgs.gov/STNServices/Instruments.json" else: - url = f'https://stn.wim.usgs.gov/STNServices/Events/{event_id}/Instruments.json' + url = f"https://stn.wim.usgs.gov/STNServices/Events/{event_id}/Instruments.json" sensors = pandas.read_json(url) - sensors.set_index('instrument_id', inplace=True) + sensors.set_index("instrument_id", inplace=True) if sensor_type is not None: if isinstance(sensor_type, SensorType): sensor_type = sensor_type.value - sensors = sensors[sensors['sensor_type_id'] == sensor_type] + sensors = sensors[sensors["sensor_type_id"] == sensor_type] if deployment_type is not None: if isinstance(deployment_type, DeploymentType): deployment_type = deployment_type.value - sensors = sensors[sensors['deployment_type_id'] == deployment_type] + sensors = sensors[sensors["deployment_type_id"] == deployment_type] return sensors diff --git a/stormevents/utilities.py b/stormevents/utilities.py index 9fc418b..bae6576 100644 --- a/stormevents/utilities.py +++ b/stormevents/utilities.py @@ -1,4 +1,5 @@ -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta from numbers import Number from typing import Union @@ -65,7 +66,9 @@ def subset_time_interval( def relative_to_time_interval( - start: datetime, end: datetime, relative: Union[datetime, timedelta], + start: datetime, + end: datetime, + relative: Union[datetime, timedelta], ) -> datetime: """ return the absolute time relative to the time interval diff --git a/tests/__init__.py b/tests/__init__.py index c7b2dee..c0f32f4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,21 +1,23 @@ import os +import re from os import PathLike from pathlib import Path -import re -from typing import Dict, List +from typing import Dict +from typing import List import pytest import xarray -from stormevents.nhc.storms import nhc_storms, nhc_storms_archive +from stormevents.nhc.storms import nhc_storms +from stormevents.nhc.storms import nhc_storms_archive -DATA_DIRECTORY = Path(__file__).parent.absolute().resolve() / 'data' -INPUT_DIRECTORY = DATA_DIRECTORY / 'input' -OUTPUT_DIRECTORY = DATA_DIRECTORY / 'output' -REFERENCE_DIRECTORY = DATA_DIRECTORY / 'reference' +DATA_DIRECTORY = Path(__file__).parent.absolute().resolve() / "data" +INPUT_DIRECTORY = DATA_DIRECTORY / "input" +OUTPUT_DIRECTORY = DATA_DIRECTORY / "output" +REFERENCE_DIRECTORY = DATA_DIRECTORY / "reference" -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def cache_nhc_storms(): nhc_storms() nhc_storms_archive() @@ -41,7 +43,7 @@ def check_reference_directory( else: test_filename = test_directory / reference_filename.name - if reference_filename.suffix in ['.h5', '.nc']: + if reference_filename.suffix in [".h5", ".nc"]: reference_filesize = Path(reference_filename).stat().st_size test_filesize = Path(test_filename).stat().st_size @@ -68,7 +70,8 @@ def check_reference_directory( ): try: lines_to_skip.update( - line_index % len(test_lines) for line_index in line_indices + line_index % len(test_lines) + for line_index in line_indices ) except ZeroDivisionError: continue @@ -77,6 +80,6 @@ def check_reference_directory( del test_lines[line_index], reference_lines[line_index] cwd = Path.cwd() - assert '\n'.join(test_lines) == '\n'.join( + assert "\n".join(test_lines) == "\n".join( reference_lines ), f'"{os.path.relpath(test_filename, cwd)}" != "{os.path.relpath(reference_filename, cwd)}"' diff --git a/tests/data/input/test_usgs_flood_event/florence2018.csv b/tests/data/input/test_usgs_flood_event/florence2018.csv index 78153ef..ce6c81b 100644 --- a/tests/data/input/test_usgs_flood_event/florence2018.csv +++ b/tests/data/input/test_usgs_flood_event/florence2018.csv @@ -221,7 +221,7 @@ Tape up to hwm of 2.25 ft.",,,POINT (-79.153377 34.227779) 33703,35.24464,-76.560641,Florence Sep 2018,Seed line,Good: +/- 0.10 ft,NAVD88,RT-GNSS,Steve Harden,Marker,Phone/Car GPS,NAD83,Anthony Gotvald,Anthony Gotvald,NCPAM26995,Vacant home on Middle Bay Rd,,,NC,Pamlico County,No,35.24464,-76.560641,Jones Bay,26995,283,5,2,Seed line consisting of paint chips inside front door on enclosed porch of vacant home. Consistent mark inside porch walls. HWM feet above ground measured to outside yard.,35.24464,-76.560641,2018-09-25 04:00:00,6.36,2.0,6.0,N/A,4.0,3,Coastal,2018-09-23 04:00:00,1.0,2,6.0,6.0,0.01,,HWM-01,[],18841.0,3.9,Level III Survey,,,POINT (-76.560641 35.24464) 33704,34.114009,-77.924904,Florence Sep 2018,Debris,Poor: +/- 0.40 ft,NAVD88,Level Gun,Steve Harden,Nail and HWM tag,Map (digital or paper),WGS84 (from Digital Map),Laura Gurley,Steve Harden,NCNEW12948,River Road Park,,"SWaTH, South Atlantic",NC,New Hanover County,No,34.113519,-77.925815,Cape Fear River,12948,283,2,4,Wide debris line near parking lot near boat ramp and pier entrance.,34.114009,-77.924904,2018-09-26 04:00:00,5.65,2.0,2.0,N/A,6.0,4,Coastal,2018-09-24 04:00:00,0.0,4,1401.0,761.0,0.06,0.4,HWM-01,[],18703.0,0.0,HWM coordinate on field sheet is for sensor site. Estimated HWM lat/long from photos and Google Earth. GNSS Level II for HWM based on BM-1.,,,POINT (-77.924904 34.114009) 33705,34.37194,-79.35889,Florence Sep 2018,Seed line,Good: +/- 0.10 ft,NAVD88,RT-GNSS,Andy Caldwell,Marker,Phone/Car GPS,WGS84 (from Digital Map),Philip Habermehl,Andy Caldwell,SCDIL27058,RESIDENCE ON BLACK BRANCH RD,,South Atlantic,SC,Dillon County,No,34.37194,-79.35889,MAPLE SWAMP,27058,283,5,2,"FACING HOUSE, MARK IS LOCATED ON RIGHT JAMB OF LEFT GARAGE DOOR.",34.37194,-79.35889,2018-09-29 04:00:00,89.03,2.0,6.0,N/A,4.0,3,Riverine,2018-09-25 04:00:00,0.0,4,1600.0,1381.0,0.09,0.1,HWM-01,[],18182.0,0.86,Level II survey,,,POINT (-79.35889 34.37194) -33706,34.21777,-77.81164,Florence Sep 2018,Debris,Fair: +/- 0.20 ft,NAVD88,Level Gun,Steve Harden,Stake,Map (digital or paper),WGS84 (from Digital Map),Laura Gurley,Steve Harden,NCNEW13008,Wrightsville Beach Boating Access,,"SWaTH, South Atlantic",NC,New Hanover County,No,34.218354,-77.81145,Atlantic Intracoastal Waterway,13008,283,2,3,Marker: stake with flagging tape. Thick debris line under bridge.,34.21777,-77.81164,2018-09-26 04:00:00,7.04,2.0,2.0,N/A,7.0,4,Coastal,2018-09-24 04:00:00,0.0,4,1401.0,761.0,0.01,0.2,HWM-01,[],18688.0,0.0,"GNSS level II based on RM-1. +33706,34.21777,-77.81164,Florence Sep 2018,Debris,Fair: +/- 0.20 ft,NAVD88,Level Gun,Steve Harden,Stake,Map (digital or paper),WGS84 (from Digital Map),Laura Gurley,Steve Harden,NCNEW13008,Wrightsville Beach Boating Access,,"SWaTH, South Atlantic",NC,New Hanover County,No,34.218354,-77.81145,Atlantic Intracoastal Waterway,13008,283,2,3,Marker: stake with flagging tape. Thick debris line under bridge.,34.21777,-77.81164,2018-09-26 04:00:00,7.04,2.0,2.0,N/A,7.0,4,Coastal,2018-09-24 04:00:00,0.0,4,1401.0,761.0,0.01,0.2,HWM-01,[],18688.0,0.0,"GNSS level II based on RM-1. HWM coordinate on field sheet is for sensor site. Estimated HWM lat/long from photos and Google Earth.",,,POINT (-77.81164 34.21777) 33707,34.772862,-79.3337624,Florence Sep 2018,Seed line,Good: +/- 0.10 ft,NAVD88,RT-GNSS,Anthony Gotvald,Marker,Phone/Car GPS,WGS84 (from Digital Map),Daniel McCay,Daniel McCay,NCSCO27062,Sycamore Hill Church,,,NC,Scotland County,No,34.7722858,-79.3337576,Lumber River,27062,283,5,2,Seed line on street facing door of meeting hall near road. Transferred over to mark on molding.,34.772862,-79.3337624,2018-09-30 04:00:00,189.418,2.0,6.0,Right,4.0,3,Riverine,2018-09-25 04:00:00,0.0,4,1591.0,1591.0,0.17,0.1,HWM-01,[],19546.0,4.4,GNSS Level II survey.,,,POINT (-79.3337624 34.772862) 33708,34.34944,-79.16556,Florence Sep 2018,Seed line,Poor: +/- 0.40 ft,NAVD88,RT-GNSS,Andy Caldwell,Marker,Phone/Car GPS,NAD83,Philip Habermehl,Andy Caldwell,SCDIL18611,"Page's Mill Pond in Lion's Park @@ -592,7 +592,7 @@ N side of the warehouse on corrugated metal siding.",,South Atlantic,SC,Horry Co the intersection of Boundary St. and Main St. and next to 1102 Boundary St. HWM was found on the N side of the warehouse on corrugated metal siding. Black ink line drawn on metal siding. Orange flagging tape affixed to side of bay door next to HWM and on a nearby spigot.",33.8607613,-79.0576537,2018-10-02 04:00:00,15.45,2.0,6.0,N/A,4.0,3,Riverine,2018-09-30 04:00:00,0.0,2,1381.0,1381.0,0.05,,HWM1,[],18221.0,2.4,Level II survey,,,POINT (-79.0576537 33.8607613) -34037,34.107478,-77.879297,Florence Sep 2018,Debris,VP: > 0.40 ft,NAVD88,Level Gun,Steve Harden,Stake,Map (digital or paper),NAD83,Steve Harden,Steve Harden,NCNEW12868,The Tides Marina at AIW,,"South Atlantic, SWaTH",NC,New Hanover County,No,34.107129,-77.879175,AIW - Masonboro Sound,12868,283,2,5,Debris line that was recently cleaned up; used downed grass line where debris was located.,34.107478,-77.879297,2018-09-25 04:00:00,6.55,2.0,2.0,N/A,7.0,4,Coastal,2018-09-25 04:00:00,0.0,2,761.0,761.0,0.04,,HWM-01,[],18289.0,0.0,"GNSS level II based on BM1. +34037,34.107478,-77.879297,Florence Sep 2018,Debris,VP: > 0.40 ft,NAVD88,Level Gun,Steve Harden,Stake,Map (digital or paper),NAD83,Steve Harden,Steve Harden,NCNEW12868,The Tides Marina at AIW,,"South Atlantic, SWaTH",NC,New Hanover County,No,34.107129,-77.879175,AIW - Masonboro Sound,12868,283,2,5,Debris line that was recently cleaned up; used downed grass line where debris was located.,34.107478,-77.879297,2018-09-25 04:00:00,6.55,2.0,2.0,N/A,7.0,4,Coastal,2018-09-25 04:00:00,0.0,2,761.0,761.0,0.04,,HWM-01,[],18289.0,0.0,"GNSS level II based on BM1. HWM coordinate on field sheet is for sensor site. Estimated HWM lat/long from photo and Google Earth.",,,POINT (-77.879297 34.107478) 34039,34.16772,-78.56064,Florence Sep 2018,Mud,Fair: +/- 0.20 ft,NAVD88,RT-GNSS,Steve Harden,Marker,Phone/Car GPS,WGS84 (from Digital Map),Cristal Younker,James Chapman,NCCOL27363,House on Crusoe Island Rd,,,NC,Columbus County,No,34.16772,-78.56064,Waccamaw River,27363,283,1,3,HWM line on outside wall left front corner of southern most shed behind mobile home. Residence is to the left of the Fire Station.,34.16772,-78.56064,2018-10-01 04:00:00,40.5,2.0,6.0,N/A,4.0,3,Riverine,2018-09-30 04:00:00,0.0,4,1715.0,1722.0,0.12,,HWM-01,[],19538.0,3.05,Level II survey,,,POINT (-78.56064 34.16772) 34040,34.0132,-78.63366,Florence Sep 2018,Seed line,Good: +/- 0.10 ft,NAVD88,RT-GNSS,Anthony Gotvald,Marker,Phone/Car GPS,WGS84 (from Digital Map),Cristal Younker,James Chapman,NCBRU27367,House on Rivergate Dr NW,,,NC,Brunswick County,No,34.0132,-78.63366,Waccamaw River,27367,283,5,2,Home under construction but next neighbor gave permission to survey. Mark is on right most window opening on block wall. Mark was leveled from seed line on inside of building.,34.0132,-78.63366,2018-10-01 04:00:00,29.99,2.0,6.0,N/A,4.0,3,Riverine,2018-09-30 04:00:00,0.0,4,1715.0,1722.0,0.12,,HWM-01,[],19291.0,5.83,Level II survey,,,POINT (-78.63366 34.0132) diff --git a/tests/data/input/test_vortex_track_no_internet/fort.22 b/tests/data/input/test_vortex_track_no_internet/fort.22 index ba4c27b..6e1ba2d 100644 --- a/tests/data/input/test_vortex_track_no_internet/fort.22 +++ b/tests/data/input/test_vortex_track_no_internet/fort.22 @@ -67,4 +67,4 @@ AL, 06, 2018091712, , BEST, 156, 378N, 822W, 25, 1008, EX, 0, , 0, AL, 06, 2018091718, , BEST, 162, 388N, 820W, 25, 1008, EX, 0, , 0, 0, 0, 0, 1013, 360, 160, , , , , , 90, 12, FLORENCE , 29 AL, 06, 2018091800, , BEST, 168, 395N, 805W, 25, 1008, EX, 0, , 0, 0, 0, 0, 1013, 360, 160, , , , , , 89, 29, FLORENCE , 30 AL, 06, 2018091806, , BEST, 174, 413N, 768W, 25, 1007, EX, 0, , 0, 0, 0, 0, 1013, 360, 170, , , , , , 89, 26, FLORENCE , 31 -AL, 06, 2018091812, , BEST, 180, 422N, 733W, 25, 1006, EX, 34, NEQ, 0, 0, 0, 0, 1013, 360, 180, , , , , ,271, 26, FLORENCE , 32 \ No newline at end of file +AL, 06, 2018091812, , BEST, 180, 422N, 733W, 25, 1006, EX, 34, NEQ, 0, 0, 0, 0, 1013, 360, 180, , , , , ,271, 26, FLORENCE , 32 diff --git a/tests/data/reference/test_coops_product_within_region/data.nc b/tests/data/reference/test_coops_product_within_region/data.nc index 731d057573db5d5057702dae6a71bc86cd6d8234..334b24067461b77f949662ba8a41faa4ff68fffd 100644 GIT binary patch delta 302 zcmX>#hw;=L#tjWDlRH=xn4l{7gfH<>s`_OE>@1 zxWkql66E4$$iTp$4#Xh93ZWQ$yu-ob8c;q=+&9<>EUt+r9_Hu`7S}=(ckvD0Y-{jJ PcCx;oGzVBN#m)f$MCVZ2 delta 293 zcmX>#hw;=L#tjWDOws(4t60<~uV7ihUYv2!euv!E$^NX0lPg$faBy8;Y@ILEY&}_k zO<}VIn=C71%49!o^~uW&vp2us?qcG~D=&`EOi`#%sGMBGm&OwE<>Z&ielF6R8TlKT zktKVC(pa8~RUO&g?0)PWcTSRoWcNRSIyTm#C7iTikmgT*z`#C?OEz~WkH;$e>7n{5qV$ugzb JO;+?%1ONpWPWAu* diff --git a/tests/data/reference/test_coops_stations_within_region/stations.csv b/tests/data/reference/test_coops_stations_within_region/stations.csv index 1b6ef78..ed0208b 100644 --- a/tests/data/reference/test_coops_stations_within_region/stations.csv +++ b/tests/data/reference/test_coops_stations_within_region/stations.csv @@ -27,12 +27,12 @@ nos_id,nws_id,name,state,status,removed,geometry 8665530,CHTS1,"Charleston, Cooper River Entrance",SC,active,"2021-12-11 16:30:00,2018-12-03 00:00:00,2018-12-01 00:00:00,2018-04-18 17:40:00,2017-12-06 00:00:00,2017-12-02 00:00:00,2016-12-03 00:01:00,2016-12-03 00:00:00,2016-12-02 00:00:00,2016-11-09 15:00:00,2016-11-08 14:00:00,2015-12-09 11:20:00,2015-12-09 00:00:00,2014-12-08 00:00:00,2013-12-16 00:00:00,2012-08-30 00:00:00,2011-11-02 00:00:00,2011-11-01 00:00:00,2010-12-01 00:07:00,2009-12-14 11:49:00,2009-12-13 00:01:00,2008-12-11 00:00:00,2008-12-09 00:00:00,2007-11-09 00:00:00,2006-10-24 23:59:00,2006-10-24 00:00:00,2005-12-07 00:00:00,2004-12-06 00:00:00,2003-12-04 00:00:00,2002-12-09 00:00:00,2001-05-12 00:00:00,2000-05-17 00:00:00,1999-04-30 00:00:00,1998-03-30 00:00:00,1997-04-22 00:00:00,1993-06-22 23:00:00,1992-04-14 00:00:00,1991-02-25 00:00:00",POINT (-79.9375 32.78125) 8656483,BFTN7,"Beaufort, Duke Marine Lab",NC,active,"2021-12-15 11:00:00,2021-12-10 11:00:00,2021-12-09 13:00:00,2019-11-19 00:00:00,2018-12-28 16:00:00,2018-12-28 00:00:00,2017-10-16 13:00:00,2016-08-22 12:30:00,2015-10-19 00:00:00,2014-11-11 00:00:00,2014-03-29 00:00:00,2012-08-16 20:00:00,2011-11-17 13:46:00,2011-10-26 07:04:00,2011-01-12 14:43:00,2009-10-21 00:00:00,2009-03-23 00:00:00,2008-08-25 10:35:00,2007-05-15 00:00:00,2006-11-28 23:59:00,2006-11-28 00:01:00,2006-11-28 00:00:00,2005-07-26 00:00:00,2004-07-20 00:00:00,2003-12-15 00:00:00,2002-11-14 00:00:00,2001-06-27 00:00:00,2000-10-18 00:00:00,1999-03-30 00:00:00,1998-04-14 00:00:00,1997-08-07 00:00:00,1993-05-25 23:00:00,1993-01-13 23:00:00,1992-05-13 23:00:00,1991-03-20 00:00:00,1901-01-01 00:00:00",POINT (-76.6875 34.71875) 8725110,NPSF1,"Naples, Gulf of Mexico",FL,active,"2022-01-23 12:00:00,2022-01-23 00:00:00,2022-01-22 12:00:00,2020-01-27 00:00:00,2019-09-10 10:00:00,2018-08-22 01:00:00,2018-03-16 23:58:00,2017-03-29 09:00:00,2017-03-28 10:30:00,2017-03-28 10:00:00,2015-11-15 00:00:00,2015-11-11 00:00:00,2015-06-16 00:00:00,2015-02-14 00:00:00,2014-01-23 13:00:00,2013-02-06 00:00:00,2012-01-25 09:09:00,2011-04-05 23:43:00,2011-02-02 17:00:00,2010-03-17 00:00:00,2010-03-16 00:00:00,2009-05-05 00:00:00,2008-03-26 00:00:00,2008-03-25 00:00:00,2006-04-28 23:59:00,2006-04-28 13:00:00,2006-04-28 11:00:00,2006-04-28 00:00:00,2005-02-16 00:00:00,2004-03-01 00:00:00,2003-02-19 00:00:00,2002-02-08 00:00:00,2000-03-29 00:00:00,1999-03-07 00:00:00,1998-02-07 00:00:00,1997-09-06 00:00:00,1997-03-03 00:00:00,1995-09-11 23:00:00,1993-04-08 23:00:00,1992-12-21 23:59:00",POINT (-81.8125 26.125) -8720219,DMSF1,Dames Point,FL,active,"2022-01-26 00:00:00,2021-09-23 16:00:00,2021-01-20 16:00:00,2018-01-16 19:20:00,2017-01-10 20:00:00,2016-01-19 10:00:00,2015-01-10 14:00:00,2013-11-14 01:00:00,2013-11-14 00:59:00,2013-11-14 00:00:00,2004-04-08 23:59:00,2004-04-08 00:00:00,1999-07-01 23:59:00,1999-07-01 00:00:00,1998-11-02 23:59:00,1998-11-02 00:00:00,1996-06-10 23:59:00,1996-06-10 00:00:00",POINT (-81.5625 30.390625) 8721604,TRDF1,"Trident Pier, Port Canaveral",FL,active,"2022-01-28 17:00:00,2021-09-11 14:00:00,2021-02-20 23:58:00,2020-01-24 00:00:00,2019-02-05 18:24:00,2018-11-06 20:48:00,2017-01-18 00:00:00,2016-02-24 10:00:00,2015-01-31 00:00:00,2014-12-26 00:00:00,2014-01-29 00:00:00,2013-02-05 00:00:00,2012-11-01 00:00:00,2012-01-27 19:00:00,2011-03-24 23:59:00,2011-03-24 01:00:00,2011-03-22 00:00:00,2010-02-17 00:00:00,2010-02-16 00:00:00,2009-06-16 12:00:00,2008-07-02 00:00:00,2008-06-30 23:54:00,2007-10-19 00:00:00,2007-03-07 00:00:00,2006-02-23 23:59:00,2006-02-23 00:00:00,2006-02-22 00:00:00,2005-02-24 00:00:00,2004-05-07 00:00:00,2003-04-16 00:00:00,2002-04-19 00:00:00,2001-02-06 00:00:00,2000-07-28 00:00:00,2000-03-15 00:00:00,1999-02-11 00:00:00,1998-02-19 00:00:00",POINT (-80.5625 28.421875) 8722670,LKWF1,"Lake Worth Pier, Atlantic Ocean",FL,active,"2022-01-29 14:00:00,2022-01-29 13:00:00,2022-01-29 00:00:00,2022-01-28 14:00:00,2022-01-27 11:00:00,2020-01-19 11:00:00,2020-01-18 14:00:00,2020-01-18 13:00:00,2020-01-18 10:00:00,2020-01-17 13:00:00,2020-01-17 11:00:00,2020-01-17 10:00:00,2020-01-17 00:00:00,2019-07-19 09:00:00,2019-02-08 00:00:00,2017-01-24 00:00:00,2016-10-15 00:00:00,2016-01-21 00:00:00,2015-02-05 00:00:00,2014-07-29 00:00:00,2014-02-03 00:00:00,2013-08-08 16:30:00,2013-02-08 00:00:00,2012-01-30 00:00:00,2011-06-08 00:00:00,2010-06-14 12:00:00,2010-06-14 01:00:00,2010-06-14 00:00:00,2010-06-02 00:01:00,2010-06-02 00:00:00,2010-06-01 00:01:00,2010-06-01 00:00:00",POINT (-80.0625 26.609375) 8720226,MSBF1,"Southbank Riverwalk, St Johns River",FL,active,"2022-02-02 14:00:00,2021-01-16 17:57:00,2020-01-23 09:30:00,2014-04-12 00:00:00,2008-08-18 23:59:00,2008-08-18 00:00:00,1999-07-01 23:59:00,1999-07-01 23:59:00,1999-07-01 00:00:00,1998-11-02 23:59:00,1998-11-02 00:00:00,1997-07-10 23:59:00,1997-07-10 00:00:00",POINT (-81.6875 30.3125) 8723214,VAKF1,"Virginia Key, Biscayne Bay",FL,active,"2022-02-04 00:00:00,2022-02-03 16:00:00,2021-09-30 13:00:00,2020-12-03 14:00:00,2020-09-10 11:00:00,2020-01-23 03:00:00,2017-04-01 00:01:00,2017-04-01 00:00:00,2016-03-09 11:00:00,2016-02-25 00:00:00,2016-01-26 00:00:00,2016-01-23 00:00:00,2015-11-13 00:00:00,2015-02-10 00:00:00,2014-09-27 08:15:00,2014-02-05 10:25:00,2013-07-26 00:00:00,2013-02-11 00:00:00,2012-01-31 19:28:00,2011-02-02 20:14:00,2010-02-24 00:00:00,2010-02-23 00:00:00,2009-12-09 00:00:00,2009-10-27 00:00:00,2009-03-05 00:00:00,2009-03-04 23:59:00,2009-03-04 00:00:00,2009-02-23 00:00:00,2007-12-01 00:00:00,2007-11-30 00:00:00,2007-11-26 23:59:00,2007-11-26 00:00:00,2007-08-07 00:00:00,2006-09-05 00:00:00,2005-02-22 00:00:00,2004-05-03 00:00:00,2003-04-11 00:00:00,2001-11-30 00:00:00,2001-02-09 00:00:00,2000-03-01 00:00:00,1999-02-16 00:00:00,1998-01-28 00:00:00",POINT (-80.1875 25.734375) 8720357,BKBF1,I-295 Buckman Bridge,FL,active,"2022-02-04 17:00:00,2021-02-20 14:45:00,2021-02-20 14:00:00,2021-02-20 13:00:00,2021-02-20 12:00:00,2021-02-09 00:00:00,2017-09-25 00:00:00,2017-01-13 14:00:00,2017-01-13 12:00:00,2016-11-16 19:50:00,2016-01-14 12:00:00,2015-10-26 10:30:00,2015-01-13 09:00:00,2013-11-17 00:00:00,2013-11-16 00:00:00,2013-11-14 00:00:00,2013-11-07 23:59:00,2013-11-07 00:00:00,1997-07-10 23:59:00,1997-07-10 00:00:00",POINT (-81.6875 30.1875) +8720219,DMSF1,Dames Point,FL,active,"2022-03-14 15:37:00,2022-01-26 00:00:00,2021-09-23 16:00:00,2021-01-20 16:00:00,2018-01-16 19:20:00,2017-01-10 20:00:00,2016-01-19 10:00:00,2015-01-10 14:00:00,2013-11-14 01:00:00,2013-11-14 00:59:00,2013-11-14 00:00:00,2004-04-08 23:59:00,2004-04-08 00:00:00,1999-07-01 23:59:00,1999-07-01 00:00:00,1998-11-02 23:59:00,1998-11-02 00:00:00,1996-06-10 23:59:00,1996-06-10 00:00:00",POINT (-81.5625 30.390625) 8720242,LNBF1,Long Branch,FL,discontinued,"2004-04-07 23:59:00,2004-04-07 00:00:00,1999-07-01 23:59:00,1999-07-01 00:00:00,1998-11-02 23:59:00,1998-11-02 00:00:00,1996-06-10 23:59:00,1996-06-10 00:00:00",POINT (-81.625 30.359375) 8664941,SCIS1,South Capers Island,SC,discontinued,"2008-03-09 23:59:00,2008-03-09 00:00:00,2005-04-11 00:00:00,2004-07-12 00:00:00",POINT (-79.6875 32.84375) 8668498,FRPS1,Fripps Inlet,SC,discontinued,"2008-03-13 23:59:00,2008-03-13 00:00:00,2008-01-28 00:00:00,2004-07-19 00:00:00",POINT (-80.4375 32.34375) diff --git a/tests/data/reference/test_nhc_storms/storms.csv b/tests/data/reference/test_nhc_storms/storms.csv index 7ba73a1..103a2f1 100644 --- a/tests/data/reference/test_nhc_storms/storms.csv +++ b/tests/data/reference/test_nhc_storms/storms.csv @@ -2704,8 +2704,8 @@ AL172021,ROSE,TS,2021,AL,17,GIS_ARCHIVE,, EP172021,RICK,HU,2021,EP,17,GIS_ARCHIVE,, AL182021,SAM,HU,2021,AL,18,GIS_ARCHIVE,, EP182021,TERRY,TS,2021,EP,18,GIS_ARCHIVE,, -EP192021,SANDRA,LO,2021, EP,19,,2021-11-03 18:00:00,2021-11-09 18:00:00 AL192021,TERESA,ST,2021,AL,19,GIS_ARCHIVE,, +EP192021,SANDRA,TS,2021,EP,19,GIS_ARCHIVE,, AL202021,VICTOR,TS,2021,AL,20,GIS_ARCHIVE,, AL212021,WANDA,TS,2021, AL,21,WARNING,2021-11-05 12:00:00, CP902021,INVEST,LO,2021, CP,90,METWATCH,2021-07-24 12:00:00, diff --git a/tests/data/reference/test_storm_event_coops_product_within_isotach/florence2018_water_levels.nc b/tests/data/reference/test_storm_event_coops_product_within_isotach/florence2018_water_levels.nc index f30ed9f17980e41782cf45f7ac394e37ce4c0d68..6bdd00bb94552b5f8d9fa29d61799444117f2f55 100644 GIT binary patch delta 106 zcmeC$%hR=&XG0MmbA>|XTP9`zyV{s+%VVZ=W2@*d7Cu&t(Zw(5r0k+rYTJZv)exTmY9gC1n5r diff --git a/tests/data/reference/test_usgs_flood_storms/storms.csv b/tests/data/reference/test_usgs_flood_storms/storms.csv index fccd086..9cabb8b 100644 --- a/tests/data/reference/test_usgs_flood_storms/storms.csv +++ b/tests/data/reference/test_usgs_flood_storms/storms.csv @@ -1,28 +1,32 @@ -usgs_id,usgs_name,year,nhc_name,nhc_code,description,event_type,event_status,coordinator,instruments,last_updated,last_updated_by,start_date,end_date -8,Wilma,2005,WILMA,AL252005,"Category 3 in west FL. +nhc_code,usgs_id,usgs_name,year,nhc_name,description,event_type,event_status,coordinator,instruments,last_updated,last_updated_by,start_date,end_date +AL252005,8,Wilma,2005,WILMA,"Category 3 in west FL. Hurricane Wilma was the most intense tropical cyclone ever recorded in the Atlantic basin. Part of the record breaking 2005 Atlantic hurricane season.",HURRICANE,COMPLETED,515,[],,,2005-10-20 00:00:00,2005-10-31 00:00:00 -18,Isaac Aug 2012,2012,ISAAC,AL092012,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],2018-09-10 14:14:55.378445,35.0,2012-08-27 05:00:00,2012-09-02 05:00:00 -19,Rita,2005,RITA,AL182005,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,515,[],,,2005-09-23 04:00:00,2005-09-25 04:00:00 -23,Irene,2011,IRENE,AL092011,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],,,2011-08-26 04:00:00,2011-08-29 04:00:00 -24,Sandy,2012,SANDY,AL182012,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],,,2012-10-21 04:00:00,2012-10-30 04:00:00 -25,Gustav,2008,GUSTAV,AL072008,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,515,[],,,2008-08-31 04:00:00,2008-09-03 04:00:00 -26,Ike,2008,IKE,AL092008,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,515,[],,,2008-09-11 04:00:00,2008-09-12 04:00:00 -119,Joaquin,2015,JOAQUIN,AL112015,Hurricane/Tropical storm with intense waves predicted in the Atlantic,HURRICANE,COMPLETED,36,[],,,2015-10-01 04:00:00,2015-10-08 04:00:00 -131,Hermine,2016,HERMINE,AL092016,Small deployment of Initial landfall of Hermine is in the Big Bend area of FL to assist NWS modeling,HURRICANE,COMPLETED,3,[],,,2016-09-01 04:00:00,2016-09-07 04:00:00 -133,Isabel September 2003,2003,ISABEL,AL132003,"Hurricane Isabel formed near the Cape Verde Islands from a tropical wave on Sep. 6 and reached peak winds of 165 mph (265 km/h) on Sep. 11. It made landfall on Sep. 18 in North Carolina, killing 16 and damaging over $5 billion of property.",HURRICANE,COMPLETED,515,[],,,2003-09-06 05:00:00,2003-09-30 05:00:00 -135,Matthew October 2016,2016,MATTHEW,AL142016,Hurricane Matthew,HURRICANE,COMPLETED,35,[],,,2016-10-03 04:00:00,2016-10-30 04:00:00 -180,Harvey Aug 2017,2017,HARVEY,AL092017,Hurricane Harvey Texas Coast August 2017,HURRICANE,COMPLETED,35,[],,,2017-08-24 05:00:00,2017-09-24 05:00:00 -182,Irma September 2017,2017,IRMA,AL112017,"Hurricane affecting PR, USVI and maybe mainland US",HURRICANE,COMPLETED,36,[],2018-09-07 15:07:25.174430,35.0,2017-09-03 05:00:00,2017-09-30 05:00:00 -189,Maria September 2017,2017,MARIA,AL152017,TD15,HURRICANE,COMPLETED,36,[],2020-10-05 19:41:33.520407,1.0,2017-09-17 04:00:00,2017-10-02 04:00:00 -190,Jose September 2017,2017,JOSE,AL122017,Tropical storm impacting the northeast us,HURRICANE,COMPLETED,36,[],2020-10-05 19:40:26.866120,1.0,2017-09-18 04:00:00,2017-09-25 04:00:00 -196,Nate October 2017,2017,NATE,AL162017,TD16 will be developing into Nate. North Gulf deployment,HURRICANE,COMPLETED,36,[],2018-09-07 15:06:46.782674,35.0,2017-10-05 05:00:00,2017-10-14 05:00:00 -281,Lane August 2018,2018,LANE,EP142018,Hurricane Lane in the central Pacific Ocean.,HURRICANE,ACTIVE,35,[],2018-08-22 02:38:28.664132,35.0,2018-08-22 05:00:00,2018-09-15 05:00:00 -282,Gordon Sep 2018,2018,GORDON,AL072018,TS/Hurricane Gordon in Gulf of Mexico,HURRICANE,ACTIVE,35,[],2018-09-04 14:10:53.247893,35.0,2018-09-04 05:00:00,2018-10-04 05:00:00 -283,Florence Sep 2018,2018,FLORENCE,AL062018,,HURRICANE,ACTIVE,35,[],2018-09-07 15:05:56.149271,35.0,2018-09-07 05:00:00,2018-10-07 05:00:00 -284,Isaac Sep 2018,2018,ISAAC,AL092018,,HURRICANE,COMPLETED,3,[],2018-12-06 16:10:38.342516,3.0,2018-09-11 04:00:00,2018-09-18 04:00:00 -287,Michael Oct 2018,2018,MICHAEL,AL142018,storm hit the Mexico Beach area of the Florida panhandle,HURRICANE,COMPLETED,3,[],2018-12-06 16:24:40.504991,3.0,2018-10-08 04:00:00,2018-10-15 04:00:00 -291,2019 Hurricane Dorian,2019,DORIAN,AL052019,,HURRICANE,ACTIVE,864,[],2019-08-31 18:00:27.314745,36.0,2019-08-28 04:00:00,2019-09-20 04:00:00 -301,2020 Hurricane Isaias,2020,ISAIAS,AL092020,,HURRICANE,ACTIVE,1001,[],2020-07-31 11:47:44.480931,864.0,2020-07-31 05:00:00,2020-08-07 05:00:00 -303,2020 TS Marco - Hurricane Laura,2020,MARCO,AL142020,,HURRICANE,ACTIVE,864,[],2020-08-24 18:31:59.388708,864.0,2020-08-22 05:00:00,2020-08-30 05:00:00 -304,2020 Hurricane Sally,2020,SALLY,AL192020,,HURRICANE,ACTIVE,864,[],2020-09-13 13:15:24.843513,864.0,2020-09-13 05:00:00,2020-09-20 05:00:00 -305,2020 Hurricane Delta,2020,DELTA,AL262020,,HURRICANE,ACTIVE,864,[],2020-10-06 12:58:46.905258,864.0,2020-10-06 05:00:00,2020-10-13 05:00:00 +AL092012,18,Isaac Aug 2012,2012,ISAAC,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],2018-09-10 14:14:55.378445,35.0,2012-08-27 05:00:00,2012-09-02 05:00:00 +AL182005,19,Rita,2005,RITA,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,515,[],,,2005-09-23 04:00:00,2005-09-25 04:00:00 +AL092011,23,Irene,2011,IRENE,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],,,2011-08-26 04:00:00,2011-08-29 04:00:00 +AL092011,23,Irene,2011,IRENE,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],,,2011-08-26 04:00:00,2011-08-29 04:00:00 +AL182012,24,Sandy,2012,SANDY,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,36,[],,,2012-10-21 04:00:00,2012-10-30 04:00:00 +AL072008,25,Gustav,2008,GUSTAV,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,515,[],,,2008-08-31 04:00:00,2008-09-03 04:00:00 +AL092008,26,Ike,2008,IKE,historical hurricane data loaded by the data archive team,HURRICANE,COMPLETED,515,[],,,2008-09-11 04:00:00,2008-09-12 04:00:00 +AL112015,119,Joaquin,2015,JOAQUIN,Hurricane/Tropical storm with intense waves predicted in the Atlantic,HURRICANE,COMPLETED,36,[],,,2015-10-01 04:00:00,2015-10-08 04:00:00 +AL092016,131,Hermine,2016,HERMINE,Small deployment of Initial landfall of Hermine is in the Big Bend area of FL to assist NWS modeling,HURRICANE,COMPLETED,3,[],,,2016-09-01 04:00:00,2016-09-07 04:00:00 +AL132003,133,Isabel September 2003,2003,ISABEL,"Hurricane Isabel formed near the Cape Verde Islands from a tropical wave on Sep. 6 and reached peak winds of 165 mph (265 km/h) on Sep. 11. It made landfall on Sep. 18 in North Carolina, killing 16 and damaging over $5 billion of property.",HURRICANE,COMPLETED,515,[],,,2003-09-06 05:00:00,2003-09-30 05:00:00 +AL142016,135,Matthew October 2016,2016,MATTHEW,Hurricane Matthew,HURRICANE,COMPLETED,35,[],,,2016-10-03 04:00:00,2016-10-30 04:00:00 +AL092017,180,Harvey Aug 2017,2017,HARVEY,Hurricane Harvey Texas Coast August 2017,HURRICANE,COMPLETED,35,[],,,2017-08-24 05:00:00,2017-09-24 05:00:00 +AL112017,182,Irma September 2017,2017,IRMA,"Hurricane affecting PR, USVI and maybe mainland US",HURRICANE,COMPLETED,36,[],2018-09-07 15:07:25.174430,35.0,2017-09-03 05:00:00,2017-09-30 05:00:00 +AL152017,189,Maria September 2017,2017,MARIA,TD15,HURRICANE,COMPLETED,36,[],2020-10-05 19:41:33.520407,1.0,2017-09-17 04:00:00,2017-10-02 04:00:00 +AL122017,190,Jose September 2017,2017,JOSE,Tropical storm impacting the northeast us,HURRICANE,COMPLETED,36,[],2020-10-05 19:40:26.866120,1.0,2017-09-18 04:00:00,2017-09-25 04:00:00 +AL162017,196,Nate October 2017,2017,NATE,TD16 will be developing into Nate. North Gulf deployment,HURRICANE,COMPLETED,36,[],2018-09-07 15:06:46.782674,35.0,2017-10-05 05:00:00,2017-10-14 05:00:00 +EP142018,281,Lane August 2018,2018,LANE,Hurricane Lane in the central Pacific Ocean.,HURRICANE,ACTIVE,35,[],2018-08-22 02:38:28.664132,35.0,2018-08-22 05:00:00,2018-09-15 05:00:00 +AL072018,282,Gordon Sep 2018,2018,GORDON,TS/Hurricane Gordon in Gulf of Mexico,HURRICANE,ACTIVE,35,[],2018-09-04 14:10:53.247893,35.0,2018-09-04 05:00:00,2018-10-04 05:00:00 +AL072018,282,Gordon Sep 2018,2018,GORDON,TS/Hurricane Gordon in Gulf of Mexico,HURRICANE,ACTIVE,35,[],2018-09-04 14:10:53.247893,35.0,2018-09-04 05:00:00,2018-10-04 05:00:00 +AL062018,283,Florence Sep 2018,2018,FLORENCE,,HURRICANE,ACTIVE,35,[],2018-09-07 15:05:56.149271,35.0,2018-09-07 05:00:00,2018-10-07 05:00:00 +AL092018,284,Isaac Sep 2018,2018,ISAAC,,HURRICANE,COMPLETED,3,[],2018-12-06 16:10:38.342516,3.0,2018-09-11 04:00:00,2018-09-18 04:00:00 +AL142018,287,Michael Oct 2018,2018,MICHAEL,storm hit the Mexico Beach area of the Florida panhandle,HURRICANE,COMPLETED,3,[],2018-12-06 16:24:40.504991,3.0,2018-10-08 04:00:00,2018-10-15 04:00:00 +AL052019,291,2019 Hurricane Dorian,2019,DORIAN,,HURRICANE,ACTIVE,864,[],2019-08-31 18:00:27.314745,36.0,2019-08-28 04:00:00,2019-09-20 04:00:00 +AL052019,291,2019 Hurricane Dorian,2019,DORIAN,,HURRICANE,ACTIVE,864,[],2019-08-31 18:00:27.314745,36.0,2019-08-28 04:00:00,2019-09-20 04:00:00 +AL092020,301,2020 Hurricane Isaias,2020,ISAIAS,,HURRICANE,ACTIVE,1001,[],2020-07-31 11:47:44.480931,864.0,2020-07-31 05:00:00,2020-08-07 05:00:00 +AL142020,303,2020 TS Marco - Hurricane Laura,2020,MARCO,,HURRICANE,ACTIVE,864,[],2020-08-24 18:31:59.388708,864.0,2020-08-22 05:00:00,2020-08-30 05:00:00 +AL132020,303,2020 TS Marco - Hurricane Laura,2020,LAURA,,HURRICANE,ACTIVE,864,[],2020-08-24 18:31:59.388708,864.0,2020-08-22 05:00:00,2020-08-30 05:00:00 +AL192020,304,2020 Hurricane Sally,2020,SALLY,,HURRICANE,ACTIVE,864,[],2020-09-13 13:15:24.843513,864.0,2020-09-13 05:00:00,2020-09-20 05:00:00 +AL262020,305,2020 Hurricane Delta,2020,DELTA,,HURRICANE,ACTIVE,864,[],2020-10-06 12:58:46.905258,864.0,2020-10-06 05:00:00,2020-10-13 05:00:00 diff --git a/tests/test_atcf.py b/tests/test_atcf.py index 92b8eb9..3d41c78 100644 --- a/tests/test_atcf.py +++ b/tests/test_atcf.py @@ -1,16 +1,20 @@ import pytest -from stormevents.nhc.atcf import ATCF_FileDeck, atcf_files, ATCF_Mode, atcf_url, get_atcf_entry +from stormevents.nhc.atcf import ATCF_FileDeck +from stormevents.nhc.atcf import atcf_files +from stormevents.nhc.atcf import ATCF_Mode +from stormevents.nhc.atcf import atcf_url +from stormevents.nhc.atcf import get_atcf_entry def test_atcf_url(): - url_1 = atcf_url(nhc_code='AL062018') - url_2 = atcf_url(mode='REALTIME', file_deck='a') - url_3 = atcf_url(mode='HISTORICAL', file_deck='a', year=2018) + url_1 = atcf_url(nhc_code="AL062018") + url_2 = atcf_url(mode="REALTIME", file_deck="a") + url_3 = atcf_url(mode="HISTORICAL", file_deck="a", year=2018) - assert url_1 == 'ftp://ftp.nhc.noaa.gov/atcf/archive/2018/aal062018.dat.gz' - assert url_2 == 'ftp://ftp.nhc.noaa.gov/atcf/aid_public/' - assert url_3 == 'ftp://ftp.nhc.noaa.gov/atcf/archive/2018/' + assert url_1 == "ftp://ftp.nhc.noaa.gov/atcf/archive/2018/aal062018.dat.gz" + assert url_2 == "ftp://ftp.nhc.noaa.gov/atcf/aid_public/" + assert url_3 == "ftp://ftp.nhc.noaa.gov/atcf/archive/2018/" def test_atcf_nhc_codes(): @@ -26,15 +30,15 @@ def test_atcf_nhc_codes(): def test_atcf_entry(): - storm_1 = get_atcf_entry(year=2018, basin='AL', storm_number=6) - storm_2 = get_atcf_entry(year=2018, storm_name='florence') + storm_1 = get_atcf_entry(year=2018, basin="AL", storm_number=6) + storm_2 = get_atcf_entry(year=2018, storm_name="florence") with pytest.raises(ValueError): get_atcf_entry(year=2018, basin=None, storm_name=None, storm_number=None) with pytest.raises(ValueError): - get_atcf_entry(year=2018, basin='EP', storm_name='nonexistent') + get_atcf_entry(year=2018, basin="EP", storm_name="nonexistent") with pytest.raises(ValueError): - get_atcf_entry(year=2018, basin='EP', storm_number=99) + get_atcf_entry(year=2018, basin="EP", storm_number=99) - assert storm_1['name'] == 'FLORENCE' - assert storm_2['basin'] == 'AL' and storm_2['number'] == 6 + assert storm_1["name"] == "FLORENCE" + assert storm_2["basin"] == "AL" and storm_2["number"] == 6 diff --git a/tests/test_coops.py b/tests/test_coops.py index bab98b7..0f68b4a 100644 --- a/tests/test_coops.py +++ b/tests/test_coops.py @@ -1,43 +1,45 @@ -from datetime import datetime import sys +from datetime import datetime import pytest from shapely.geometry import box -from stormevents.coops.tidalstations import ( - coops_product_within_region, - COOPS_Station, - coops_stations, - coops_stations_within_region, -) -from tests import check_reference_directory, OUTPUT_DIRECTORY, REFERENCE_DIRECTORY +from stormevents.coops.tidalstations import coops_product_within_region +from stormevents.coops.tidalstations import COOPS_Station +from stormevents.coops.tidalstations import coops_stations +from stormevents.coops.tidalstations import coops_stations_within_region +from tests import check_reference_directory +from tests import OUTPUT_DIRECTORY +from tests import REFERENCE_DIRECTORY # TODO figure out why retrieved stations are different in Python 3.6 @pytest.mark.skipif( - sys.version_info < (3, 7), reason='stations list differences in Python 3.6', + sys.version_info < (3, 7), + reason="stations list differences in Python 3.6", ) def test_coops_stations(): stations = coops_stations() assert len(stations) > 0 assert list(stations.columns) == [ - 'nws_id', - 'name', - 'state', - 'status', - 'removed', - 'geometry', + "nws_id", + "name", + "state", + "status", + "removed", + "geometry", ] # TODO figure out why retrieved stations are different in Python 3.6 @pytest.mark.skipif( - sys.version_info < (3, 7), reason='stations list differences in Python 3.6', + sys.version_info < (3, 7), + reason="stations list differences in Python 3.6", ) def test_coops_stations_within_region(): - reference_directory = REFERENCE_DIRECTORY / 'test_coops_stations_within_region' - output_directory = OUTPUT_DIRECTORY / 'test_coops_stations_within_region' + reference_directory = REFERENCE_DIRECTORY / "test_coops_stations_within_region" + output_directory = OUTPUT_DIRECTORY / "test_coops_stations_within_region" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) @@ -48,14 +50,14 @@ def test_coops_stations_within_region(): assert len(stations) == 45 - stations.to_csv(output_directory / 'stations.csv') + stations.to_csv(output_directory / "stations.csv") check_reference_directory(output_directory, reference_directory) def test_coops_product_within_region(): - reference_directory = REFERENCE_DIRECTORY / 'test_coops_product_within_region' - output_directory = OUTPUT_DIRECTORY / 'test_coops_product_within_region' + reference_directory = REFERENCE_DIRECTORY / "test_coops_product_within_region" + output_directory = OUTPUT_DIRECTORY / "test_coops_product_within_region" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) @@ -66,17 +68,20 @@ def test_coops_product_within_region(): end_date = datetime(2021, 1, 1, 0, 10) data = coops_product_within_region( - 'water_level', region=region, start_date=start_date, end_date=end_date, + "water_level", + region=region, + start_date=start_date, + end_date=end_date, ) - data.to_netcdf(output_directory / 'data.nc') + data.to_netcdf(output_directory / "data.nc") check_reference_directory(output_directory, reference_directory) def test_coops_station(): - reference_directory = REFERENCE_DIRECTORY / 'test_coops_station' - output_directory = OUTPUT_DIRECTORY / 'test_coops_station' + reference_directory = REFERENCE_DIRECTORY / "test_coops_station" + output_directory = OUTPUT_DIRECTORY / "test_coops_station" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) @@ -85,40 +90,42 @@ def test_coops_station(): end_date = datetime(2021, 1, 1, 0, 10) station_1 = COOPS_Station(1612480) - station_2 = COOPS_Station('OOUH1') - station_3 = COOPS_Station('Calcasieu Test Station') + station_2 = COOPS_Station("OOUH1") + station_3 = COOPS_Station("Calcasieu Test Station") station_4 = COOPS_Station(9414458) assert station_1.nos_id == 1612480 - assert station_1.nws_id == 'MOKH1' - assert station_1.name == 'Mokuoloe' + assert station_1.nws_id == "MOKH1" + assert station_1.name == "Mokuoloe" assert station_2.nos_id == 1612340 - assert station_2.nws_id == 'OOUH1' - assert station_2.name == 'Honolulu' + assert station_2.nws_id == "OOUH1" + assert station_2.name == "Honolulu" assert station_3.nos_id == 9999531 - assert station_3.nws_id == '' - assert station_3.name == 'Calcasieu Test Station' + assert station_3.nws_id == "" + assert station_3.name == "Calcasieu Test Station" assert station_4.nos_id == 9414458 - assert station_4.nws_id == 'ZSMC1' - assert station_4.name == 'San Mateo Bridge' + assert station_4.nws_id == "ZSMC1" + assert station_4.name == "San Mateo Bridge" assert not station_4.current - station_1.constituents.to_csv(output_directory / 'station1612480_constituents.csv') - station_2.constituents.to_csv(output_directory / 'station1612340_constituents.csv') + station_1.constituents.to_csv(output_directory / "station1612480_constituents.csv") + station_2.constituents.to_csv(output_directory / "station1612340_constituents.csv") assert len(station_3.constituents) == 0 - station_4.constituents.to_csv(output_directory / 'station9414458_constituents.csv') + station_4.constituents.to_csv(output_directory / "station9414458_constituents.csv") - station_1_data = station_1.product('water_level', start_date, end_date) + station_1_data = station_1.product("water_level", start_date, end_date) - station_2_data = station_2.product('water_level', start_date, end_date) + station_2_data = station_2.product("water_level", start_date, end_date) - station_3_data = station_3.product('water_level', start_date, end_date) + station_3_data = station_3.product("water_level", start_date, end_date) - station_4_data = station_4.product('water_level', '2005-03-30', '2005-03-30 02:00:00') + station_4_data = station_4.product( + "water_level", "2005-03-30", "2005-03-30 02:00:00" + ) - assert station_1_data.sizes == {'nos_id': 1, 't': 2} - assert station_2_data.sizes == {'nos_id': 1, 't': 2} - assert len(station_3_data['t']) == 0 - assert station_4_data.sizes == {'nos_id': 1, 't': 21} + assert station_1_data.sizes == {"nos_id": 1, "t": 2} + assert station_2_data.sizes == {"nos_id": 1, "t": 2} + assert len(station_3_data["t"]) == 0 + assert station_4_data.sizes == {"nos_id": 1, "t": 21} check_reference_directory(output_directory, reference_directory) diff --git a/tests/test_nhc.py b/tests/test_nhc.py index 6036fa1..364da3b 100644 --- a/tests/test_nhc.py +++ b/tests/test_nhc.py @@ -1,86 +1,86 @@ +import sys from copy import copy from datetime import timedelta -import sys import numpy import pytest from pytest_socket import SocketBlockedError -from stormevents.nhc.storms import nhc_storms, nhc_storms_gis_archive +from stormevents.nhc.storms import nhc_storms +from stormevents.nhc.storms import nhc_storms_gis_archive from stormevents.nhc.track import VortexTrack -from tests import ( - check_reference_directory, - INPUT_DIRECTORY, - OUTPUT_DIRECTORY, - REFERENCE_DIRECTORY, -) +from tests import check_reference_directory +from tests import INPUT_DIRECTORY +from tests import OUTPUT_DIRECTORY +from tests import REFERENCE_DIRECTORY def test_nhc_gis_storms(): - reference_directory = REFERENCE_DIRECTORY / 'test_nhc_gis_storms' - output_directory = OUTPUT_DIRECTORY / 'test_nhc_gis_storms' + reference_directory = REFERENCE_DIRECTORY / "test_nhc_gis_storms" + output_directory = OUTPUT_DIRECTORY / "test_nhc_gis_storms" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) storms = nhc_storms_gis_archive(year=tuple(range(2008, 2021 + 1))) - storms.to_csv(output_directory / 'storms.csv') + storms.to_csv(output_directory / "storms.csv") check_reference_directory(output_directory, reference_directory) def test_nhc_storms(): - output_directory = OUTPUT_DIRECTORY / 'test_nhc_storms' - reference_directory = REFERENCE_DIRECTORY / 'test_nhc_storms' + output_directory = OUTPUT_DIRECTORY / "test_nhc_storms" + reference_directory = REFERENCE_DIRECTORY / "test_nhc_storms" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) storms = nhc_storms(year=tuple(range(1851, 2021 + 1))) - storms.to_csv(output_directory / 'storms.csv') + storms.to_csv(output_directory / "storms.csv") check_reference_directory(output_directory, reference_directory) def test_vortex_track(): - output_directory = OUTPUT_DIRECTORY / 'test_vortex_track' - reference_directory = REFERENCE_DIRECTORY / 'test_vortex_track' + output_directory = OUTPUT_DIRECTORY / "test_vortex_track" + reference_directory = REFERENCE_DIRECTORY / "test_vortex_track" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) storms = [ - ('michael', 2018), - ('florence', 2018), - ('irma', 2017), - ('maria', 2017), - ('harvey', 2017), - ('sandy', 2012), - ('irene', 2011), - ('ike', 2008), - ('isabel', 2003), + ("michael", 2018), + ("florence", 2018), + ("irma", 2017), + ("maria", 2017), + ("harvey", 2017), + ("sandy", 2012), + ("irene", 2011), + ("ike", 2008), + ("isabel", 2003), ] for storm in storms: - track = VortexTrack.from_storm_name(*storm, file_deck='b') + track = VortexTrack.from_storm_name(*storm, file_deck="b") track.to_file( - output_directory / f'{track.name.lower()}{track.year}.fort.22', overwrite=True + output_directory / f"{track.name.lower()}{track.year}.fort.22", + overwrite=True, ) check_reference_directory(output_directory, reference_directory) def test_vortex_track_isotachs(): - track_1 = VortexTrack('florence2018') - track_2 = VortexTrack('florence2018', file_deck='a') + track_1 = VortexTrack("florence2018") + track_2 = VortexTrack("florence2018", file_deck="a") track_1.isotachs(34) track_2.isotachs(34) def test_vortex_track_properties(): - track = VortexTrack('florence2018', file_deck='a') + track = VortexTrack("florence2018", file_deck="a") assert len(track) == 10090 @@ -92,7 +92,7 @@ def test_vortex_track_properties(): assert len(track) == 9894 - track.advisories = 'OFCL' + track.advisories = "OFCL" assert len(track) == 1249 @@ -100,153 +100,163 @@ def test_vortex_track_properties(): assert len(track) == 1289 - track.nhc_code = 'AL072018' + track.nhc_code = "AL072018" assert len(track) == 175 def test_vortex_track_tracks(): - track = VortexTrack.from_storm_name('florence', 2018, file_deck='a') + track = VortexTrack.from_storm_name("florence", 2018, file_deck="a") tracks = track.tracks assert len(tracks) == 4 - assert len(tracks['OFCL']) == 77 - assert len(tracks['OFCL']['20180831T000000']) == 13 + assert len(tracks["OFCL"]) == 77 + assert len(tracks["OFCL"]["20180831T000000"]) == 13 @pytest.mark.disable_socket def test_vortex_track_from_file(): - input_directory = INPUT_DIRECTORY / 'test_vortex_track_from_file' - output_directory = OUTPUT_DIRECTORY / 'test_vortex_track_from_file' - reference_directory = REFERENCE_DIRECTORY / 'test_vortex_track_from_file' + input_directory = INPUT_DIRECTORY / "test_vortex_track_from_file" + output_directory = OUTPUT_DIRECTORY / "test_vortex_track_from_file" + reference_directory = REFERENCE_DIRECTORY / "test_vortex_track_from_file" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) - track_1 = VortexTrack.from_file(input_directory / 'irma2017_fort.22') - track_2 = VortexTrack.from_file(input_directory / 'AL062018.dat') + track_1 = VortexTrack.from_file(input_directory / "irma2017_fort.22") + track_2 = VortexTrack.from_file(input_directory / "AL062018.dat") - assert track_1.nhc_code == 'AL112017' - assert track_1.name == 'IRMA' - assert track_2.nhc_code == 'AL062018' - assert track_2.name == 'FLORENCE' + assert track_1.nhc_code == "AL112017" + assert track_1.name == "IRMA" + assert track_2.nhc_code == "AL062018" + assert track_2.name == "FLORENCE" - track_1.to_file(output_directory / 'irma2017_fort.22', overwrite=True) - track_2.to_file(output_directory / 'florence2018_fort.22', overwrite=True) + track_1.to_file(output_directory / "irma2017_fort.22", overwrite=True) + track_2.to_file(output_directory / "florence2018_fort.22", overwrite=True) check_reference_directory(output_directory, reference_directory) def test_vortex_track_to_file(): - output_directory = OUTPUT_DIRECTORY / 'test_vortex_track_to_file' - reference_directory = REFERENCE_DIRECTORY / 'test_vortex_track_to_file' + output_directory = OUTPUT_DIRECTORY / "test_vortex_track_to_file" + reference_directory = REFERENCE_DIRECTORY / "test_vortex_track_to_file" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) - track_1 = VortexTrack.from_storm_name('florence', 2018) - track_1.to_file(output_directory / 'florence2018_best.dat', overwrite=True) - track_1.to_file(output_directory / 'florence2018_best.fort.22', overwrite=True) + track_1 = VortexTrack.from_storm_name("florence", 2018) + track_1.to_file(output_directory / "florence2018_best.dat", overwrite=True) + track_1.to_file(output_directory / "florence2018_best.fort.22", overwrite=True) - track_2 = VortexTrack.from_storm_name('florence', 2018, file_deck='a') - track_2.to_file(output_directory / 'florence2018_all.dat', overwrite=True) - track_2.to_file(output_directory / 'florence2018_all.fort.22', overwrite=True) + track_2 = VortexTrack.from_storm_name("florence", 2018, file_deck="a") + track_2.to_file(output_directory / "florence2018_all.dat", overwrite=True) + track_2.to_file(output_directory / "florence2018_all.fort.22", overwrite=True) track_2.to_file( - output_directory / 'florence2018_OFCL.dat', advisory='OFCL', overwrite=True + output_directory / "florence2018_OFCL.dat", advisory="OFCL", overwrite=True ) track_2.to_file( - output_directory / 'florence2018_OFCL.fort.22', advisory='OFCL', overwrite=True + output_directory / "florence2018_OFCL.fort.22", advisory="OFCL", overwrite=True ) check_reference_directory(output_directory, reference_directory) def test_vortex_track_distances(): - track_1 = VortexTrack.from_storm_name('florence', 2018) - track_2 = VortexTrack.from_storm_name('florence', 2018, file_deck='a', advisories=['OFCL']) + track_1 = VortexTrack.from_storm_name("florence", 2018) + track_2 = VortexTrack.from_storm_name( + "florence", 2018, file_deck="a", advisories=["OFCL"] + ) - assert numpy.isclose(track_1.distances['BEST']['20180830T060000'], 8725961.838567913) - assert numpy.isclose(track_2.distances['OFCL']['20180831T000000'], 8882602.389540724) + assert numpy.isclose( + track_1.distances["BEST"]["20180830T060000"], 8725961.838567913 + ) + assert numpy.isclose( + track_2.distances["OFCL"]["20180831T000000"], 8882602.389540724 + ) def test_vortex_track_recompute_velocity(): - output_directory = OUTPUT_DIRECTORY / 'test_vortex_track_recompute_velocity' - reference_directory = REFERENCE_DIRECTORY / 'test_vortex_track_recompute_velocity' + output_directory = OUTPUT_DIRECTORY / "test_vortex_track_recompute_velocity" + reference_directory = REFERENCE_DIRECTORY / "test_vortex_track_recompute_velocity" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) - track = VortexTrack('irma2017') + track = VortexTrack("irma2017") - track.data.at[5, 'longitude'] -= 0.1 - track.data.at[5, 'latitude'] += 0.1 + track.data.at[5, "longitude"] -= 0.1 + track.data.at[5, "latitude"] += 0.1 - track.to_file(output_directory / 'irma2017_fort.22', overwrite=True) + track.to_file(output_directory / "irma2017_fort.22", overwrite=True) check_reference_directory(output_directory, reference_directory) def test_vortex_track_file_decks(): - output_directory = OUTPUT_DIRECTORY / 'test_vortex_track_file_decks' - reference_directory = REFERENCE_DIRECTORY / 'test_vortex_track_file_decks' + output_directory = OUTPUT_DIRECTORY / "test_vortex_track_file_decks" + reference_directory = REFERENCE_DIRECTORY / "test_vortex_track_file_decks" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) file_decks = { - 'a': { - 'start_date': '2018-09-11 06:00', - 'end_date': None, - 'advisories': ['OFCL', 'HWRF', 'HMON', 'CARQ'], + "a": { + "start_date": "2018-09-11 06:00", + "end_date": None, + "advisories": ["OFCL", "HWRF", "HMON", "CARQ"], }, - 'b': { - 'start_date': '2018-09-11 06:00', - 'end_date': '2018-09-18 06:00', - 'advisories': ['BEST'], + "b": { + "start_date": "2018-09-11 06:00", + "end_date": "2018-09-18 06:00", + "advisories": ["BEST"], }, } for file_deck, values in file_decks.items(): - for advisory in values['advisories']: + for advisory in values["advisories"]: track = VortexTrack( - 'al062018', - start_date=values['start_date'], - end_date=values['end_date'], + "al062018", + start_date=values["start_date"], + end_date=values["end_date"], file_deck=file_deck, advisories=advisory, ) - track.to_file(output_directory / f'{file_deck}-deck_{advisory}.22', overwrite=True) + track.to_file( + output_directory / f"{file_deck}-deck_{advisory}.22", overwrite=True + ) check_reference_directory(output_directory, reference_directory) @pytest.mark.disable_socket def test_vortex_track_no_internet(): - input_directory = INPUT_DIRECTORY / 'test_vortex_track_no_internet' - output_directory = OUTPUT_DIRECTORY / 'test_vortex_track_no_internet' - reference_directory = REFERENCE_DIRECTORY / 'test_vortex_track_no_internet' + input_directory = INPUT_DIRECTORY / "test_vortex_track_no_internet" + output_directory = OUTPUT_DIRECTORY / "test_vortex_track_no_internet" + reference_directory = REFERENCE_DIRECTORY / "test_vortex_track_no_internet" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) with pytest.raises((ConnectionError, SocketBlockedError)): - VortexTrack(storm='florence2018') + VortexTrack(storm="florence2018") with pytest.raises((ConnectionError, SocketBlockedError)): - VortexTrack(storm='al062018', start_date='20180911', end_date=None) + VortexTrack(storm="al062018", start_date="20180911", end_date=None) - track_1 = VortexTrack.from_file(input_directory / 'fort.22') - track_1.to_file(output_directory / 'vortex_1.22', overwrite=True) + track_1 = VortexTrack.from_file(input_directory / "fort.22") + track_1.to_file(output_directory / "vortex_1.22", overwrite=True) track_2 = VortexTrack.from_file(track_1.filename) - track_2.to_file(output_directory / 'vortex_2.22', overwrite=True) + track_2.to_file(output_directory / "vortex_2.22", overwrite=True) track_3 = copy(track_1) - track_3.to_file(output_directory / 'vortex_3.22', overwrite=True) + track_3.to_file(output_directory / "vortex_3.22", overwrite=True) assert track_1 == track_2 - assert track_1 != track_3 # these are not the same because of the velocity recalculation + assert ( + track_1 != track_3 + ) # these are not the same because of the velocity recalculation check_reference_directory(output_directory, reference_directory) diff --git a/tests/test_stormevent.py b/tests/test_stormevent.py index 16082d1..58c1bc5 100644 --- a/tests/test_stormevent.py +++ b/tests/test_stormevent.py @@ -1,38 +1,41 @@ -from datetime import datetime, timedelta import os +from datetime import datetime +from datetime import timedelta import pytest import shapely from shapely.geometry import box from stormevents.stormevent import StormEvent -from tests import check_reference_directory, OUTPUT_DIRECTORY, REFERENCE_DIRECTORY +from tests import check_reference_directory +from tests import OUTPUT_DIRECTORY +from tests import REFERENCE_DIRECTORY @pytest.fixture def florence2018() -> StormEvent: - return StormEvent('florence', 2018) + return StormEvent("florence", 2018) @pytest.fixture def ida2021() -> StormEvent: - return StormEvent('florence', 2018) + return StormEvent("florence", 2018) def test_storm_event_lookup(): - florence2018 = StormEvent('florence', 2018) - paine2016 = StormEvent.from_nhc_code('EP172016') + florence2018 = StormEvent("florence", 2018) + paine2016 = StormEvent.from_nhc_code("EP172016") henri2021 = StormEvent.from_usgs_id(310) - ida2021 = StormEvent('ida', 2021) + ida2021 = StormEvent("ida", 2021) with pytest.raises(ValueError): - StormEvent('nonexistent', 2021) + StormEvent("nonexistent", 2021) with pytest.raises(ValueError): - StormEvent.from_nhc_code('nonexistent') + StormEvent.from_nhc_code("nonexistent") with pytest.raises(ValueError): - StormEvent.from_nhc_code('AL992021') + StormEvent.from_nhc_code("AL992021") with pytest.raises(ValueError): StormEvent.from_nhc_code(-1) @@ -40,12 +43,12 @@ def test_storm_event_lookup(): with pytest.raises(ValueError): StormEvent.from_usgs_id(-1) - assert florence2018.name == 'FLORENCE' + assert florence2018.name == "FLORENCE" assert florence2018.year == 2018 - assert florence2018.basin == 'AL' + assert florence2018.basin == "AL" assert florence2018.number == 6 assert florence2018._StormEvent__data_start == datetime(2018, 8, 30, 6) - assert florence2018.nhc_code == 'AL062018' + assert florence2018.nhc_code == "AL062018" assert florence2018.usgs_id == 283 assert florence2018.start_date == datetime(2018, 8, 30, 6) assert florence2018.end_date == datetime(2018, 9, 18, 12) @@ -54,41 +57,41 @@ def test_storm_event_lookup(): == "StormEvent(name='FLORENCE', year=2018, start_date=Timestamp('2018-08-30 06:00:00'), end_date=Timestamp('2018-09-18 12:00:00'))" ) - assert paine2016.name == 'PAINE' + assert paine2016.name == "PAINE" assert paine2016.year == 2016 - assert paine2016.nhc_code == 'EP172016' + assert paine2016.nhc_code == "EP172016" assert paine2016.usgs_id is None assert paine2016.start_date == datetime(2016, 9, 18) assert paine2016.end_date == datetime(2016, 9, 21, 12) - assert henri2021.name == 'HENRI' + assert henri2021.name == "HENRI" assert henri2021.year == 2021 - assert henri2021.nhc_code == 'AL082021' + assert henri2021.nhc_code == "AL082021" assert henri2021.usgs_id == 310 assert henri2021.start_date == datetime(2021, 8, 20, 18) assert henri2021.end_date == datetime(2021, 8, 24, 18) - assert ida2021.name == 'IDA' + assert ida2021.name == "IDA" assert ida2021.year == 2021 - assert ida2021.nhc_code == 'AL092021' + assert ida2021.nhc_code == "AL092021" assert ida2021.usgs_id == 312 assert ida2021.start_date == datetime(2021, 8, 27, 18) assert ida2021.end_date == datetime(2021, 9, 4, 18) def test_storm_event_time_interval(): - florence2018 = StormEvent('florence', 2018, start_date=timedelta(days=-2)) - paine2016 = StormEvent.from_nhc_code('EP172016', end_date=timedelta(days=1)) + florence2018 = StormEvent("florence", 2018, start_date=timedelta(days=-2)) + paine2016 = StormEvent.from_nhc_code("EP172016", end_date=timedelta(days=1)) henri2021 = StormEvent.from_usgs_id( 310, start_date=timedelta(days=-4), end_date=timedelta(days=-2) ) ida2021 = StormEvent( - 'ida', 2021, start_date=datetime(2021, 8, 30), end_date=datetime(2021, 9, 1) + "ida", 2021, start_date=datetime(2021, 8, 30), end_date=datetime(2021, 9, 1) ) # test times outside base interval - StormEvent('florence', 2018, start_date=timedelta(days=30)) - StormEvent('florence', 2018, start_date=datetime(2018, 10, 1)) + StormEvent("florence", 2018, start_date=timedelta(days=30)) + StormEvent("florence", 2018, start_date=datetime(2018, 10, 1)) assert florence2018.start_date == datetime(2018, 9, 16, 12) assert florence2018.end_date == datetime(2018, 9, 18, 12) @@ -120,8 +123,8 @@ def test_storm_event_time_interval(): def test_storm_event_track(florence2018, ida2021): - reference_directory = REFERENCE_DIRECTORY / 'test_storm_event_track' - output_directory = OUTPUT_DIRECTORY / 'test_storm_event_track' + reference_directory = REFERENCE_DIRECTORY / "test_storm_event_track" + output_directory = OUTPUT_DIRECTORY / "test_storm_event_track" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) @@ -129,15 +132,15 @@ def test_storm_event_track(florence2018, ida2021): florence_track = florence2018.track() ida_track = ida2021.track() - florence_track.to_file(output_directory / 'florence2018.fort.22') - ida_track.to_file(output_directory / 'ida2021.fort.22') + florence_track.to_file(output_directory / "florence2018.fort.22") + ida_track.to_file(output_directory / "ida2021.fort.22") check_reference_directory(output_directory, reference_directory) def test_storm_event_high_water_marks(florence2018): - reference_directory = REFERENCE_DIRECTORY / 'test_storm_event_high_water_marks' - output_directory = OUTPUT_DIRECTORY / 'test_storm_event_high_water_marks' + reference_directory = REFERENCE_DIRECTORY / "test_storm_event_high_water_marks" + output_directory = OUTPUT_DIRECTORY / "test_storm_event_high_water_marks" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) @@ -145,14 +148,18 @@ def test_storm_event_high_water_marks(florence2018): flood = florence2018.flood_event high_water_marks = flood.high_water_marks() - high_water_marks.to_csv(output_directory / 'florence2018_hwm.csv') + high_water_marks.to_csv(output_directory / "florence2018_hwm.csv") check_reference_directory(output_directory, reference_directory) def test_storm_event_coops_product_within_isotach(florence2018): - reference_directory = REFERENCE_DIRECTORY / 'test_storm_event_coops_product_within_isotach' - output_directory = OUTPUT_DIRECTORY / 'test_storm_event_coops_product_within_isotach' + reference_directory = ( + REFERENCE_DIRECTORY / "test_storm_event_coops_product_within_isotach" + ) + output_directory = ( + OUTPUT_DIRECTORY / "test_storm_event_coops_product_within_isotach" + ) if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) for path in output_directory.iterdir(): @@ -160,30 +167,32 @@ def test_storm_event_coops_product_within_isotach(florence2018): os.remove(path) null_data = florence2018.coops_product_within_isotach( - 'water_level', wind_speed=34, end_date=florence2018.start_date + timedelta(hours=1), + "water_level", + wind_speed=34, + end_date=florence2018.start_date + timedelta(hours=1), ) tidal_data = florence2018.coops_product_within_isotach( - 'water_level', + "water_level", wind_speed=34, start_date=datetime(2018, 9, 13), end_date=datetime(2018, 9, 14), ) assert len(null_data.data_vars) == 0 - assert list(tidal_data.data_vars) == ['v', 's', 'f', 'q'] + assert list(tidal_data.data_vars) == ["v", "s", "f", "q"] - assert null_data['t'].sizes == {} - assert tidal_data.sizes == {'nos_id': 6, 't': 241} + assert null_data["t"].sizes == {} + assert tidal_data.sizes == {"nos_id": 6, "t": 241} - tidal_data.to_netcdf(output_directory / 'florence2018_water_levels.nc') + tidal_data.to_netcdf(output_directory / "florence2018_water_levels.nc") check_reference_directory(output_directory, reference_directory) def test_storm_event_coops_product_within_region(florence2018): null_tidal_data = florence2018.coops_product_within_region( - 'water_level', + "water_level", region=shapely.geometry.box(0, 0, 1, 1), start_date=florence2018.start_date, end_date=florence2018.start_date + timedelta(minutes=1), @@ -192,14 +201,14 @@ def test_storm_event_coops_product_within_region(florence2018): east_coast = shapely.geometry.box(-85, 25, -65, 45) east_coast_tidal_data = florence2018.coops_product_within_region( - 'water_level', + "water_level", region=east_coast, start_date=datetime(2018, 9, 13, 23, 59), end_date=datetime(2018, 9, 14), ) assert len(null_tidal_data.data_vars) == 0 - assert list(east_coast_tidal_data.data_vars) == ['v', 's', 'f', 'q'] + assert list(east_coast_tidal_data.data_vars) == ["v", "s", "f", "q"] - assert null_tidal_data['t'].sizes == {} - assert east_coast_tidal_data.sizes == {'nos_id': 111, 't': 1} + assert null_tidal_data["t"].sizes == {} + assert east_coast_tidal_data.sizes == {"nos_id": 111, "t": 1} diff --git a/tests/test_usgs.py b/tests/test_usgs.py index 0f7769e..09d7012 100644 --- a/tests/test_usgs.py +++ b/tests/test_usgs.py @@ -2,23 +2,26 @@ import pytest -from stormevents.usgs import USGS_Event, usgs_flood_events, usgs_flood_storms, USGS_StormEvent -from stormevents.usgs.base import EventStatus, EventType +from stormevents.usgs import USGS_Event +from stormevents.usgs import usgs_flood_events +from stormevents.usgs import usgs_flood_storms +from stormevents.usgs import USGS_StormEvent +from stormevents.usgs.base import EventStatus +from stormevents.usgs.base import EventType from stormevents.usgs.highwatermarks import HighWaterMarksQuery -from tests import ( - check_reference_directory, - INPUT_DIRECTORY, - OUTPUT_DIRECTORY, - REFERENCE_DIRECTORY, -) +from tests import check_reference_directory +from tests import INPUT_DIRECTORY +from tests import OUTPUT_DIRECTORY +from tests import REFERENCE_DIRECTORY @pytest.mark.skipif( - sys.version_info < (3, 10), reason='difference in datetime format before Python 3.10', + sys.version_info < (3, 10), + reason="difference in datetime format before Python 3.10", ) def test_usgs_flood_events(): - reference_directory = REFERENCE_DIRECTORY / 'test_usgs_flood_events' - output_directory = OUTPUT_DIRECTORY / 'test_usgs_flood_events' + reference_directory = REFERENCE_DIRECTORY / "test_usgs_flood_events" + output_directory = OUTPUT_DIRECTORY / "test_usgs_flood_events" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) @@ -31,56 +34,58 @@ def test_usgs_flood_events(): event_status=EventStatus.COMPLETED, ) - events.to_csv(output_directory / 'events.csv') + events.to_csv(output_directory / "events.csv") check_reference_directory(output_directory, reference_directory) @pytest.mark.skipif( - sys.version_info < (3, 10), reason='difference in datetime format before Python 3.10', + sys.version_info < (3, 10), + reason="difference in datetime format before Python 3.10", ) def test_usgs_flood_storms(): - reference_directory = REFERENCE_DIRECTORY / 'test_usgs_flood_storms' - output_directory = OUTPUT_DIRECTORY / 'test_usgs_flood_storms' + reference_directory = REFERENCE_DIRECTORY / "test_usgs_flood_storms" + output_directory = OUTPUT_DIRECTORY / "test_usgs_flood_storms" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) storms = usgs_flood_storms(year=tuple(range(2003, 2020 + 1))) - storms.to_csv(output_directory / 'storms.csv') + storms.to_csv(output_directory / "storms.csv") check_reference_directory(output_directory, reference_directory) @pytest.mark.skipif( - sys.version_info < (3, 10), reason='difference in datetime format before Python 3.10', + sys.version_info < (3, 10), + reason="difference in datetime format before Python 3.10", ) def test_usgs_flood_event(): - input_directory = INPUT_DIRECTORY / 'test_usgs_flood_event' - reference_directory = REFERENCE_DIRECTORY / 'test_usgs_flood_event' - output_directory = OUTPUT_DIRECTORY / 'test_usgs_flood_event' + input_directory = INPUT_DIRECTORY / "test_usgs_flood_event" + reference_directory = REFERENCE_DIRECTORY / "test_usgs_flood_event" + output_directory = OUTPUT_DIRECTORY / "test_usgs_flood_event" if not output_directory.exists(): output_directory.mkdir(parents=True, exist_ok=True) - flood_1 = USGS_Event.from_csv(input_directory / 'florence2018.csv') - flood_2 = USGS_Event.from_name('Irma September 2017') + flood_1 = USGS_Event.from_csv(input_directory / "florence2018.csv") + flood_2 = USGS_Event.from_name("Irma September 2017") with pytest.raises(ValueError): - USGS_StormEvent.from_name('nonexistent') + USGS_StormEvent.from_name("nonexistent") assert flood_2.high_water_marks().shape == (506, 53) assert flood_1 != flood_2 - flood_1.high_water_marks().to_csv(output_directory / 'florence2018.csv') + flood_1.high_water_marks().to_csv(output_directory / "florence2018.csv") check_reference_directory(output_directory, reference_directory) def test_usgs_high_water_marks_query(): query_1 = HighWaterMarksQuery(182) - query_2 = HighWaterMarksQuery(23, quality='EXCELLENT') - query_3 = HighWaterMarksQuery('nonexistent') + query_2 = HighWaterMarksQuery(23, quality="EXCELLENT") + query_3 = HighWaterMarksQuery("nonexistent") assert len(query_1.data) == 506 assert len(query_2.data) == 148 @@ -88,9 +93,9 @@ def test_usgs_high_water_marks_query(): with pytest.raises(ValueError): query_3.data - query_1.quality = 'EXCELLENT', 'GOOD' - query_2.quality = 'EXCELLENT', 'GOOD' - query_3.quality = 'EXCELLENT', 'GOOD' + query_1.quality = "EXCELLENT", "GOOD" + query_2.quality = "EXCELLENT", "GOOD" + query_3.quality = "EXCELLENT", "GOOD" assert len(query_1.data) == 277 assert len(query_2.data) == 628 diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 7d36e7a..64289b5 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,9 +1,11 @@ -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta import numpy import pytest -from stormevents.utilities import relative_to_time_interval, subset_time_interval +from stormevents.utilities import relative_to_time_interval +from stormevents.utilities import subset_time_interval def test_subset_time_interval(): @@ -13,22 +15,24 @@ def test_subset_time_interval(): datetime(2020, 2, 1), datetime(2020, 11, 1), ) - interval_2 = subset_time_interval('2020-01-01', '2021-01-01', '2020-02-01', '2020-11-01') + interval_2 = subset_time_interval( + "2020-01-01", "2021-01-01", "2020-02-01", "2020-11-01" + ) interval_3 = subset_time_interval( - '2020-01-01', '2021-01-01', '2020-02-01', -1 * 31 * 24 * 60 * 60 + "2020-01-01", "2021-01-01", "2020-02-01", -1 * 31 * 24 * 60 * 60 ) with pytest.raises(ValueError): - subset_time_interval('2021-01-01', '2020-01-01', '2020-02-01', '2020-11-01') + subset_time_interval("2021-01-01", "2020-01-01", "2020-02-01", "2020-11-01") with pytest.raises(ValueError): - subset_time_interval('2020-01-01', '2021-01-01', '2020-11-01', '2020-02-01') + subset_time_interval("2020-01-01", "2021-01-01", "2020-11-01", "2020-02-01") with pytest.raises(ValueError): - subset_time_interval(None, '2021-01-01', '2020-02-01', '2020-11-01') + subset_time_interval(None, "2021-01-01", "2020-02-01", "2020-11-01") with pytest.raises(ValueError): - subset_time_interval('2020-01-01', numpy.nan, '2020-02-01', '2020-11-01') + subset_time_interval("2020-01-01", numpy.nan, "2020-02-01", "2020-11-01") assert interval_1 == (datetime(2020, 2, 1), datetime(2020, 11, 1)) assert interval_2 == (datetime(2020, 2, 1), datetime(2020, 11, 1)) @@ -37,26 +41,30 @@ def test_subset_time_interval(): def test_relative_to_time_interval(): time_1 = relative_to_time_interval( - datetime(2020, 1, 1), datetime(2021, 1, 1), datetime(2020, 2, 1), + datetime(2020, 1, 1), + datetime(2021, 1, 1), + datetime(2020, 2, 1), + ) + time_2 = relative_to_time_interval("2020-01-01", "2021-01-01", "2020-02-01") + time_3 = relative_to_time_interval( + "2020-01-01", "2021-01-01", timedelta(days=30 * 3) ) - time_2 = relative_to_time_interval('2020-01-01', '2021-01-01', '2020-02-01') - time_3 = relative_to_time_interval('2020-01-01', '2021-01-01', timedelta(days=30 * 3)) - time_4 = relative_to_time_interval('2020-01-01', '2021-01-01', 5 * 24 * 60 * 60) + time_4 = relative_to_time_interval("2020-01-01", "2021-01-01", 5 * 24 * 60 * 60) with pytest.raises(ValueError): - relative_to_time_interval('2021-01-01', '2020-01-01', '2020-02-01') + relative_to_time_interval("2021-01-01", "2020-01-01", "2020-02-01") with pytest.raises(ValueError): - relative_to_time_interval('2021-01-01', '2020-01-01', '2020-02-01') + relative_to_time_interval("2021-01-01", "2020-01-01", "2020-02-01") with pytest.raises(ValueError): - relative_to_time_interval('2020-01-01', '2021-01-01', '2021-02-01') + relative_to_time_interval("2020-01-01", "2021-01-01", "2021-02-01") with pytest.raises(ValueError): - relative_to_time_interval('2020-01-01', '2021-01-01', None) + relative_to_time_interval("2020-01-01", "2021-01-01", None) with pytest.raises(ValueError): - relative_to_time_interval(numpy.nan, '2021-01-01', '2020-02-01') + relative_to_time_interval(numpy.nan, "2021-01-01", "2020-02-01") assert time_1 == datetime(2020, 2, 1) assert time_2 == datetime(2020, 2, 1)