diff --git a/stormevents/stormevent.py b/stormevents/stormevent.py index 31673f0..216a268 100644 --- a/stormevents/stormevent.py +++ b/stormevents/stormevent.py @@ -7,6 +7,7 @@ import pandas import xarray +from pandas import Series from searvey.coops import COOPS_Interval from searvey.coops import COOPS_Product from searvey.coops import COOPS_Station @@ -49,12 +50,15 @@ def __init__( year: int, start_date: datetime = None, end_date: datetime = None, + synthetic: bool = False, + **kwargs, ): """ :param name: storm name :param year: storm year :param start_date: starting time :param end_date: ending time + :param synthetic: whether the storm actually exists; `True` will skip lookup in the storm table >>> StormEvent('florence', 2018) StormEvent(name='FLORENCE', year=2018, start_date=Timestamp('2018-08-30 06:00:00'), end_date=Timestamp('2018-09-18 12:00:00')) @@ -72,17 +76,30 @@ def __init__( StormEvent(name='IDA', year=2021, start_date=Timestamp('2021-08-27 18:00:00'), end_date=Timestamp('2021-08-29 18:00:00')) """ - storms = nhc_storms(year=year) - storms = storms[storms["name"].str.contains(name.upper())] - if len(storms) > 0: - self.__entry = storms.iloc[0] + if not synthetic: + storms = nhc_storms(year=year) + storms = storms[storms["name"].str.contains(name.upper())] + if len(storms) > 0: + self.__entry = storms.iloc[0] + else: + raise ValueError(f'storm "{name} {year}" not found in NHC database') else: - raise ValueError(f'storm "{name} {year}" not found in NHC database') + self.__entry = Series( + { + "name": name, + "year": year, + "start_date": start_date, + "end_date": end_date, + **kwargs, + }, + index=None, + ) 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.__synthetic = synthetic self.start_date = start_date self.end_date = end_date @@ -227,7 +244,7 @@ def start_date(self, start_date: datetime): @lru_cache(maxsize=None) def __data_start(self) -> datetime: data_start = self.__entry["start_date"] - if pandas.isna(data_start): + if pandas.isna(data_start) and not self.synthetic: data_start = VortexTrack.from_storm_name(self.name, self.year).start_date return data_start @@ -252,7 +269,7 @@ def end_date(self, end_date: datetime): @lru_cache(maxsize=None) def __data_end(self) -> datetime: data_end = self.__entry["end_date"] - if pandas.isna(data_end): + if pandas.isna(data_end) and not self.synthetic: data_end = VortexTrack.from_storm_name(self.name, self.year).end_date return data_end @@ -268,6 +285,10 @@ def status(self) -> StormStatus: else: return StormStatus.HISTORICAL + @property + def synthetic(self) -> bool: + return self.__synthetic + def track( self, start_date: datetime = None, @@ -510,3 +531,49 @@ def __repr__(self) -> str: f"end_date={repr(self.end_date)}" f")" ) + + def __copy__(self) -> "StormEvent": + return self.__class__( + self.name, + year=self.year, + start_date=self.start_date, + end_date=self.end_date, + ) + + def perturb( + self, + name: str = None, + year: int = None, + start_date: datetime = None, + end_date: datetime = None, + **kwargs, + ) -> "StormEvent": + """ + :param name: storm name + :param year: storm year + :param start_date: starting time + :param end_date: ending time + :return: a new synthetic storm based on parameters from the current storm + """ + + if name is None: + name = self.name + if year is None: + year = self.year + if start_date is None: + start_date = self.start_date + elif isinstance(start_date, timedelta): + start_date = self.start_date + start_date + if end_date is None: + end_date = self.end_date + elif isinstance(end_date, timedelta): + end_date = self.end_date + end_date + + return self.__class__( + name=name, + year=year, + start_date=start_date, + end_date=end_date, + synthetic=True, + **kwargs, + ) diff --git a/tests/test_stormevent.py b/tests/test_stormevent.py index 7686a39..73543fe 100644 --- a/tests/test_stormevent.py +++ b/tests/test_stormevent.py @@ -81,6 +81,38 @@ def test_storm_event_lookup(): assert ida2021.end_date == datetime(2021, 9, 4, 18) +def test_synthetic_stormevent(florence2018): + synth_1 = StormEvent("synth_1", year=2018, synthetic=True) + synth_2 = StormEvent( + "synth_2", + start_date=datetime(2019, 10, 2), + end_date=datetime(2019, 10, 10), + year=2019, + synthetic=True, + ) + synth_florence = florence2018.perturb(name="synth_florence") + + assert synth_1.name == "synth_1" + assert synth_1.year == 2018 + assert synth_1.nhc_code is None + assert synth_1.start_date is None + assert synth_1.end_date is None + assert synth_1.synthetic + + assert synth_2.name == "synth_2" + assert synth_2.year == 2019 + assert synth_2.nhc_code is None + assert synth_2.start_date == datetime(2019, 10, 2) + assert synth_2.end_date == datetime(2019, 10, 10) + assert synth_2.synthetic + + assert synth_florence.name == "synth_florence" + assert synth_florence.year == 2018 + assert synth_florence.start_date == florence2018.start_date + assert synth_florence.end_date == florence2018.end_date + assert synth_florence.synthetic + + 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))