diff --git a/adcircpy/driver.py b/adcircpy/driver.py index 4cdd6313..9df18b27 100644 --- a/adcircpy/driver.py +++ b/adcircpy/driver.py @@ -410,6 +410,9 @@ def write( script = DriverFile(self, nproc) script.write(output_directory / driver, overwrite) + if self.wave_forcing is not None: + self.wave_forcing.write(output_directory,overwrite) + def import_stations( self, fort15: os.PathLike, diff --git a/adcircpy/forcing/waves/__init__.py b/adcircpy/forcing/waves/__init__.py index 6477dd72..0c6ca488 100644 --- a/adcircpy/forcing/waves/__init__.py +++ b/adcircpy/forcing/waves/__init__.py @@ -1,7 +1,9 @@ from adcircpy.forcing.waves.base import WaveForcing from adcircpy.forcing.waves.ww3 import WaveWatch3DataForcing +from adcircpy.forcing.waves.swan import SWANForcing __all__ = [ 'WaveForcing', 'WaveWatch3DataForcing', + 'SWANForcing', ] diff --git a/adcircpy/forcing/waves/swan.py b/adcircpy/forcing/waves/swan.py new file mode 100644 index 00000000..83815435 --- /dev/null +++ b/adcircpy/forcing/waves/swan.py @@ -0,0 +1,21 @@ +from os import PathLike +from pathlib import Path + +from adcircpy.forcing.waves import WaveForcing + +import warnings + +from adcircpy.warnings import warn_adcirc, UnsupportedFeatureWarning + +class SWANForcing(WaveForcing): + def __init__(self, nrs: int = 3, \ + interval_seconds: int = 600): + super().__init__(nrs=nrs, interval_seconds=interval_seconds) + + def write(self, directory: PathLike, overwrite: bool = False): + txt=f'ADCIRCPy does not currently support writing '\ + +f'coupled ADCIRC+SWAN input files (fort.26 and swaninit). '\ + +f'User will have to create these files themselves to run '\ + +f'padcswan.' + warn_adcirc(txt, UnsupportedFeatureWarning) + pass diff --git a/adcircpy/forcing/winds/best_track.py b/adcircpy/forcing/winds/best_track.py index 3dc25bf6..dacefdd4 100644 --- a/adcircpy/forcing/winds/best_track.py +++ b/adcircpy/forcing/winds/best_track.py @@ -4,7 +4,7 @@ import os from os import PathLike import pathlib -from typing import Union +from typing import Union, Optional, List from matplotlib import pyplot from matplotlib.axis import Axis @@ -29,12 +29,17 @@ def __init__( interval_seconds: int = None, start_date: datetime = None, end_date: datetime = None, + file_deck: str = 'b', + advisories: Optional[List[str]] = None, *args, **kwargs, ): if nws is None: nws = 20 + if advisories is None: + advisories=['BEST'] + valid_nws_values = [8, 19, 20] assert ( nws in valid_nws_values @@ -48,8 +53,8 @@ def __init__( storm=storm, start_date=start_date, end_date=end_date, - file_deck='b', - advisories=['BEST'], + file_deck=file_deck, + advisories=advisories, ) WindForcing.__init__(self, nws=nws, interval_seconds=interval_seconds) @@ -61,9 +66,20 @@ def from_fort22( interval_seconds: int = None, start_date: datetime = None, end_date: datetime = None, + file_deck: Optional[str] = None, + advisories: Optional[List[str]] = None, ) -> 'WindForcing': - instance = cls.from_file(path=fort22, start_date=start_date, end_date=end_date) - WindForcing.__init__(instance, nws=nws, interval_seconds=interval_seconds) + if file_deck is None: + file_deck='b' + if advisories is None: + advisories=['BEST'] + + instance = cls.from_file(path=fort22, start_date=start_date, \ + end_date=end_date, file_deck=file_deck,\ + advisories=advisories) + + WindForcing.__init__(instance, nws=nws, \ + interval_seconds=interval_seconds) return instance def summary( @@ -71,8 +87,11 @@ def summary( ): min_storm_speed = numpy.min(self.data['speed']) max_storm_speed = numpy.max(self.data['speed']) - track_length = self.distance - duration = self.duration + track_length=0 + for key in self.distances.keys(): + for key2 in self.distances[key].keys(): + track_length+=self.distances[key][key2] + duration = self.duration.total_seconds()/86400 # seconds to days min_central_pressure = numpy.min(self.data['central_pressure']) max_wind_speed = numpy.max(self.data['max_sustained_wind_speed']) start_loc = (self.data['longitude'][0], self.data['latitude'][0]) diff --git a/adcircpy/fort15.py b/adcircpy/fort15.py index 21f6ec3d..d4b1e0d4 100644 --- a/adcircpy/fort15.py +++ b/adcircpy/fort15.py @@ -19,6 +19,7 @@ from adcircpy.mesh.mesh import AdcircMesh +from adcircpy.warnings import warn_adcirc, ModelSetupWarning class StationType(Enum): ELEVATION = 'NSTAE' @@ -154,6 +155,9 @@ def __init__(self, mesh: AdcircMesh = None): self._mesh = mesh self._runtype = None + # initialize user-defined nameilsts + self._custom_namelists: dict[str, dict[str,str]] = {} + @property def mesh(self) -> AdcircMesh: return self._mesh @@ -167,10 +171,10 @@ def fort15(self, runtype: str) -> str: f.extend( [ fort15_line( - self.RUNDES, 'RUNDES', '32 CHARACTER ALPHANUMERIC RUN DESCRIPTION' + self.RUNDES ), fort15_line( - self.RUNID, 'RUNID', '24 CHARACTER ALPANUMERIC RUN IDENTIFICATION' + self.RUNID ), fort15_line(f'{self.NFOVER}', 'NFOVER', 'NONFATAL ERROR OVERRIDE OPTION'), fort15_line( @@ -540,26 +544,34 @@ def fort15(self, runtype: str) -> str: raise NotImplementedError('3D runs not yet implemented') f.extend( [ - fort15_line(self.NCPROJ, 'NCPROJ', 'PROJECT TITLE'), - fort15_line(self.NCINST, 'NCINST', 'PROJECT INSTITUTION'), - fort15_line(self.NCSOUR, 'NCSOUR', 'PROJECT SOURCE'), - fort15_line(self.NCHIST, 'NCHIST', 'PROJECT HISTORY'), - fort15_line(self.NCREF, 'NCREF', 'PROJECT REFERENCES'), - fort15_line(self.NCCOM, 'NCCOM', 'PROJECT COMMENTS'), - fort15_line(self.NCHOST, 'NCHOST', 'PROJECT HOST'), - fort15_line(self.NCCONV, 'NCONV', 'CONVENTIONS'), - fort15_line(self.NCCONT, 'NCCONT', 'CONTACT INFORMATION'), - fort15_line(self.NCDATE, 'NCDATE', 'forcing start date'), + fort15_line(self.NCPROJ), + fort15_line(self.NCINST), + fort15_line(self.NCSOUR), + fort15_line(self.NCHIST), + fort15_line(self.NCREF), + fort15_line(self.NCCOM), + fort15_line(self.NCHOST), + fort15_line(self.NCCONV), + fort15_line(self.NCCONT), + fort15_line(self.NCDATE), ] ) del self._outputs + def _format_namelist_value(value): + if isinstance(value, bool): + return 'T' if value else 'F' + elif isinstance(value, str): + return f'"{value}"' + else: + return str(value) + for name, namelist in self.namelists.items(): - f.append( - f'&{name} ' - + ', '.join([f'{key}={value}' for key, value in namelist.items()]) - + ' \\' - ) + f.append(f'! -- Begin {name} Namelist --') + f.append(f'&{name}') + for key, value in namelist.items(): + f.append(f' {key} = {_format_namelist_value(value)},') + f.append(f"/ ! End {name} Namelist") f.append("") return '\n'.join(f) @@ -637,8 +649,45 @@ def namelists(self) -> {str: {str: str}}: 'outputWindDrag': 'F', 'invertedBarometerOnElevationBoundary': 'T', } + + # Handle user-defined namelist values/additions + for name, overrides in self._custom_namelists.items(): + namelists.setdefault(name, {}).update(overrides) + return namelists + def add_namelist( + self, + name: str, + entries: dict[str,str] | None=None + ) -> None: + ''' + Adds or updates a namelist to the fort.15 structure + ''' + if name not in self._custom_namelists: + self._custom_namelists[name] = {} + if entries: + self._custom_namelists[name].update(entries) + + def set_namelist_values( + self, + block: str, + key: str, + value: str | float | int + ) -> None: + ''' + Sets or updates a single value within a custom namelist + ''' + if block not in self._custom_namelists: + self._custom_namelists[block] = {} + self._custom_namelists[block][key] = value + + def clear_namelists(self) -> None: + ''' + Deletes all custom namelist entries + ''' + self._custom_namelists.clear() + def set_time_weighting_factors_in_gwce(self, A00: float, B00: float, C00: float): A00 = float(A00) B00 = float(B00) @@ -962,7 +1011,7 @@ def timestep(self, timestep: float): @property def RUNDES(self) -> str: try: - self.__RUNDES + return self.__RUNDES except AttributeError: return datetime.now().strftime('created on %Y-%m-%d %H:%M') @@ -973,7 +1022,7 @@ def RUNDES(self, RUNDES: str): @property def RUNID(self) -> str: try: - self.__RUNID + return self.__RUNID except AttributeError: return self.mesh.description @@ -1409,7 +1458,12 @@ def NTIP(self) -> int: try: self.fort24 except AttributeError: - raise Exception('Must generate fort.24 file.') + warn_adcirc( + "NTIP is presently set to 2 but no self attraction" + " and loading data is present and thus no fort.24 will be" + " generated.", + ModelSetupWarning + ) return NTIP except AttributeError: return 1 @@ -1551,7 +1605,13 @@ def REFTIM(self, REFTIM: float): @property def WTIMINC(self) -> Union[int, str]: - if self.NWS in [8, 19, 20]: + if self.NWS in [8, 19]: + return ( + f'{self.forcing_start_date:%Y %m %d %H} ' + f'{self.wind_forcing.data["storm_number"].iloc[0]} ' + f'{self.wind_forcing.BLADj} ' + ) + elif self.NWS in [20]: return ( f'{self.forcing_start_date:%Y %m %d %H} ' f'{self.wind_forcing.data["storm_number"].iloc[0]} ' @@ -1582,7 +1642,7 @@ def RNDAY(self) -> int: RNDAY = self.end_date - self.start_date else: RNDAY = self.end_date - self.forcing_start_date - return RNDAY / timedelta(days=1) + return np.floor((RNDAY / timedelta(days=1))*10)/10 @property def DRAMP(self) -> str: @@ -2569,7 +2629,7 @@ def NCCONT(self, NCCONT: str): @property def NCDATE(self) -> str: - return f'{self.forcing_start_date:%Y-%m-%d %H:%M}' + return f'{self.forcing_start_date:%Y-%m-%d %H:%M:%S}' @property def FortranNamelists(self) -> str: diff --git a/adcircpy/mesh/fort14.py b/adcircpy/mesh/fort14.py index be1d8be0..3bae407c 100644 --- a/adcircpy/mesh/fort14.py +++ b/adcircpy/mesh/fort14.py @@ -221,6 +221,33 @@ def open(cls, path, crs=None): _grd['nodes'].iloc[:, 2:] *= -1 return cls(**_grd) + def update_bathymetry(self, new_values): + """ + Update the bathymetry (values column) of the Fort14 mesh. + + Parameters + ---------- + new_values : array-like + Array of bathymetry values aligned to the node index order. + """ + if len(new_values) != len(self._values): + raise ValueError("new_values must be same length as number of nodes") + + # update internal values DataFrame + self._values.iloc[:, 0] = new_values + + # keep 'nodes' table synchronized + self.nodes.iloc[:, 2] = new_values + + @property + def bathymetry(self): + '''Returns the primary bathymetry column as a numpy array''' + return self._values.iloc[:,0].to_numpy() + + @bathymetry.setter + def bathymetry(self, new_values): + self.update_bathymetry(new_values) + def write(self, path, overwrite=False, format='fort.14'): if format in ['fort.14']: _grd = self.to_dict() diff --git a/adcircpy/warnings.py b/adcircpy/warnings.py new file mode 100644 index 00000000..76c54781 --- /dev/null +++ b/adcircpy/warnings.py @@ -0,0 +1,35 @@ +import warnings + +class ADCIRCPyWarning(UserWarning): + ''' + base adcircpy warning class + ''' + pass + +class UnsupportedFeatureWarning(ADCIRCPyWarning): + ''' + Feature exists in ADCIRC but is not yet supported by adcircpy + ''' + pass + +class ModelSetupWarning(ADCIRCPyWarning): + ''' + Indicates a configuration or setup issue in adcircpy inputs + ''' + pass + +def warn_adcirc(message, category=ADCIRCPyWarning, stacklevel=2): + ''' + Prints a standardized warning message + + Parameters + ---------- + message: str + The warning message to display + category: Warning subclass, optional + The type of warning to issue (defaults to ADCIRCPyWarning) + stacklevel : int, optional + The stack level for the warning location + ''' + clean_message = (message.strip()).rstrip(".") + "." + warnings.warn(clean_message, category, stacklevel=stacklevel)