From 695abd57a7aa77b3297091820c479009cfa1e99d Mon Sep 17 00:00:00 2001 From: Kim Pevey Date: Thu, 13 Sep 2018 15:11:12 -0500 Subject: [PATCH 01/24] add getting started guide --- harmonica/examples/getting_started.ipynb | 288 +++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 harmonica/examples/getting_started.ipynb diff --git a/harmonica/examples/getting_started.ipynb b/harmonica/examples/getting_started.ipynb new file mode 100644 index 0000000..4aaf752 --- /dev/null +++ b/harmonica/examples/getting_started.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "import numpy as np\n", + "\n", + "from pytides.tide import Tide as pyTide\n", + "import harmonica\n", + "from harmonica.harmonica import Tide" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tidal Reconstruction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Tide.reconstruct_tide?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Required Input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# location of interest\n", + "location = (38.375789, -74.943915)\n", + "# datetime object of time zero\n", + "time_zero = datetime.now()\n", + "# array of hour offsets from time zero (MUST BE IN HOURS)\n", + "hours_offset_from_zero = np.arange(0, 1000, 1, dtype=float)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional Input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# set the model name\n", + "model_name = 'tpxo9'\n", + "# requested constituent(s) \n", + "constituents = None\n", + "# should phase be all positive [0 360]?\n", + "positive_phase = True\n", + "# output file\n", + "outfile = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Process input data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# convert the numpy array of offset time to datetime objects\n", + "times = pyTide._times(time_zero, hours_offset_from_zero)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Reconstruct tides" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# reconstruct the tide\n", + "tide = Tide().reconstruct_tide(loc=location, times=times, model=model_name, cons=constituents, \n", + " positive_ph=positive_phase)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### View/Save output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# if output file requested\n", + "if outfile is not None:\n", + " # write to file\n", + " tide.data.to_csv(outfile, sep='\\t', header=True, index=False)\n", + " \n", + "# display the dataframe\n", + "display(tide.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tidal Deconstruction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Tide.deconstruct_tide?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Required Input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# datetime object of time zero\n", + "time_zero = datetime.now()\n", + "# array of hour offsets from time zero (MUST BE IN HOURS)\n", + "hours_offset_from_zero = np.arange(0, 1000, 1, dtype=float)\n", + "# array of water levels\n", + "water_level = np.cos(hours_offset_from_zero)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional Input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# list of requested constituents\n", + "requested_cons = ['M2', 'S2','N2','K1','M4','O1']\n", + "# number of required periods for inclusion of consituents\n", + "periods = 4\n", + "# should phase be all positive [0 360]?\n", + "positive_ph = False\n", + "# output file\n", + "decomp_out = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Process input data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# convert the numpy array of offset time to datetime objects\n", + "times = pyTide._times(time_zero, hours_offset_from_zero)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Deconstruct tides" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "constituents = Tide().deconstruct_tide(water_level, times, cons=requested_cons, n_period=periods, positive_ph=positive_ph)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "constituents.constituents.data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# if output file requested\n", + "if decomp_out is not None:\n", + " # write to file\n", + " constituents.constituents.data.to_csv(decomp_out, sep='\\t', header=True, index=False)\n", + " \n", + "# display the dataframe\n", + "display(constituents.constituents.data)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From cae401b7707d22db939d28eb5ecf9bbfc5eb689e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 14 Sep 2018 15:51:32 -0600 Subject: [PATCH 02/24] Move LeProvost and ADCIRC tidal database extractors into Harmonica repo. Changed the new extractors to return pandas DataFrames. Changed new extractors to download databases from aqauveo.com if not found. --- harmonica/adcirc_database.py | 231 ++++++++++++++++++ harmonica/leprovost_database.py | 230 ++++++++++++++++++ harmonica/tidal_constituents.py | 91 +++---- harmonica/tidal_database.py | 415 ++++++++++++++++++++++++++++++++ test.py | 82 +++++++ 5 files changed, 1004 insertions(+), 45 deletions(-) create mode 100644 harmonica/adcirc_database.py create mode 100644 harmonica/leprovost_database.py create mode 100644 harmonica/tidal_database.py create mode 100644 test.py diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py new file mode 100644 index 0000000..8c7ef84 --- /dev/null +++ b/harmonica/adcirc_database.py @@ -0,0 +1,231 @@ +#! python3 + +from enum import Enum +import os +import subprocess +import random +import string +import shutil +import urllib.request +from zipfile import ZipFile + +import pandas as pd + +from .tidal_constituents import NOAA_SPEEDS +from .tidal_database import TidalDB + + +class TidalDBAdcircEnum(Enum): + """Enum for specifying the type of an ADCIRC database. + + North West Atlantic and North East Pacific are the only valid options. + + Attributes: + TIDE_NWAT: North West Atlantic database + TIDE_NEPAC: North East Pacific database + TIDE_NONE: Enum end - legacy from SMS port. + + """ + TIDE_NWAT = 0 # North West Atlantic Tidal Database + TIDE_NEPAC = 1 # North East Pacific Tidal Database + TIDE_NONE = 2 # Used if data not within any ADCIRC database domain + + +class AdcircDB(TidalDB): + """The class for extracting tidal data, specifically amplitude and phases, from an ADCIRC database. + + Attributes: + work_path (str): The path of the working directory. + exe_with_path (str): The path of the ADCIRC executable. + db_region (:obj: `TidalDBAdcircEnum`): The type of database. + cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the ADCIRC database + grid_no_path (str): Filename of *.grd file to use. + harm_no_path (str): Filename of *.tdb file to use. + temp_folder (str): Temporary folder to hold files in while running executables. + tide_in (str): Temporary 'tides.in' filename with path. + tide_out (str): Temporary 'tides.out' filename with path. + + """ + def __init__(self, work_path, exe_with_path, db_region): + """Get the amplitude and phase for the given constituents at the given points. + + Args: + work_path (str): The path of the working directory. + exe_with_path (str): The path of the ADCIRC executable. + db_region (:obj: `TidalDBAdcircEnum`): The db_region of database. + + """ + TidalDB.__init__(self) + self.work_path = work_path + self.exe_with_path = exe_with_path + self.db_region = db_region + self.cons = ['M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'Q1', 'K2'] + if self.db_region == TidalDBAdcircEnum.TIDE_NWAT: + self.grid_no_path = 'ec2001.grd' + self.harm_no_path = 'ec2001.tdb' + elif self.db_region == TidalDBAdcircEnum.TIDE_NEPAC: + self.grid_no_path = 'enpac2003.grd' + self.harm_no_path = 'enpac2003.tdb' + else: + self.grid_no_path = '' + self.harm_no_path = '' + src_list = list(string.ascii_uppercase + string.digits) + rand_str = random.choice(src_list) + self.temp_folder = os.path.join(self.work_path, '_'.join(rand_str)) + # check that the folder does not exist + while os.path.isdir(self.temp_folder): + rand_str = random.choice(src_list) + self.temp_folder = self.temp_folder + rand_str + self.tide_in = os.path.join(self.temp_folder, 'tides.in') + self.tide_out = os.path.join(self.temp_folder, 'tides.out') + self.validate_files() + + def validate_files(self): + """Check to ensure that the ADCIRC database exists. + + If the database is nonexistent, a new ADCIRC database will be downloaded from Aquaveo.com. Whether the Atlantic + or Pacific database is download depends on how the object was constructed. + + """ + if not os.path.isfile(self.exe_with_path): # Make sure the executable exists + # Download from the Aquaveo website + if self.db_region == TidalDBAdcircEnum.TIDE_NWAT: # Download the ADCIRC Atlantic database + adcirc_db_url = 'http://sms.aquaveo.com/adcircnwattides.zip' + basename = 'adcircnwattides' + else: # Download the ADCIRC Pacific database + adcirc_db_url = 'http://sms.aquaveo.com/adcircnepactides.zip' + basename = 'adcircnepactides' + zip_file = os.path.join(self.work_path, basename + '.zip') + with urllib.request.urlopen(adcirc_db_url) as response, open(zip_file, 'wb') as out_file: + shutil.copyfileobj(response, out_file) + # Unzip the files + dest_path = os.path.join(self.work_path, basename) + if not os.path.isdir(dest_path): + os.mkdir(dest_path) + with ZipFile(zip_file, 'r') as unzipper: + unzipper.extractall(path=dest_path) + os.remove(zip_file) # delete the zip file + self.exe_with_path = os.path.join(dest_path, basename + ".exe") + + def get_components(self, locs, cons=None, positive_ph=False): + """Get the amplitude, phase, and speed for the given constituents at the given points. + + Args: + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): List of the point locations to get + amplitude and phase for. e.g. [(x1, y1), (x2, y2)] + cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If + not supplied, all valid constituents will be extracted. + positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or + [-180 180] (False, the default). + + Returns: + :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including + amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, + where each element in the return list is the constituent data for the corresponding element in locs. + + """ + # pre-allocate the return value + constituents = [] + if not cons: + cons = self.cons # Get all constituents by default + + con_data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + for con in cons: + if self.have_constituent(con): + con = con.lower() + constituents.append(con) + + # create the temp directory + os.mkdir(self.temp_folder) + os.chdir(self.temp_folder) + try: + # write tides.in into the temp directory + with open(self.tide_in, 'w') as f: + f.write("{}\n".format(str(len(locs)))) + for pt in locs: + # 15.10f + f.write("{:15.10f}{:15.10f}\n".format(pt[0], pt[1])) + # copy the executable and .grd and .tdb file to the temp folder + temp_exe_with_path = os.path.join(self.temp_folder, os.path.basename(self.exe_with_path)) + temp_grd_with_path = os.path.join(self.temp_folder, self.grid_no_path) + temp_tdb_with_path = os.path.join(self.temp_folder, self.harm_no_path) + + old_exe_path = os.path.dirname(self.exe_with_path) + old_grd_with_path = os.path.join(old_exe_path, self.grid_no_path) + old_tdb_with_path = os.path.join(old_exe_path, self.harm_no_path) + + shutil.copyfile(self.exe_with_path, temp_exe_with_path) + shutil.copyfile(old_grd_with_path, temp_grd_with_path) + shutil.copyfile(old_tdb_with_path, temp_tdb_with_path) + # run the executable + subprocess.run([temp_exe_with_path]) + # read tides.out from the temp directory + with open(self.tide_out, 'r') as f: + all_lines = f.readlines() + last_name_line = 0 + is_first = True + used_constituent = False + con_name = '' + column_count = 0 + curr_pt = 0 + for count, line in enumerate(all_lines): + line = line.strip() + if not line: + continue + elif any(not(c.isdigit() or c.isspace() or c == 'e' or c == 'E' + or c == '.' or c == '-' or c == '+') for c in line): + last_name_line = count + is_first = True + curr_pt = 0 + continue + elif is_first: + con_name = all_lines[last_name_line].strip().lower() + is_first = False + if any(con_name in name for name in constituents): + used_constituent = True + else: + used_constituent = False + + if used_constituent: + if column_count == 0: + line_nums = [float(num) for num in line.split()] + column_count = len(line_nums) + if column_count == 2: + amp, pha = map(float, line.split()) + elif column_count == 4: + junk, junk, amp, pha = map(float, line.split()) + elif column_count == 6: + amp, pha, junk, junk, junk, junk = map(float, line.split()) + elif column_count == 8: + junk, junk, amp, pha, junk, junk, junk, junk = map(float, line.split()) + else: + # we have a problem + continue + con_data[curr_pt].loc[con_name.upper()] = [amp, + pha + (360.0 if positive_ph and pha < 0 else 0), + NOAA_SPEEDS[con_name.upper()]] + curr_pt += 1 + finally: + # delete the temp directory + del_files = os.listdir(self.temp_folder) + for del_file in del_files: + os.remove(del_file) + os.chdir(self.work_path) + os.rmdir(self.temp_folder) + + return con_data + + def have_constituent(self, a_name): + """Check if teh given constituent is supported by the ADCIRC tidal database. + + Args: + a_name (str): The name of the constituent to check. + + Returns: + True if the constituent is supported by ADCIRC tidal databases, False for unsupported. + + """ + if a_name.upper() in self.cons: + return True + else: + return False diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py new file mode 100644 index 0000000..91a2ed9 --- /dev/null +++ b/harmonica/leprovost_database.py @@ -0,0 +1,230 @@ +"""LeProvost tidal database extractor + +This module contains the tidal database extractor for the LeProvost tidal database. + +""" + +import math +import os +import shutil +import urllib.request +from zipfile import ZipFile + +import pandas as pd + +from .tidal_constituents import NOAA_SPEEDS +from .tidal_database import TidalDB + + +class LeProvostDB(TidalDB): + """Extractor class for the LeProvost tidal database. + + Attributes: + leprovost_path (str): Fully qualified path to a folder containing the LeProvost *.legi files + cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the LeProvost database + + """ + def __init__(self, leprovost_path): + """Constructor for database extractor + + Args: + leprovost_path (str): Fully qualified path to a folder containing the LeProvost *.legi files + + """ + TidalDB.__init__(self) + self.cons = ['M2', 'S2', 'N2', 'K1', 'O1', 'NU2', 'MU2', '2N2', 'Q1', 'T2', 'P1', 'L2', 'K2'] + self.leprovost_path = leprovost_path + self.validate_files() + + def validate_files(self): + """Check to ensure that the LeProvost folder exists and contains a complete set of files. + + If the database is incomplete or nonexistent, a new LeProvost database will be downloaded from Aquaveo.com. + + """ + if not os.path.isdir(self.leprovost_path): # Make sure the directory exists + os.mkdir(self.leprovost_path) + # Make sure all the database files exist in the directory + legi_files = ['M2.legi', 'S2.legi', 'N2.legi', 'K1.legi', 'O1.legi', 'NU2.legi', 'MU2.legi', '2N2.legi', + 'Q1.legi', 'T2.legi', 'P1.legi', 'L2.legi', 'K2.legi'] + if not all([os.path.isfile(os.path.join(self.leprovost_path, f)) for f in legi_files]): + # Download from the Aquaveo website + leprovost_url = 'http://sms.aquaveo.com/ADCIRC_Essentials.zip' + zip_file = os.path.join(self.leprovost_path, "ADCIRC_Essentials.zip") + with urllib.request.urlopen(leprovost_url) as response, open(zip_file, 'wb') as out_file: + shutil.copyfileobj(response, out_file) + # Unzip the files + with ZipFile(zip_file, 'r') as unzipper: + for file in unzipper.namelist(): + if file.startswith('LeProvost/'): + unzipper.extract(file, self.leprovost_path) + os.remove(zip_file) # delete the zip file + self.leprovost_path = os.path.join(self.leprovost_path, "LeProvost") + + def get_components(self, locs, cons=None, positive_ph=False): + """Get the amplitude, phase, and speed of specified constituents at specified point locations. + + Args: + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): List of the point locations to get + amplitude and phase for. e.g. [(x1, y1), (x2, y2)] + cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If + not supplied, all valid constituents will be extracted. + positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or + [-180 180] (False, the default). + + Returns: + :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including + amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, + where each element in the return list is the constituent data for the corresponding element in locs. + Empty list on error. + + """ + # If no constituents specified, extract all valid constituents. + if not cons: + cons = self.cons + # read the file for each constituent + con_data = [] + for pt in locs: + y_lat = pt[1] + x_lon = pt[0] + if x_lon < 0.0: + x_lon = x_lon + 360.0 + if x_lon > 180.0: + x_lon = x_lon - 360.0 + if x_lon > 180.0 or x_lon < -180.0 or y_lat > 90.0 or y_lat < -90.0: + # ERROR: Not in latitude/longitude + return con_data + + con_data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + + deg2rad = 1.0 / 180.0 * math.pi + rad2deg = 1.0 / deg2rad + for con in cons: + if self.have_constituent(con): + con = con.lower() + else: + continue + filename = os.path.join(self.leprovost_path, con + '.legi') + with open(filename, 'r') as f: + # 30 columns in the file + lon_min, lon_max = map(float, f.readline().split()) + lat_min, lat_max = map(float, f.readline().split()) + d_lon, d_lat = map(float, f.readline().split()) + n_lon, n_lat = map(int, f.readline().split()) + undef_a, undef_p = map(float, f.readline().split()) + if undef_a != undef_p: + # there was an error, the undefined values should be the same + return [] + else: + undef = undef_a + all_lines = f.readlines() + g_amp = [[0.0 for _ in range(n_lat)] for _ in range(n_lon)] + g_pha = [[0.0 for _ in range(n_lat)] for _ in range(n_lon)] + cur_line = -1 + for lat in range(n_lat): + for lon in range(0, n_lon-1, 30): + cur_line += 1 + cur_val = 0 + line_vals = all_lines[cur_line].split() + for k in range(lon, lon+30): + g_amp[k][lat] = float(line_vals[cur_val]) + cur_val += 1 + cur_line += 1 + cur_val = 0 + line_vals = all_lines[cur_line].split() + for k in range(lon, lon+30): + g_pha[k][lat] = float(line_vals[cur_val]) + cur_val += 1 + + for i, pt in enumerate(locs): + y_lat = pt[1] + x_lon = pt[0] + if x_lon < 0.0: + x_lon = x_lon + 360.0 + if x_lon > 180.0: + x_lon = x_lon - 360.0 + ixlo = int((x_lon - lon_min) / d_lon) + 1 + xlonlo = lon_min + (ixlo - 1) * d_lon + ixhi = ixlo + 1 + if ixlo == n_lon: + ixhi = 1 + iylo = int((y_lat - lat_min) / d_lon) + 1 + ylatlo = lat_min + (iylo - 1) * d_lat + iyhi = iylo + 1 + ixlo -= 1 + ixhi -= 1 + iylo -= 1 + iyhi -= 1 + skip = False + + if (ixlo > n_lon or ixhi > n_lon or iyhi > n_lat or iylo > n_lat or ixlo < 0 or ixhi < 0 or iyhi < 0 or + iylo < 0): + skip = True + elif (g_amp[ixlo][iyhi] == undef and g_amp[ixhi][iyhi] == undef and g_amp[ixlo][iylo] == undef and + g_amp[ixhi][iylo] == undef): + skip = True + elif (g_pha[ixlo][iyhi] == undef and g_pha[ixhi][iyhi] == undef and g_pha[ixlo][iylo] == undef and + g_pha[ixhi][iylo] == undef): + skip = True + + if skip: + con_data[i].loc[con.upper()] = [0.0, 0.0, 0.0] + else: + xratio = (x_lon - xlonlo) / d_lon + yratio = (y_lat - ylatlo) / d_lat + xcos1 = g_amp[ixlo][iyhi] * math.cos(deg2rad * g_pha[ixlo][iyhi]) + xcos2 = g_amp[ixhi][iyhi] * math.cos(deg2rad * g_pha[ixhi][iyhi]) + xcos3 = g_amp[ixlo][iylo] * math.cos(deg2rad * g_pha[ixlo][iylo]) + xcos4 = g_amp[ixhi][iylo] * math.cos(deg2rad * g_pha[ixhi][iylo]) + xsin1 = g_amp[ixlo][iyhi] * math.sin(deg2rad * g_pha[ixlo][iyhi]) + xsin2 = g_amp[ixhi][iyhi] * math.sin(deg2rad * g_pha[ixhi][iyhi]) + xsin3 = g_amp[ixlo][iylo] * math.sin(deg2rad * g_pha[ixlo][iylo]) + xsin4 = g_amp[ixhi][iylo] * math.sin(deg2rad * g_pha[ixhi][iylo]) + + xcos = 0.0 + xsin = 0.0 + denom = 0.0 + if g_amp[ixlo][iyhi] != undef and g_pha[ixlo][iyhi] != undef: + xcos = xcos + xcos1 * (1.0 - xratio) * yratio + xsin = xsin + xsin1 * (1.0 - xratio) * yratio + denom = denom + (1.0 - xratio) * yratio + if g_amp[ixhi][iyhi] != undef and g_pha[ixhi][iyhi] != undef: + xcos = xcos + xcos2 * xratio * yratio + xsin = xsin + xsin2 * xratio * yratio + denom = denom + xratio * yratio + if g_amp[ixlo][iylo] != undef and g_pha[ixlo][iylo] != undef: + xcos = xcos + xcos3 * (1.0 - xratio) * (1 - yratio) + xsin = xsin + xsin3 * (1.0 - xratio) * (1 - yratio) + denom = denom + (1.0 - xratio) * (1.0 - yratio) + if g_amp[ixhi][iylo] != undef and g_pha[ixhi][iylo] != undef: + xcos = xcos + xcos4 * (1.0 - yratio) * xratio + xsin = xsin + xsin4 * (1.0 - yratio) * xratio + denom = denom + (1.0 - yratio) * xratio + + xcos = xcos / denom + xsin = xsin / denom + + amp = math.sqrt(xcos * xcos + xsin * xsin) + phase = rad2deg * math.acos(xcos / amp) + amp /= 100.0 + if xsin < 0.0: + phase = 360.0 - phase + phase += (360. if positive_ph and phase < 0 else 0) + con_data[i].loc[con.upper()] = [amp, phase, NOAA_SPEEDS[con.upper()]] + + return con_data + + def have_constituent(self, a_name): + """Checks if a constituent name is valid for the LeProvost tidal database + + Args: + a_name (str): Name of the constituent to check. + + Returns: + True if the constituent is valid for the LeProvost tidal database, False otherwise. + + """ + if a_name.upper() in self.cons: + return True + else: + return False diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index f052e29..81a8e37 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -6,53 +6,54 @@ import sys +# Dictionary of NOAA constituent speed constants (deg/hr) +# Source: https://tidesandcurrents.noaa.gov +# The speed is the rate change in the phase of a constituent, and is equal to 360 degrees divided by the +# constituent period expressed in hours +NOAA_SPEEDS = { + 'OO1': 16.139101, + '2Q1': 12.854286, + '2MK3': 42.92714, + '2N2': 27.895355, + '2SM2': 31.015896, + 'K1': 15.041069, + 'K2': 30.082138, + 'J1': 15.5854435, + 'L2': 29.528479, + 'LAM2': 29.455626, + 'M1': 14.496694, + 'M2': 28.984104, + 'M3': 43.47616, + 'M4': 57.96821, + 'M6': 86.95232, + 'M8': 115.93642, + 'MF': 1.0980331, + 'MK3': 44.025173, + 'MM': 0.5443747, + 'MN4': 57.423832, + 'MS4': 58.984104, + 'MSF': 1.0158958, + 'MU2': 27.968208, + 'N2': 28.43973, + 'NU2': 28.512583, + 'O1': 13.943035, + 'P1': 14.958931, + 'Q1': 13.398661, + 'R2': 30.041067, + 'RHO': 13.471515, + 'S1': 15.0, + 'S2': 30.0, + 'S4': 60.0, + 'S6': 90.0, + 'SA': 0.0410686, + 'SSA': 0.0821373, + 'T2': 29.958933, +} + + class Constituents: """Harmonica tidal constituents.""" - # Dictionary of NOAA constituent speed constants (deg/hr) - # Source: https://tidesandcurrents.noaa.gov - # The speed is the rate change in the phase of a constituent, and is equal to 360 degrees divided by the - # constituent period expressed in hours - NOAA_SPEEDS = { - 'OO1': 16.139101, - '2Q1': 12.854286, - '2MK3': 42.92714, - '2N2': 27.895355, - '2SM2': 31.015896, - 'K1': 15.041069, - 'K2': 30.082138, - 'J1': 15.5854435, - 'L2': 29.528479, - 'LAM2': 29.455626, - 'M1': 14.496694, - 'M2': 28.984104, - 'M3': 43.47616, - 'M4': 57.96821, - 'M6': 86.95232, - 'M8': 115.93642, - 'MF': 1.0980331, - 'MK3': 44.025173, - 'MM': 0.5443747, - 'MN4': 57.423832, - 'MS4': 58.984104, - 'MSF': 1.0158958, - 'MU2': 27.968208, - 'N2': 28.43973, - 'NU2': 28.512583, - 'O1': 13.943035, - 'P1': 14.958931, - 'Q1': 13.398661, - 'R2': 30.041067, - 'RHO': 13.471515, - 'S1': 15.0, - 'S2': 30.0, - 'S4': 60.0, - 'S6': 90.0, - 'SA': 0.0410686, - 'SSA': 0.0821373, - 'T2': 29.958933, - } - def __init__(self): # constituent information dataframe: # amplitude (meters) @@ -135,7 +136,7 @@ def get_components(self, loc, model=ResourceManager.DEFAULT_RESOURCE, cons=[], p # phase ph + (360. if positive_ph and ph < 0 else 0), # speed - self.NOAA_SPEEDS[c] + NOAA_SPEEDS[c] ] return self \ No newline at end of file diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py new file mode 100644 index 0000000..2dc3fdf --- /dev/null +++ b/harmonica/tidal_database.py @@ -0,0 +1,415 @@ +#! python3 + +from enum import Enum +from abc import ABCMeta, abstractmethod +import math + +import pandas as pd + +class TidalDBEnum(Enum): + TIDAL_DB_LEPROVOST = 0 + TIDAL_DB_ADCIRC = 1 + + +NCNST = 37 + + +class OrbitVariables(object): + def __init__(self): + self.dh = 0.0 + self.di = 0.0 + self.dn = 0.0 + self.dnu = 0.0 + self.dnup = 0.0 + self.dnup2 = 0.0 + self.dp = 0.0 + self.dp1 = 0.0 + self.dpc = 0.0 + self.ds = 0.0 + self.dxi = 0.0 + self.grterm = NCNST*[0.0] + self.hour = 0.0 + self.nodfac = NCNST*[0.0] + self.day = 0 + self.month = 0 + self.year = 0 + + +class TidalDB(object): + """The base class for extracting tidal data from a database. + + Attributes: + attr2 (:obj:`list` of :obj:`float`): The starting days of the months (non-leap year). + pi180 (float): PI divided by 180.0. + orbit (:obj:`OrbitVariables`): The orbit variables. + + """ + day_t = [0.0, 31.0, 59.0, 90.0, 120.0, 151.0, 181.0, 212.0, 243.0, 273.0, 304.0, 334.0] + pi180 = math.pi/180.0 + + def __init__(self): + self.orbit = OrbitVariables() + + __metaclass__ = ABCMeta + + @abstractmethod + def get_components(self, a_constituents, a_points): + pass + + @abstractmethod + def have_constituent(self, a_name): + pass + + def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): + """Get the nodal factor for specified constituents at a specified time. + + Args: + a_names (:obj:`list` of :obj:`str`): Names of the constituents to get nodal factors for + a_hour (float): The hour of the specified time. Can be fractional + a_day (int): The day of the specified time. + a_month (int): The month of the specified time. + a_year (int): The year of the specified time. + + Returns: + :obj:`pandas.DataFrame`: Constituent data frames. Each row contains frequency, earth tidal reduction factor, + amplitude, nodal factor, and equilibrium argument for one of the specified constituents. Rows labeled by + constituent name. + + """ + con_names = ['M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'MK3', 'S4', 'MN4', 'NU2', 'S6', 'MU2', '2N2', 'OO1', + 'LAMBDA2', 'S1', 'M1', 'J1', 'MM', 'SSA', 'SA', 'MSF', 'MF', 'RHO1', 'Q1', 'T2', 'R2', '2Q1', + 'P1', '2SM2', 'M3', 'L2', '2MK3', 'K2', 'M8', 'MS4'] + con_freqs = [0.000140518902509, 0.000145444104333, 0.000137879699487, 0.000072921158358, 0.000281037805017, + 0.000067597744151, 0.000421556708011, 0.000213440061351, 0.000290888208666, 0.000278398601995, + 0.000138232903707, 0.000436332312999, 0.000135593700684, 0.000135240496464, 0.000078244573050, + 0.000142804901311, 0.000072722052166, 0.000070281955336, 0.000075560361380, 0.000002639203022, + 0.000000398212868, 0.000000199106191, 0.000004925201824, 0.000005323414692, 0.000065311745349, + 0.000064958541129, 0.000145245007353, 0.000145643201313, 0.000062319338107, 0.000072522945975, + 0.000150369306157, 0.000210778353763, 0.000143158105531, 0.000208116646659, 0.000145842317201, + 0.000562075610519, 0.000285963006842] + con_etrf = NCNST*[0.0690] + con_etrf[3] = 0.736 # clK1 + con_etrf[5] = 0.695 # O1 + con_etrf[29] = 0.706 # P1 + con_etrf[25] = 0.695 # Q1 + con_etrf[0] = 0.693 # M2 + con_etrf[2] = 0.693 # N2 + con_etrf[1] = 0.693 # S2 + con_etrf[34] = 0.693 # K2 + con_amp = NCNST*[0.0] + con_amp[3] = 0.141565 # K1 + con_amp[5] = 0.100514 # O1 + con_amp[29] = 0.046834 # P1 + con_amp[25] = 0.019256 # Q1 + con_amp[0] = 0.242334 # M2 + con_amp[2] = 0.046398 # N2 + con_amp[1] = 0.112841 # S2 + con_amp[34] = 0.030704 # K2 + + con_data = pd.DataFrame(columns=["amplitude", "frequency", "earth_tide_reduction_factor", + "equilibrium_argument", "nodal_factor"]) + self.get_eq_args(a_hour, a_day, a_month, a_year) + for idx, name in enumerate(a_names): + name = name.upper() + try: + name_idx = con_names.index(name) + except ValueError: + continue + equilibrium_arg = 0.0 + nodal_factor = 0.0 + if self.orbit.nodfac[name_idx] != 0.0: + nodal_factor = self.orbit.nodfac[name_idx] + equilibrium_arg = self.orbit.grterm[name_idx] + con_data.loc[name] = [con_amp[name_idx], con_freqs[name_idx], con_etrf[name_idx], equilibrium_arg, + nodal_factor] + return con_data + + def get_eq_args(self, a_hour, a_day, a_month, a_year): + """Get equilibrium arguments at a starting time. + + Args: + a_hour (float): The starting hour. + a_day (int): The starting day. + a_month (int): The starting month. + a_year (int): The starting year. + + """ + day_julian = self.get_day_julian(a_day, a_month, a_year) + hrm = a_hour / 2.0 + self.nfacs(a_year, day_julian, hrm) + self.gterms(a_year, day_julian, a_hour, hrm) + + def angle(self, a_number): + """Converts the angle to be within 0-360. + + Args: + a_number (float): The angle to convert. + + Returns: + The angle converted to be within 0-360. + + """ + ret_val = a_number + while ret_val < 0.0: + ret_val += 360.0 + while ret_val > 360.0: + ret_val -= 360.0 + return ret_val + + def get_day_julian(self, a_day, a_month, a_year): + """Get a float representing the Julian date. + + Args: + a_day (float): The day. + a_month (float): The month. + a_year (float): The year. + + Returns: + The Julian date with epoch of January 1, 1900. + + """ + days = 12*[0.0] + days[1] = 31.0 + year_offset = int(a_year-1900.0) + yrlp = float(abs(year_offset) % 4) + if year_offset < 0.0: + yrlp *= -1.0 + d_inc = 0.0 + if yrlp == 0.0: + d_inc = 1.0 + for i in range(2, 12): + days[i] = self.day_t[i] + d_inc + return days[int(a_month)-1]+a_day + + def set_orbit(self, a_year, a_day_julian, a_hour): + """Determination of primary and secondary orbital functions. + + Args: + a_year (float): The year. + a_day_julian (float): The day in julian format. + a_hour (float): The hour.. + + """ + x = int((a_year-1901.0)/4.0) + dyr = a_year-1900.0 + dday = a_day_julian+x-1.0 + + # dn IS THE MOON'S NODE (CAPITAL N, TABLE 1, SCHUREMAN) + self.orbit.dn = 259.1560564-19.328185764*dyr-0.0529539336*dday-0.0022064139*a_hour + self.orbit.dn = self.angle(self.orbit.dn) + n = self.orbit.dn*self.pi180 + + # dp IS THE LUNAR PERIGEE (SMALL P, TABLE 1) + self.orbit.dp = 334.3837214+40.66246584*dyr+0.111404016*dday+0.004641834*a_hour + self.orbit.dp = self.angle(self.orbit.dp) + # p = self.orbit.dp*self.pi180 + fi = math.acos(0.9136949-0.0356926*math.cos(n)) + self.orbit.di = self.angle(fi/self.pi180) + + nu = math.asin(0.0897056*math.sin(n)/math.sin(fi)) + self.orbit.dnu = nu/self.pi180 + xi = n-2.0*math.atan(0.64412*math.tan(n/2.0))-nu + self.orbit.dxi = xi/self.pi180 + self.orbit.dpc = self.angle(self.orbit.dp-self.orbit.dxi) + + # dh IS THE MEAN LONGITUDE OF THE SUN (SMALL H, TABLE 1) + self.orbit.dh = 280.1895014-0.238724988*dyr+0.9856473288*dday+0.0410686387*a_hour + self.orbit.dh = self.angle(self.orbit.dh) + + # dp1 IS THE SOLAR PERIGEE (SMALL P1, TABLE 1) + self.orbit.dp1 = 281.2208569+0.01717836*dyr+0.000047064*dday+0.000001961*a_hour + self.orbit.dp1 = self.angle(self.orbit.dp1) + + # ds IS THE MEAN LONGITUDE OF THE MOON (SMALL S, TABLE 1) + self.orbit.ds = 277.0256206+129.38482032*dyr+13.176396768*dday+0.549016532*a_hour + self.orbit.ds = self.angle(self.orbit.ds) + nup = math.atan(math.sin(nu)/(math.cos(nu)+0.334766/math.sin(2.0*fi))) + self.orbit.dnup = nup/self.pi180 + nup2 = math.atan(math.sin(2.0*nu)/(math.cos(2.0*nu)+0.0726184/pow(math.sin(fi), 2.0)))/2.0 + self.orbit.dnup2 = nup2/self.pi180 + + def nfacs(self, a_year, a_day_julian, a_hour): + """Calculates node factors for constituent tidal signal. + + Args: + a_year (float): The year. + a_day_julian (float): The day in julian format. + a_hour (float): The hour. + + Returns: + The same values as found in table 14 of schureman. + + """ + self.set_orbit(a_year, a_day_julian, a_hour) + # n = self.orbit.dn*self.pi180 + fi = self.orbit.di*self.pi180 + nu = self.orbit.dnu*self.pi180 + # xi = self.orbit.dxi*self.pi180 + # p = self.orbit.dp*self.pi180 + # pc = self.orbit.dpc*self.pi180 + sini = math.sin(fi) + sini2 = math.sin(fi/2.0) + sin2i = math.sin(2.0*fi) + cosi2 = math.cos(fi/2.0) + # tani2 = math.tan(fi/2.0) + # EQUATION 197, SCHUREMAN + # qainv = math.sqrt(2.310+1.435*math.cos(2.0*pc)) + # EQUATION 213, SCHUREMAN + # rainv = math.sqrt(1.0-12.0*math.pow(tani2, 2)*math.cos(2.0*pc)+36.0*math.pow(tani2, 4)) + # VARIABLE NAMES REFER TO EQUATION NUMBERS IN SCHUREMAN + eq73 = (2.0/3.0-math.pow(sini, 2))/0.5021 + eq74 = math.pow(sini, 2.0)/0.1578 + eq75 = sini*math.pow(cosi2, 2.0)/0.37988 + eq76 = math.sin(2*fi)/0.7214 + eq77 = sini*math.pow(sini2, 2.0)/0.0164 + eq78 = math.pow(cosi2, 4.0)/0.91544 + eq149 = math.pow(cosi2, 6.0)/0.8758 + # eq207 = eq75*qainv + # eq215 = eq78*rainv + eq227 = math.sqrt(0.8965*math.pow(sin2i, 2.0)+0.6001*sin2i*math.cos(nu)+0.1006) + eq235 = 0.001+math.sqrt(19.0444*math.pow(sini, 4.0)+2.7702*pow(sini, 2.0)*math.cos(2.0*nu)+0.0981) + # NODE FACTORS FOR 37 CONSTITUENTS: + self.orbit.nodfac[0] = eq78 + self.orbit.nodfac[1] = 1.0 + self.orbit.nodfac[2] = eq78 + self.orbit.nodfac[3] = eq227 + self.orbit.nodfac[4] = math.pow(self.orbit.nodfac[0], 2.0) + self.orbit.nodfac[5] = eq75 + self.orbit.nodfac[6] = math.pow(self.orbit.nodfac[0], 3.0) + self.orbit.nodfac[7] = self.orbit.nodfac[0]*self.orbit.nodfac[3] + self.orbit.nodfac[8] = 1.0 + self.orbit.nodfac[9] = math.pow(self.orbit.nodfac[0], 2.0) + self.orbit.nodfac[10] = eq78 + self.orbit.nodfac[11] = 1.0 + self.orbit.nodfac[12] = eq78 + self.orbit.nodfac[13] = eq78 + self.orbit.nodfac[14] = eq77 + self.orbit.nodfac[15] = eq78 + self.orbit.nodfac[16] = 1.0 + # EQUATION 207 NOT PRODUCING CORRECT ANSWER FOR M1 + # SET NODE FACTOR FOR M1 = 0 UNTIL CAN FURTHER RESEARCH + self.orbit.nodfac[17] = 0.0 + self.orbit.nodfac[18] = eq76 + self.orbit.nodfac[19] = eq73 + self.orbit.nodfac[20] = 1.0 + self.orbit.nodfac[21] = 1.0 + self.orbit.nodfac[22] = eq78 + self.orbit.nodfac[23] = eq74 + self.orbit.nodfac[24] = eq75 + self.orbit.nodfac[25] = eq75 + self.orbit.nodfac[26] = 1.0 + self.orbit.nodfac[27] = 1.0 + self.orbit.nodfac[28] = eq75 + self.orbit.nodfac[29] = 1.0 + self.orbit.nodfac[30] = eq78 + self.orbit.nodfac[31] = eq149 + # EQUATION 215 NOT PRODUCING CORRECT ANSWER FOR L2 + # SET NODE FACTOR FOR L2 = 0 UNTIL CAN FURTHER RESEARCH + self.orbit.nodfac[32] = 0.0 + self.orbit.nodfac[33] = math.pow(self.orbit.nodfac[0], 2.0)*self.orbit.nodfac[3] + self.orbit.nodfac[34] = eq235 + self.orbit.nodfac[35] = math.pow(self.orbit.nodfac[0], 4.0) + self.orbit.nodfac[36] = eq78 + + def gterms(self, a_year, a_day_julian, a_hour, a_hrm): + """Determines the Greenwich equilibrium terms. + + Args: + a_year (float): The year. + a_day_julian (float): The day in julian format. + a_hour (float): The hour for V0. + a_hrm (float): The hour for U. + + Returns: + The same values as found in table 15 of schureman. + + """ + # OBTAINING ORBITAL VALUES AT BEGINNING OF SERIES FOR V0 + self.set_orbit(a_year, a_day_julian, a_hour) + s = self.orbit.ds + p = self.orbit.dp + h = self.orbit.dh + p1 = self.orbit.dp1 + t = self.angle(180.0+a_hour*(360.0/24.0)) + + # OBTAINING ORBITAL VALUES AT MIDDLE OF SERIES FOR U + self.set_orbit(a_year, a_day_julian, a_hrm) + nu = self.orbit.dnu + xi = self.orbit.dxi + nup = self.orbit.dnup + nup2 = self.orbit.dnup2 + + # SUMMING TERMS TO OBTAIN EQUILIBRIUM ARGUMENTS + self.orbit.grterm[0] = 2.0*(t-s+h)+2.0*(xi-nu) + self.orbit.grterm[1] = 2.0*t + self.orbit.grterm[2] = 2.0*(t+h)-3.*s+p+2.0*(xi-nu) + self.orbit.grterm[3] = t+h-90.0-nup + self.orbit.grterm[4] = 4.0*(t-s+h)+4.0*(xi-nu) + self.orbit.grterm[5] = t-2.0*s+h+90.0+2.0*xi-nu + self.orbit.grterm[6] = 6.0*(t-s+h)+6.0*(xi-nu) + self.orbit.grterm[7] = 3.0*(t+h)-2.0*s-90.0+2.0*(xi-nu)-nup + self.orbit.grterm[8] = 4.0*t + self.orbit.grterm[9] = 4.0*(t+h)-5.0*s+p+4.0*(xi-nu) + self.orbit.grterm[10] = 2.0*t-3.0*s+4.0*h-p+2.0*(xi-nu) + self.orbit.grterm[11] = 6.0*t + self.orbit.grterm[12] = 2.0*(t+2.0*(h-s))+2.0*(xi-nu) + self.orbit.grterm[13] = 2.0*(t-2.0*s+h+p)+2.0*(xi-nu) + self.orbit.grterm[14] = t+2.0*s+h-90.0-2.0*xi-nu + self.orbit.grterm[15] = 2.0*t-s+p+180.0+2.0*(xi-nu) + self.orbit.grterm[16] = t + fi = self.orbit.di*self.pi180 + pc = self.orbit.dpc*self.pi180 + top = (5.0*math.cos(fi)-1.0)*math.sin(pc) + bottom = (7.0*math.cos(fi)+1.0)*math.cos(pc) + q = self.arctan(top, bottom) + self.orbit.grterm[17] = t-s+h-90.0+xi-nu+q + self.orbit.grterm[18] = t+s+h-p-90.0-nu + self.orbit.grterm[19] = s-p + self.orbit.grterm[20] = 2.0*h + self.orbit.grterm[21] = h + self.orbit.grterm[22] = 2.0*(s-h) + self.orbit.grterm[23] = 2.0*s-2.0*xi + self.orbit.grterm[24] = t+3.0*(h-s)-p+90.0+2.0*xi-nu + self.orbit.grterm[25] = t-3.0*s+h+p+90.0+2.0*xi-nu + self.orbit.grterm[26] = 2.0*t-h+p1 + self.orbit.grterm[27] = 2.0*t+h-p1+180.0 + self.orbit.grterm[28] = t-4.0*s+h+2.0*p+90.0+2.0*xi-nu + self.orbit.grterm[29] = t-h+90.0 + self.orbit.grterm[30] = 2.0*(t+s-h)+2.0*(nu-xi) + self.orbit.grterm[31] = 3.0*(t-s+h)+3.0*(xi-nu) + r = math.sin(2.0*pc)/((1.0/6.0)*math.pow((1.0/math.tan(0.5*fi)), 2)-math.cos(2.0*pc)) + r = math.atan(r)/self.pi180 + self.orbit.grterm[32] = 2.0*(t+h)-s-p+180.0+2.0*(xi-nu)-r + self.orbit.grterm[33] = 3.0*(t+h)-4.0*s+90.0+4.0*(xi-nu)+nup + self.orbit.grterm[34] = 2.0*(t+h)-2.0*nup2 + self.orbit.grterm[35] = 8.0*(t-s+h)+8.0*(xi-nu) + self.orbit.grterm[36] = 2.0*(2.0*t-s+h)+2.0*(xi-nu) + for ih in range(0, 37): + self.orbit.grterm[ih] = self.angle(self.orbit.grterm[ih]) + + def arctan(self, a_top, a_bottom): + """Determine the arctangent and place in correct quadrant. + + Args: + TODO Find out what these are + a_top: The first parameter. + a_bottom: The second parameter. + + Returns: + The arc tangent in degrees in the correct quadrant. + + """ + if a_bottom == 0.0: + if a_top < 0.0: + value = 270.0 + elif a_top == 0.0: + value = 0.0 + else: + value = 90.0 + return value + value = math.atan(a_top/a_bottom)*57.2957795 + if a_bottom < 0.0: + value += 180.0 + else: + value += 360.0 + return value diff --git a/test.py b/test.py new file mode 100644 index 0000000..3df5d9b --- /dev/null +++ b/test.py @@ -0,0 +1,82 @@ +import argparse +import os + +import harmonica.adcirc_database +import harmonica.leprovost_database +#import TPXODatabase + +def write_nodal(f, nf): + for n in nf: + f.write(n.name + ", " + + str(n.amplitude) + ", " + + str(n.frequency) + ", " + + str(n.earth_tide_reduction_factor) + ", " + + str(n.equilibrium_argument) + ", " + + str(n.nodal_factor) + "\n") + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-w", "--work_dir", default=os.getcwd(), + help="directory to save output and temporary files") + parser.add_argument("-a", "--adcirc_atlantic", default="", + help="path to the ADCIRC Atlantic database") + parser.add_argument("-p", "--adcirc_pacific", default="", + help="path to the ADCIRC Pacific database") + parser.add_argument("-l", "--leprovost", + default=os.getcwd(), help="path to the LeProvost database folder") + args = vars(parser.parse_args()) + + work_dir = args["work_dir"] + adcirc_atlantic = args["adcirc_atlantic"] + adcir_pacific = args["adcirc_pacific"] + leprovost = args["leprovost"] + + atlantic = [(-74.07, 39.74), # Not valid with ADCIRC Pacific database + (-71.4, 42.32), + (-69.38, 45.44)] + pacific = [(-124.55, 43.63), # Not valid with ADCIRC Atlantic database + (-124.38, 46.18)] + all_points = [] # All locations valid with LeProvost + all_points.extend(atlantic) + all_points.extend(pacific) + + good_cons = ['M2', 'S2', 'N2', 'K1'] + # bad_con = 'ZZ7' + ad_alantic_db = harmonica.adcirc_database.AdcircDB(work_dir, adcirc_atlantic, + harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NWAT) + ad_pacific_db = harmonica.adcirc_database.AdcircDB(work_dir, adcir_pacific, + harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NEPAC) + le_db = harmonica.leprovost_database.LeProvostDB(leprovost) + # tp_db = TPXODatabase.TpxoDB('tpxo8') + + ad_al_nf = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + ad_pa_nf = ad_pacific_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + le_nf = le_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + # tp_nf = tp_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + + f = open(os.path.join(work_dir, "tidal_test.out"), "w") + f.write("ADCIRC Atlantic nodal factor:\n") + f.write(ad_al_nf.to_string() + "\n\n") + f.write("ADCIRC Pacific nodal factor:\n") + f.write(ad_pa_nf.to_string() + "\n\n") + f.write("LeProvost nodal factor:\n") + f.write(le_nf.to_string() + "\n\n") + f.flush() + + ap1 = ad_alantic_db.get_components(atlantic, good_cons) + ap2 = ad_pacific_db.get_components(pacific, good_cons) + ap3 = le_db.get_components(all_points, good_cons) + #ap4 = tp_db.get_amplitude_and_phase(good_cons, all_points) + + f.write("ADCIRC Atlantic components:\n") + for pt in ap1: + f.write(pt.to_string() + "\n\n") + f.write("ADCIRC Pacific components:\n") + for pt in ap2: + f.write(pt.to_string() + "\n\n") + f.write("LeProvost components:\n") + for pt in ap3: + f.write(pt.to_string() + "\n\n") + # f.write(str(ap4) + "\n\n") + + f.close() \ No newline at end of file From a648bb3c8a688df5838881ba96151720d3f9b42e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 19 Sep 2018 13:07:04 -0600 Subject: [PATCH 03/24] Fix TPX0 extraction in test case. Use (lat, lon) instead of (x, y) locations for LeProvost and ADCIRC. Changed resource fetching for ADCIRC and LeProvost to check current directory as well as specified. Changed LeProvost and ADCIRC to use fluent interface pattern with get_components to be consistent with TPX0 interface. --- harmonica/adcirc_database.py | 52 ++++++++++++++++------- harmonica/leprovost_database.py | 65 +++++++++++++++++------------ harmonica/tidal_database.py | 34 ++++++++++++++- test.py | 73 ++++++++++++++++++--------------- 4 files changed, 147 insertions(+), 77 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 8c7ef84..67fed6d 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -12,18 +12,15 @@ import pandas as pd from .tidal_constituents import NOAA_SPEEDS -from .tidal_database import TidalDB +from .tidal_database import convert_coords, TidalDB class TidalDBAdcircEnum(Enum): """Enum for specifying the type of an ADCIRC database. - North West Atlantic and North East Pacific are the only valid options. - - Attributes: - TIDE_NWAT: North West Atlantic database - TIDE_NEPAC: North East Pacific database - TIDE_NONE: Enum end - legacy from SMS port. + TIDE_NWAT = North West Atlantic database + TIDE_NEPAC = North East Pacific database + TIDE_NONE = Enum end - legacy from SMS port. """ TIDE_NWAT = 0 # North West Atlantic Tidal Database @@ -39,6 +36,8 @@ class AdcircDB(TidalDB): exe_with_path (str): The path of the ADCIRC executable. db_region (:obj: `TidalDBAdcircEnum`): The type of database. cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the ADCIRC database + data (:obj:`list` of :obj:`pandas.DataFrame`): List of the constituent component DataFrames with one + per point location requested from get_components(). Intended return value of get_components(). grid_no_path (str): Filename of *.grd file to use. harm_no_path (str): Filename of *.tdb file to use. temp_folder (str): Temporary folder to hold files in while running executables. @@ -60,6 +59,7 @@ def __init__(self, work_path, exe_with_path, db_region): self.exe_with_path = exe_with_path self.db_region = db_region self.cons = ['M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'Q1', 'K2'] + self.data = [] if self.db_region == TidalDBAdcircEnum.TIDE_NWAT: self.grid_no_path = 'ec2001.grd' self.harm_no_path = 'ec2001.tdb' @@ -87,23 +87,39 @@ def validate_files(self): or Pacific database is download depends on how the object was constructed. """ + if not os.path.isdir(self.work_path): + self.work_path = os.getcwd() + if not os.path.isfile(self.exe_with_path): # Make sure the executable exists # Download from the Aquaveo website if self.db_region == TidalDBAdcircEnum.TIDE_NWAT: # Download the ADCIRC Atlantic database adcirc_db_url = 'http://sms.aquaveo.com/adcircnwattides.zip' basename = 'adcircnwattides' + if not self.exe_with_path: # check the local directory + local_exe = os.path.join(os.getcwd(), "adcircnwattides/adcircnwattides.exe") + if os.path.isfile(local_exe): + self.exe_with_path = local_exe + return else: # Download the ADCIRC Pacific database adcirc_db_url = 'http://sms.aquaveo.com/adcircnepactides.zip' basename = 'adcircnepactides' + if not self.exe_with_path: # check the local directory + local_exe = os.path.join(os.getcwd(), "adcircnepactides/adcircnepactides.exe") + if os.path.isfile(local_exe): + self.exe_with_path = local_exe + return zip_file = os.path.join(self.work_path, basename + '.zip') + print("Downloading resource: {}".format(adcirc_db_url)) with urllib.request.urlopen(adcirc_db_url) as response, open(zip_file, 'wb') as out_file: shutil.copyfileobj(response, out_file) # Unzip the files dest_path = os.path.join(self.work_path, basename) if not os.path.isdir(dest_path): os.mkdir(dest_path) + print("Unzipping files to: {}".format(dest_path)) with ZipFile(zip_file, 'r') as unzipper: unzipper.extractall(path=dest_path) + print("Deleting zip file: {}".format(zip_file)) os.remove(zip_file) # delete the zip file self.exe_with_path = os.path.join(dest_path, basename + ".exe") @@ -111,8 +127,8 @@ def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed for the given constituents at the given points. Args: - locs (:obj:`list` of :obj:`tuple` of :obj:`float`): List of the point locations to get - amplitude and phase for. e.g. [(x1, y1), (x2, y2)] + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] + of the requested points. cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If not supplied, all valid constituents will be extracted. positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or @@ -122,6 +138,7 @@ def get_components(self, locs, cons=None, positive_ph=False): :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, where each element in the return list is the constituent data for the corresponding element in locs. + Note that function uses fluent interface pattern. """ # pre-allocate the return value @@ -129,7 +146,12 @@ def get_components(self, locs, cons=None, positive_ph=False): if not cons: cons = self.cons # Get all constituents by default - con_data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + # Make sure point locations are valid lat/lon + locs = convert_coords(locs) + if not locs: + return self # ERROR: Not in latitude/longitude + + self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] for con in cons: if self.have_constituent(con): con = con.lower() @@ -144,7 +166,7 @@ def get_components(self, locs, cons=None, positive_ph=False): f.write("{}\n".format(str(len(locs)))) for pt in locs: # 15.10f - f.write("{:15.10f}{:15.10f}\n".format(pt[0], pt[1])) + f.write("{:15.10f}{:15.10f}\n".format(pt[1], pt[0])) # copy the executable and .grd and .tdb file to the temp folder temp_exe_with_path = os.path.join(self.temp_folder, os.path.basename(self.exe_with_path)) temp_grd_with_path = os.path.join(self.temp_folder, self.grid_no_path) @@ -201,9 +223,9 @@ def get_components(self, locs, cons=None, positive_ph=False): else: # we have a problem continue - con_data[curr_pt].loc[con_name.upper()] = [amp, - pha + (360.0 if positive_ph and pha < 0 else 0), - NOAA_SPEEDS[con_name.upper()]] + self.data[curr_pt].loc[con_name.upper()] = [amp, + pha + (360.0 if positive_ph and pha < 0 else 0), + NOAA_SPEEDS[con_name.upper()]] curr_pt += 1 finally: # delete the temp directory @@ -213,7 +235,7 @@ def get_components(self, locs, cons=None, positive_ph=False): os.chdir(self.work_path) os.rmdir(self.temp_folder) - return con_data + return self def have_constituent(self, a_name): """Check if teh given constituent is supported by the ADCIRC tidal database. diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 91a2ed9..73058e0 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -13,15 +13,17 @@ import pandas as pd from .tidal_constituents import NOAA_SPEEDS -from .tidal_database import TidalDB +from .tidal_database import convert_coords, TidalDB class LeProvostDB(TidalDB): """Extractor class for the LeProvost tidal database. Attributes: - leprovost_path (str): Fully qualified path to a folder containing the LeProvost *.legi files cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the LeProvost database + leprovost_path (str): Fully qualified path to a folder containing the LeProvost *.legi files + data (:obj:`list` of :obj:`pandas.DataFrame`): List of the constituent component DataFrames with one + per point location requested from get_components(). Intended return value of get_components(). """ def __init__(self, leprovost_path): @@ -34,6 +36,7 @@ def __init__(self, leprovost_path): TidalDB.__init__(self) self.cons = ['M2', 'S2', 'N2', 'K1', 'O1', 'NU2', 'MU2', '2N2', 'Q1', 'T2', 'P1', 'L2', 'K2'] self.leprovost_path = leprovost_path + self.data = [] self.validate_files() def validate_files(self): @@ -42,22 +45,35 @@ def validate_files(self): If the database is incomplete or nonexistent, a new LeProvost database will be downloaded from Aquaveo.com. """ - if not os.path.isdir(self.leprovost_path): # Make sure the directory exists - os.mkdir(self.leprovost_path) - # Make sure all the database files exist in the directory legi_files = ['M2.legi', 'S2.legi', 'N2.legi', 'K1.legi', 'O1.legi', 'NU2.legi', 'MU2.legi', '2N2.legi', 'Q1.legi', 'T2.legi', 'P1.legi', 'L2.legi', 'K2.legi'] + + if not self.leprovost_path: # Use the working directory if none provided + local_leprovost = os.path.join(os.getcwd(), "LeProvost") + if all([os.path.isfile(os.path.join(local_leprovost, f)) for f in legi_files]): + self.leprovost_path = local_leprovost + return + else: + self.leprovost_path = os.getcwd() + + if not os.path.isdir(self.leprovost_path): + os.mkdir(self.leprovost_path) + + # Make sure all the database files exist in the directory if not all([os.path.isfile(os.path.join(self.leprovost_path, f)) for f in legi_files]): # Download from the Aquaveo website leprovost_url = 'http://sms.aquaveo.com/ADCIRC_Essentials.zip' zip_file = os.path.join(self.leprovost_path, "ADCIRC_Essentials.zip") + print("Downloading resource: {}".format(leprovost_url)) with urllib.request.urlopen(leprovost_url) as response, open(zip_file, 'wb') as out_file: shutil.copyfileobj(response, out_file) # Unzip the files + print("Unzipping files to: {}".format(self.leprovost_path)) with ZipFile(zip_file, 'r') as unzipper: for file in unzipper.namelist(): if file.startswith('LeProvost/'): unzipper.extract(file, self.leprovost_path) + print("Deleting zip file: {}".format(zip_file)) os.remove(zip_file) # delete the zip file self.leprovost_path = os.path.join(self.leprovost_path, "LeProvost") @@ -65,8 +81,8 @@ def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed of specified constituents at specified point locations. Args: - locs (:obj:`list` of :obj:`tuple` of :obj:`float`): List of the point locations to get - amplitude and phase for. e.g. [(x1, y1), (x2, y2)] + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] + of the requested points. cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If not supplied, all valid constituents will be extracted. positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or @@ -76,29 +92,23 @@ def get_components(self, locs, cons=None, positive_ph=False): :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, where each element in the return list is the constituent data for the corresponding element in locs. - Empty list on error. + Empty list on error. Note that function uses fluent interface pattern. """ # If no constituents specified, extract all valid constituents. if not cons: cons = self.cons - # read the file for each constituent - con_data = [] - for pt in locs: - y_lat = pt[1] - x_lon = pt[0] - if x_lon < 0.0: - x_lon = x_lon + 360.0 - if x_lon > 180.0: - x_lon = x_lon - 360.0 - if x_lon > 180.0 or x_lon < -180.0 or y_lat > 90.0 or y_lat < -90.0: - # ERROR: Not in latitude/longitude - return con_data - - con_data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + + # Make sure point locations are valid lat/lon + locs = convert_coords(locs) + if not locs: + return self # ERROR: Not in latitude/longitude + + self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] deg2rad = 1.0 / 180.0 * math.pi rad2deg = 1.0 / deg2rad + # read the file for each constituent for con in cons: if self.have_constituent(con): con = con.lower() @@ -136,9 +146,10 @@ def get_components(self, locs, cons=None, positive_ph=False): g_pha[k][lat] = float(line_vals[cur_val]) cur_val += 1 + # Extract components for each point for this constituent for i, pt in enumerate(locs): - y_lat = pt[1] - x_lon = pt[0] + y_lat = pt[0] + x_lon = pt[1] if x_lon < 0.0: x_lon = x_lon + 360.0 if x_lon > 180.0: @@ -168,7 +179,7 @@ def get_components(self, locs, cons=None, positive_ph=False): skip = True if skip: - con_data[i].loc[con.upper()] = [0.0, 0.0, 0.0] + self.data[i].loc[con.upper()] = [0.0, 0.0, 0.0] else: xratio = (x_lon - xlonlo) / d_lon yratio = (y_lat - ylatlo) / d_lat @@ -210,9 +221,9 @@ def get_components(self, locs, cons=None, positive_ph=False): if xsin < 0.0: phase = 360.0 - phase phase += (360. if positive_ph and phase < 0 else 0) - con_data[i].loc[con.upper()] = [amp, phase, NOAA_SPEEDS[con.upper()]] + self.data[i].loc[con.upper()] = [amp, phase, NOAA_SPEEDS[con.upper()]] - return con_data + return self def have_constituent(self, a_name): """Checks if a constituent name is valid for the LeProvost tidal database diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index 2dc3fdf..b2ca353 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -6,13 +6,43 @@ import pandas as pd + +NCNST = 37 + + +def convert_coords(coords): + """Convert latitude coordinates to [-180, 180]. + + Args: + coords (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] + of the requested point. + + Returns: + :obj:`list` of :obj:`tuple` of :obj:`float`: The list of converted coordinates. None if a coordinate out of + range was encountered + + """ + # Make sure point locations are valid lat/lon + for idx, pt in enumerate(coords): + y_lat = pt[0] + x_lon = pt[1] + if x_lon < 0.0: + x_lon = x_lon + 360.0 + if x_lon > 180.0: + x_lon = x_lon - 360.0 + if x_lon > 180.0 or x_lon < -180.0 or y_lat > 90.0 or y_lat < -90.0: + # ERROR: Not in latitude/longitude + return None + else: + coords[idx] = (y_lat, x_lon) + return coords + + class TidalDBEnum(Enum): TIDAL_DB_LEPROVOST = 0 TIDAL_DB_ADCIRC = 1 -NCNST = 37 - class OrbitVariables(object): def __init__(self): diff --git a/test.py b/test.py index 3df5d9b..198d820 100644 --- a/test.py +++ b/test.py @@ -3,16 +3,8 @@ import harmonica.adcirc_database import harmonica.leprovost_database -#import TPXODatabase - -def write_nodal(f, nf): - for n in nf: - f.write(n.name + ", " + - str(n.amplitude) + ", " + - str(n.frequency) + ", " + - str(n.earth_tide_reduction_factor) + ", " + - str(n.equilibrium_argument) + ", " + - str(n.nodal_factor) + "\n") +import harmonica.tidal_constituents +# import TPXODatabase if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -22,20 +14,27 @@ def write_nodal(f, nf): help="path to the ADCIRC Atlantic database") parser.add_argument("-p", "--adcirc_pacific", default="", help="path to the ADCIRC Pacific database") - parser.add_argument("-l", "--leprovost", - default=os.getcwd(), help="path to the LeProvost database folder") + parser.add_argument("-l", "--leprovost", default="", + help="path to the LeProvost database folder") args = vars(parser.parse_args()) work_dir = args["work_dir"] adcirc_atlantic = args["adcirc_atlantic"] adcir_pacific = args["adcirc_pacific"] leprovost = args["leprovost"] - - atlantic = [(-74.07, 39.74), # Not valid with ADCIRC Pacific database - (-71.4, 42.32), - (-69.38, 45.44)] - pacific = [(-124.55, 43.63), # Not valid with ADCIRC Atlantic database - (-124.38, 46.18)] + + # Need to be in (lat, lon), not (x, y) + # Invert commented list declarations to test [-180, 180] vs. [0, 360] ranges. + #atlantic = [(39.74, 285.93), # Not valid with ADCIRC Pacific database + # (42.32, 288.6), + # (45.44, 290.62)] + #pacific = [(43.63, 235.45), # Not valid with ADCIRC Atlantic database + # (46.18, 235.62)] + atlantic = [(39.74, -74.07), # Not valid with ADCIRC Pacific database + (42.32, -71.4), + (45.44, -69.38)] + pacific = [(43.63, -124.55), # Not valid with ADCIRC Atlantic database + (46.18, -124.38)] all_points = [] # All locations valid with LeProvost all_points.extend(atlantic) all_points.extend(pacific) @@ -46,37 +45,45 @@ def write_nodal(f, nf): harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NWAT) ad_pacific_db = harmonica.adcirc_database.AdcircDB(work_dir, adcir_pacific, harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NEPAC) - le_db = harmonica.leprovost_database.LeProvostDB(leprovost) + leprovost_db = harmonica.leprovost_database.LeProvostDB(leprovost) # tp_db = TPXODatabase.TpxoDB('tpxo8') + tpx0_db = harmonica.tidal_constituents.Constituents() - ad_al_nf = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) - ad_pa_nf = ad_pacific_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) - le_nf = le_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + ad_al_nodal_factor = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + ad_pa_nodal_factor = ad_pacific_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + leprovost_nodal_factor = leprovost_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) # tp_nf = tp_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) f = open(os.path.join(work_dir, "tidal_test.out"), "w") f.write("ADCIRC Atlantic nodal factor:\n") - f.write(ad_al_nf.to_string() + "\n\n") + f.write(ad_al_nodal_factor.to_string() + "\n\n") f.write("ADCIRC Pacific nodal factor:\n") - f.write(ad_pa_nf.to_string() + "\n\n") + f.write(ad_pa_nodal_factor.to_string() + "\n\n") f.write("LeProvost nodal factor:\n") - f.write(le_nf.to_string() + "\n\n") + f.write(leprovost_nodal_factor.to_string() + "\n\n") f.flush() - ap1 = ad_alantic_db.get_components(atlantic, good_cons) - ap2 = ad_pacific_db.get_components(pacific, good_cons) - ap3 = le_db.get_components(all_points, good_cons) - #ap4 = tp_db.get_amplitude_and_phase(good_cons, all_points) + all_points1 = ad_alantic_db.get_components(atlantic, good_cons) + all_points2 = ad_pacific_db.get_components(pacific, good_cons) + all_points3 = leprovost_db.get_components(all_points, good_cons) + #all_points4 = tp_db.get_amplitude_and_phase(good_cons, all_points) f.write("ADCIRC Atlantic components:\n") - for pt in ap1: + for pt in all_points1.data: f.write(pt.to_string() + "\n\n") f.write("ADCIRC Pacific components:\n") - for pt in ap2: + for pt in all_points2.data: f.write(pt.to_string() + "\n\n") f.write("LeProvost components:\n") - for pt in ap3: + for pt in all_points3.data: f.write(pt.to_string() + "\n\n") - # f.write(str(ap4) + "\n\n") + f.write("TPX0 components:\n") + try: + for pt in all_points: + components = tpx0_db.get_components(pt, 'tpxo8', good_cons, True) + f.write(components.data.to_string() + "\n\n") + f.flush() + except Exception as e: + print("Exception thrown during TPX0 extraction: {}".format(e)) f.close() \ No newline at end of file From 800a6b9de832d77fdf67da50c895d7fd90286a53 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 20 Sep 2018 14:01:33 -0600 Subject: [PATCH 04/24] changed a list comprehension --- harmonica/tidal_database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index b2ca353..2cb9b53 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -117,7 +117,7 @@ def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): 0.000064958541129, 0.000145245007353, 0.000145643201313, 0.000062319338107, 0.000072522945975, 0.000150369306157, 0.000210778353763, 0.000143158105531, 0.000208116646659, 0.000145842317201, 0.000562075610519, 0.000285963006842] - con_etrf = NCNST*[0.0690] + con_etrf = [0.0690 for _ in range(NCNST)] con_etrf[3] = 0.736 # clK1 con_etrf[5] = 0.695 # O1 con_etrf[29] = 0.706 # P1 @@ -126,7 +126,7 @@ def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): con_etrf[2] = 0.693 # N2 con_etrf[1] = 0.693 # S2 con_etrf[34] = 0.693 # K2 - con_amp = NCNST*[0.0] + con_amp = [0.0 for _ in range(NCNST)] con_amp[3] = 0.141565 # K1 con_amp[5] = 0.100514 # O1 con_amp[29] = 0.046834 # P1 From 880c4cde01010649fb4635d8fcb657f4f01ecdbf Mon Sep 17 00:00:00 2001 From: Winters Date: Fri, 21 Sep 2018 11:20:03 -0500 Subject: [PATCH 05/24] fix docstring and missing import --- harmonica/cli/main_deconstruct.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/harmonica/cli/main_deconstruct.py b/harmonica/cli/main_deconstruct.py index d1820b3..f98cc11 100644 --- a/harmonica/cli/main_deconstruct.py +++ b/harmonica/cli/main_deconstruct.py @@ -5,12 +5,14 @@ import argparse import numpy as np import pandas as pd +import sys DESCR = 'Deconstruct the signal into its tidal constituents.' EXAMPLE = """ Example: - harmonica deconstruct 38.375789 -74.943915 -C M2 K1 -M tpxo8 + harmonica deconstruct CO-OPS__8760922__wl.csv --columns "Date Time" "Water Level" \ + --datetime_format '%Y-%m-%d %H:%M' -C M2 S2 N2 K1 """ def config_parser(p, sub=False): From 1bb7a4929d0dbf33c8d2eae1b4c6ba7ce10caabb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 21 Sep 2018 10:48:40 -0600 Subject: [PATCH 06/24] Reorganized test case into Tutorials folder. Added Sphinx files for auto doc generation. Added a tutorial for using the Python API with TPXO, ADCIRC, and LeProvost. --- doc/Makefile | 20 +++ doc/README.txt | 12 ++ doc/make.bat | 36 ++++ doc/source/Documentation.rst | 10 ++ doc/source/Installation.rst | 6 + doc/source/Tutorials.rst | 6 + doc/source/adcirc_database.rst | 13 ++ doc/source/conf.py | 168 +++++++++++++++++++ doc/source/index.rst | 28 ++++ doc/source/leprovost_database.rst | 11 ++ doc/source/resource.rst | 6 + doc/source/simple_python.rst | 70 ++++++++ doc/source/tidal_constituents.rst | 6 + doc/source/tidal_database.rst | 6 + tutorials/python_api/expected_tidal_test.out | 115 +++++++++++++ test.py => tutorials/python_api/test.py | 42 ++--- 16 files changed, 535 insertions(+), 20 deletions(-) create mode 100644 doc/Makefile create mode 100644 doc/README.txt create mode 100644 doc/make.bat create mode 100644 doc/source/Documentation.rst create mode 100644 doc/source/Installation.rst create mode 100644 doc/source/Tutorials.rst create mode 100644 doc/source/adcirc_database.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/index.rst create mode 100644 doc/source/leprovost_database.rst create mode 100644 doc/source/resource.rst create mode 100644 doc/source/simple_python.rst create mode 100644 doc/source/tidal_constituents.rst create mode 100644 doc/source/tidal_database.rst create mode 100644 tutorials/python_api/expected_tidal_test.out rename test.py => tutorials/python_api/test.py (72%) diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..5f19f6c --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = harmonica +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/doc/README.txt b/doc/README.txt new file mode 100644 index 0000000..2c6cbb3 --- /dev/null +++ b/doc/README.txt @@ -0,0 +1,12 @@ +To build the documentation for harmonica execute the following command in a console: + +make + +Format is typically "html", but to view all available formats execute the make command with no arguments. + +After executing the make command, the output files will be in the build directory. "Index.html" +(or "Index" with other format extension) is the root of the documentation pages. + +Edit the *.rst reStructuredText files in the source directory to change the layout/content/format +of the pages. Edit the conf.py file in the source directory to change the settings used by Sphinx +to generate the documentation. \ No newline at end of file diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..096841d --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=harmonica + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/doc/source/Documentation.rst b/doc/source/Documentation.rst new file mode 100644 index 0000000..380d2da --- /dev/null +++ b/doc/source/Documentation.rst @@ -0,0 +1,10 @@ +Documentation +===================================== + +.. toctree:: + + tidal_database + tidal_constituents + adcirc_database + leprovost_database + resource \ No newline at end of file diff --git a/doc/source/Installation.rst b/doc/source/Installation.rst new file mode 100644 index 0000000..1953b32 --- /dev/null +++ b/doc/source/Installation.rst @@ -0,0 +1,6 @@ +.. _Installation: + +Installation +===================================== + +TODO: Provide steps for installing the harmonica package. \ No newline at end of file diff --git a/doc/source/Tutorials.rst b/doc/source/Tutorials.rst new file mode 100644 index 0000000..db4c028 --- /dev/null +++ b/doc/source/Tutorials.rst @@ -0,0 +1,6 @@ +Tutorials +===================================== + +.. toctree:: + + simple_python \ No newline at end of file diff --git a/doc/source/adcirc_database.rst b/doc/source/adcirc_database.rst new file mode 100644 index 0000000..22f784e --- /dev/null +++ b/doc/source/adcirc_database.rst @@ -0,0 +1,13 @@ +harmonica.adcirc_database Module +===================================== + +This module is used to extract amplitude and phase tidal harmonics from the +ADCIRC tidal database. NOAA constituent speeds are also provided (https://tidesandcurrents.noaa.gov). +This module can also extract the frequency, earth tidal reduction factor, amplitude, nodal factor, +and equilibrium argument for specified constituents at a specified time. ADCIRC provides two +tidal databases, one for the Northwest Atlantic and one for Northeast Pacific. Specify +the appropriate location on construction. + +.. automodule:: harmonica.adcirc_database + :members: + :noindex: \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..7e6f457 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- 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 +import sys +# sys.path.insert(0, os.path.abspath('.')) +sys.path.append("C:\\Users\\aclark\\AppData\\Local\\conda\\conda\\envs\\harmonica\\Lib\\site-packages") + + +# -- Project information ----------------------------------------------------- + +project = 'harmonica' +copyright = '2018, Kevin Winters' +author = 'Kevin Winters' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '0.1' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.ifconfig', + 'sphinx.ext.napoleon' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# 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'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'harmonicadoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'harmonica.tex', 'harmonica Documentation', + 'Kevin Winters', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'harmonica', 'harmonica Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'harmonica', 'harmonica Documentation', + author, 'harmonica', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..2581326 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,28 @@ +.. harmonica documentation master file, created by + sphinx-quickstart on Thu Sep 20 17:01:36 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to harmonica's documentation! +===================================== + +harmonica is an API and CLI to get amplitude, phase, and speed of tidal harmonics +from various tidal models. harmonica currently supports TPXO (versions 7.2, 8, and 9), +ADCIRC, and LeProvost. Also provides functionality to build water surface time series +utilizing pytides reconstruction and access to pytides deconstruction. + +.. toctree:: + :maxdepth: 1 + + Installation + Documentation + Tutorials + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/leprovost_database.rst b/doc/source/leprovost_database.rst new file mode 100644 index 0000000..84b25e7 --- /dev/null +++ b/doc/source/leprovost_database.rst @@ -0,0 +1,11 @@ +harmonica.leprovost_database Module +===================================== + +This module is used to extract amplitude and phase tidal harmonics from the +LeProvost tidal database. NOAA constituent speeds are also provided (https://tidesandcurrents.noaa.gov). +This module can also provide the frequency, earth tidal reduction factor, amplitude, +nodal factor, and equilibrium argument for specified constituents at a specified time. + +.. automodule:: harmonica.leprovost_database + :members: + :noindex: \ No newline at end of file diff --git a/doc/source/resource.rst b/doc/source/resource.rst new file mode 100644 index 0000000..29cad96 --- /dev/null +++ b/doc/source/resource.rst @@ -0,0 +1,6 @@ +harmonica.resource Module +===================================== + +.. automodule:: harmonica.resource + :members: + :noindex: \ No newline at end of file diff --git a/doc/source/simple_python.rst b/doc/source/simple_python.rst new file mode 100644 index 0000000..00b25ac --- /dev/null +++ b/doc/source/simple_python.rst @@ -0,0 +1,70 @@ +Using the Python API +===================================== + +This tutorial demonstrates the use of the hamonica Python API. Amplitude, phase, +and speed tidal harmonics are extracted for a small set of points from the TPXO8, ADCIRC +(Northwest Atlantic and Northeast Pacific), and LeProvost tidal databases. The tutorial +also provides an example of obtaining the frequency, earth tidal reduction factor, amplitude, +nodal factor, and equilibrium argument of a constituent at a specified time using the ADCIRC +and LeProvost tidal databases. + +To complete this tutorial you must have the harmonica package installed. See :ref:`Installation` for +instructions on installing the harmonica package to a Python environment. + +We will be using the script located at :file:`{harmonica root}/tutorials/python_api/test.py`. +The code is also provided below. + +.. literalinclude:: ../../tutorials/python_api/test.py + :linenos: + +Executing :file:`test.py` will fetch the required tidal resources from the Internet if +they do not already exist in the working directory. To specify existing resources use the +following command line arguments: + +-l path Path to the LeProvost *.legi files +-a file Path and filename of the ADCIRC Northwest Atlantic executable +-p file Path and filename of the ADCIRC Northeast Pacific executable + +You may also specify a temporary working directory that will be used by the ADCIRC +database extractors (default is current working directory). + +-w path Path ADCIRC extractors will use as a temporary working directory + +After executing the script, output from the tidal harmonic extraction can be viewed in +:file:`tidal_test.out` located in the Python API tutorial directory. + +The following lines set up the point locations we will be extracting tidal harmonic +components for. Locations should be specified as tuples of latitude and longitude +degrees. Note that we have split locations in the Atlantic and Pacific. LeProvost and +TPXO support all ocean locations, but the ADCIRC database is restricted to one or the other. + +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 25-39 + :lineno-match: + +The next section of code sets up the tidal harmonic extraction interface. For the ADCIRC +extractors, the tidal region must be specified at construction. + +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 42-51 + :lineno-match: + +The ADCIRC and LeProvost extractors can also provide frequencies, earth tidal reduction factors, amplitudes, nodal factors, +and equilibrium arguments for specified constituents at a specified time. The next section of code demonstrates this. + +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 53-56 + :lineno-match: + +The next block uses the ADCIRC and LeProvost interfaces to extract tidal harmonic constituents for a list of +locations and constituents. + +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 67-71 + :lineno-match: + +Finally, the TPXO interface extracts tidal harmonic constituents for a single location at a time. + +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 83-87 + :lineno-match: \ No newline at end of file diff --git a/doc/source/tidal_constituents.rst b/doc/source/tidal_constituents.rst new file mode 100644 index 0000000..d9259dc --- /dev/null +++ b/doc/source/tidal_constituents.rst @@ -0,0 +1,6 @@ +harmonica.tidal_constituents Module +===================================== + +.. automodule:: harmonica.tidal_constituents + :members: + :noindex: \ No newline at end of file diff --git a/doc/source/tidal_database.rst b/doc/source/tidal_database.rst new file mode 100644 index 0000000..29b9b64 --- /dev/null +++ b/doc/source/tidal_database.rst @@ -0,0 +1,6 @@ +harmonica.tidal_database Module +===================================== + +.. automodule:: harmonica.tidal_database + :members: + :noindex: \ No newline at end of file diff --git a/tutorials/python_api/expected_tidal_test.out b/tutorials/python_api/expected_tidal_test.out new file mode 100644 index 0000000..5a6271d --- /dev/null +++ b/tutorials/python_api/expected_tidal_test.out @@ -0,0 +1,115 @@ +ADCIRC Atlantic nodal factor: + amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor +M2 0.242334 0.000141 0.693 345.151862 1.021162 +S2 0.112841 0.000145 0.693 90.000000 1.000000 +N2 0.046398 0.000138 0.693 77.558471 1.021162 +K1 0.141565 0.000073 0.736 105.776526 0.945419 + +ADCIRC Pacific nodal factor: + amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor +M2 0.242334 0.000141 0.693 345.151862 1.021162 +S2 0.112841 0.000145 0.693 90.000000 1.000000 +N2 0.046398 0.000138 0.693 77.558471 1.021162 +K1 0.141565 0.000073 0.736 105.776526 0.945419 + +LeProvost nodal factor: + amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor +M2 0.242334 0.000141 0.693 345.151862 1.021162 +S2 0.112841 0.000145 0.693 90.000000 1.000000 +N2 0.046398 0.000138 0.693 77.558471 1.021162 +K1 0.141565 0.000073 0.736 105.776526 0.945419 + +ADCIRC Atlantic components: + amplitude phase speed +K1 0.094224 174.103 15.041069 +M2 0.574420 351.285 28.984104 +S2 0.116490 12.800 30.000000 +N2 0.132220 337.764 28.439730 + + amplitude phase speed +K1 0.10643 207.605 15.041069 +M2 1.42770 114.565 28.984104 +S2 0.21213 150.550 30.000000 +N2 0.29871 83.796 28.439730 + + amplitude phase speed +K1 0.12156 198.872 15.041069 +M2 1.99330 96.345 28.984104 +S2 0.29833 131.891 30.000000 +N2 0.40195 66.496 28.439730 + +ADCIRC Pacific components: + amplitude phase speed +K1 0.44356 234.182 15.041069 +M2 0.84280 221.629 28.984104 +S2 0.22670 245.825 30.000000 +N2 0.17248 195.838 28.439730 + + amplitude phase speed +K1 0.45602 238.807 15.041069 +M2 0.94421 230.499 28.984104 +S2 0.26453 256.892 30.000000 +N2 0.19271 205.061 28.439730 + +LeProvost components: + amplitude phase speed +M2 0.589858 353.748502 28.984104 +S2 0.083580 20.950538 30.000000 +N2 0.136323 337.950345 28.439730 +K1 0.080152 171.446949 15.041069 + + amplitude phase speed +M2 0.0 0.0 0.0 +S2 0.0 0.0 0.0 +N2 0.0 0.0 0.0 +K1 0.0 0.0 0.0 + + amplitude phase speed +M2 0.0 0.0 0.0 +S2 0.0 0.0 0.0 +N2 0.0 0.0 0.0 +K1 0.0 0.0 0.0 + + amplitude phase speed +M2 0.770437 224.199371 28.984104 +S2 0.209673 249.043022 30.000000 +N2 0.159378 202.381338 28.439730 +K1 0.428306 233.851748 15.041069 + + amplitude phase speed +M2 0.858488 232.029922 28.984104 +S2 0.242000 258.787649 30.000000 +N2 0.175845 210.476586 28.439730 +K1 0.441807 238.246609 15.041069 + +TPX0 components: + amplitude phase speed +S2 0.106576 18.615143 30.000000 +M2 0.560701 352.265551 28.984104 +K1 0.092135 176.901563 15.041069 +N2 0.139185 346.750468 28.439730 + + amplitude phase speed +S2 0.0 0.0 30.000000 +M2 0.0 0.0 28.984104 +K1 0.0 0.0 15.041069 +N2 0.0 0.0 28.439730 + + amplitude phase speed +S2 0.0 0.0 30.000000 +M2 0.0 0.0 28.984104 +K1 0.0 0.0 15.041069 +N2 0.0 0.0 28.439730 + + amplitude phase speed +S2 0.219652 246.510256 30.000000 +M2 0.809024 222.339913 28.984104 +K1 0.412604 232.880147 15.041069 +N2 0.169892 196.376201 28.439730 + + amplitude phase speed +S2 0.253107 260.181916 30.000000 +M2 0.910663 234.117743 28.984104 +K1 0.428817 239.531238 15.041069 +N2 0.188528 206.717292 28.439730 + diff --git a/test.py b/tutorials/python_api/test.py similarity index 72% rename from test.py rename to tutorials/python_api/test.py index 198d820..9499830 100644 --- a/test.py +++ b/tutorials/python_api/test.py @@ -4,7 +4,6 @@ import harmonica.adcirc_database import harmonica.leprovost_database import harmonica.tidal_constituents -# import TPXODatabase if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -35,24 +34,26 @@ (45.44, -69.38)] pacific = [(43.63, -124.55), # Not valid with ADCIRC Atlantic database (46.18, -124.38)] - all_points = [] # All locations valid with LeProvost + all_points = [] # All locations valid with LeProvost and TPXO all_points.extend(atlantic) all_points.extend(pacific) good_cons = ['M2', 'S2', 'N2', 'K1'] - # bad_con = 'ZZ7' + # Create an ADCIRC database for the Atlantic locations. ad_alantic_db = harmonica.adcirc_database.AdcircDB(work_dir, adcirc_atlantic, harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NWAT) + # Create an ADCIRC database for the Pacific locations. ad_pacific_db = harmonica.adcirc_database.AdcircDB(work_dir, adcir_pacific, harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NEPAC) + # Create a LeProvost database for all locations. leprovost_db = harmonica.leprovost_database.LeProvostDB(leprovost) - # tp_db = TPXODatabase.TpxoDB('tpxo8') - tpx0_db = harmonica.tidal_constituents.Constituents() + # Create a TPXO database for all locations. + tpxo_db = harmonica.tidal_constituents.Constituents() + # Get nodal factor data from the ADCIRC and LeProvost tidal databases ad_al_nodal_factor = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) ad_pa_nodal_factor = ad_pacific_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) leprovost_nodal_factor = leprovost_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) - # tp_nf = tp_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) f = open(os.path.join(work_dir, "tidal_test.out"), "w") f.write("ADCIRC Atlantic nodal factor:\n") @@ -63,27 +64,28 @@ f.write(leprovost_nodal_factor.to_string() + "\n\n") f.flush() - all_points1 = ad_alantic_db.get_components(atlantic, good_cons) - all_points2 = ad_pacific_db.get_components(pacific, good_cons) - all_points3 = leprovost_db.get_components(all_points, good_cons) - #all_points4 = tp_db.get_amplitude_and_phase(good_cons, all_points) + # Get tidal harmonic components for a list of points using the ADCIRC and + # LeProvost databases. + ad_atlantic_comps = ad_alantic_db.get_components(atlantic, good_cons) + ad_pacific_comps = ad_pacific_db.get_components(pacific, good_cons) + leprovost_comps = leprovost_db.get_components(all_points, good_cons) f.write("ADCIRC Atlantic components:\n") - for pt in all_points1.data: + for pt in ad_atlantic_comps.data: f.write(pt.to_string() + "\n\n") f.write("ADCIRC Pacific components:\n") - for pt in all_points2.data: + for pt in ad_pacific_comps.data: f.write(pt.to_string() + "\n\n") f.write("LeProvost components:\n") - for pt in all_points3.data: + for pt in leprovost_comps.data: f.write(pt.to_string() + "\n\n") + + # Get tidal harmonic components for a single point using the TPXO tidal model. f.write("TPX0 components:\n") - try: - for pt in all_points: - components = tpx0_db.get_components(pt, 'tpxo8', good_cons, True) - f.write(components.data.to_string() + "\n\n") - f.flush() - except Exception as e: - print("Exception thrown during TPX0 extraction: {}".format(e)) + for pt in all_points: + # Specify TPXO version when calling get_components() + components = tpxo_db.get_components(pt, 'tpxo8', good_cons, True) + f.write(components.data.to_string() + "\n\n") + f.flush() f.close() \ No newline at end of file From c9319519217234fd4be742743dc46f2ca25d46f8 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 4 Oct 2018 09:49:49 -0600 Subject: [PATCH 07/24] Subclass TPX0 from TidalDb, Move resource download/extract into base class - This an intermediate commit, not ready --- harmonica/adcirc_database.py | 77 ++++------------ harmonica/harmonica.py | 25 +++--- harmonica/leprovost_database.py | 65 +++----------- harmonica/resource.py | 142 +++++++++++++++++++++++------- harmonica/tidal_constituents.py | 150 +++++++++++--------------------- harmonica/tidal_database.py | 73 ++++++++++++++-- tutorials/python_api/test.py | 36 ++++---- 7 files changed, 282 insertions(+), 286 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 67fed6d..e652c97 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -6,13 +6,10 @@ import random import string import shutil -import urllib.request -from zipfile import ZipFile import pandas as pd -from .tidal_constituents import NOAA_SPEEDS -from .tidal_database import convert_coords, TidalDB +from .tidal_database import convert_coords, NOAA_SPEEDS, TidalDB class TidalDBAdcircEnum(Enum): @@ -45,7 +42,7 @@ class AdcircDB(TidalDB): tide_out (str): Temporary 'tides.out' filename with path. """ - def __init__(self, work_path, exe_with_path, db_region): + def __init__(self, resource_dir=None, db_region="adcircnwat"): """Get the amplitude and phase for the given constituents at the given points. Args: @@ -54,74 +51,32 @@ def __init__(self, work_path, exe_with_path, db_region): db_region (:obj: `TidalDBAdcircEnum`): The db_region of database. """ - TidalDB.__init__(self) - self.work_path = work_path - self.exe_with_path = exe_with_path - self.db_region = db_region self.cons = ['M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'Q1', 'K2'] - self.data = [] - if self.db_region == TidalDBAdcircEnum.TIDE_NWAT: + + if db_region.lower() == "adcircnwat": + self.db_region = TidalDBAdcircEnum.TIDE_NWAT self.grid_no_path = 'ec2001.grd' self.harm_no_path = 'ec2001.tdb' - elif self.db_region == TidalDBAdcircEnum.TIDE_NEPAC: + elif db_region.lower() == "adcircnepac": + self.db_region = TidalDBAdcircEnum.TIDE_NEPAC self.grid_no_path = 'enpac2003.grd' self.harm_no_path = 'enpac2003.tdb' else: - self.grid_no_path = '' - self.harm_no_path = '' + raise ValueError('unrecognized ADCIRC database region.') + super().__init__(db_region.lower()) + resource_dir = self.resources.download_model(resource_dir) + self.exe_with_path = os.path.join(resource_dir, self.resources.model_atts["consts"][0]["M2"]) + + # Build the temp working folder name src_list = list(string.ascii_uppercase + string.digits) rand_str = random.choice(src_list) - self.temp_folder = os.path.join(self.work_path, '_'.join(rand_str)) + self.temp_folder = os.path.join(resource_dir, '_'.join(rand_str)) # check that the folder does not exist while os.path.isdir(self.temp_folder): rand_str = random.choice(src_list) self.temp_folder = self.temp_folder + rand_str self.tide_in = os.path.join(self.temp_folder, 'tides.in') self.tide_out = os.path.join(self.temp_folder, 'tides.out') - self.validate_files() - - def validate_files(self): - """Check to ensure that the ADCIRC database exists. - - If the database is nonexistent, a new ADCIRC database will be downloaded from Aquaveo.com. Whether the Atlantic - or Pacific database is download depends on how the object was constructed. - - """ - if not os.path.isdir(self.work_path): - self.work_path = os.getcwd() - - if not os.path.isfile(self.exe_with_path): # Make sure the executable exists - # Download from the Aquaveo website - if self.db_region == TidalDBAdcircEnum.TIDE_NWAT: # Download the ADCIRC Atlantic database - adcirc_db_url = 'http://sms.aquaveo.com/adcircnwattides.zip' - basename = 'adcircnwattides' - if not self.exe_with_path: # check the local directory - local_exe = os.path.join(os.getcwd(), "adcircnwattides/adcircnwattides.exe") - if os.path.isfile(local_exe): - self.exe_with_path = local_exe - return - else: # Download the ADCIRC Pacific database - adcirc_db_url = 'http://sms.aquaveo.com/adcircnepactides.zip' - basename = 'adcircnepactides' - if not self.exe_with_path: # check the local directory - local_exe = os.path.join(os.getcwd(), "adcircnepactides/adcircnepactides.exe") - if os.path.isfile(local_exe): - self.exe_with_path = local_exe - return - zip_file = os.path.join(self.work_path, basename + '.zip') - print("Downloading resource: {}".format(adcirc_db_url)) - with urllib.request.urlopen(adcirc_db_url) as response, open(zip_file, 'wb') as out_file: - shutil.copyfileobj(response, out_file) - # Unzip the files - dest_path = os.path.join(self.work_path, basename) - if not os.path.isdir(dest_path): - os.mkdir(dest_path) - print("Unzipping files to: {}".format(dest_path)) - with ZipFile(zip_file, 'r') as unzipper: - unzipper.extractall(path=dest_path) - print("Deleting zip file: {}".format(zip_file)) - os.remove(zip_file) # delete the zip file - self.exe_with_path = os.path.join(dest_path, basename + ".exe") def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed for the given constituents at the given points. @@ -158,7 +113,7 @@ def get_components(self, locs, cons=None, positive_ph=False): constituents.append(con) # create the temp directory - os.mkdir(self.temp_folder) + os.makedirs(self.temp_folder) os.chdir(self.temp_folder) try: # write tides.in into the temp directory @@ -232,7 +187,7 @@ def get_components(self, locs, cons=None, positive_ph=False): del_files = os.listdir(self.temp_folder) for del_file in del_files: os.remove(del_file) - os.chdir(self.work_path) + os.chdir(os.path.dirname(self.temp_folder)) os.rmdir(self.temp_folder) return self diff --git a/harmonica/harmonica.py b/harmonica/harmonica.py index f4dcc38..51910ba 100644 --- a/harmonica/harmonica.py +++ b/harmonica/harmonica.py @@ -1,4 +1,4 @@ -from .tidal_constituents import Constituents +from .tidal_constituents import Constituents, NOAA_SPEEDS from .resource import ResourceManager from pytides.astro import astro from pytides.tide import Tide as pyTide @@ -6,7 +6,6 @@ from datetime import datetime import numpy as np import pandas as pd -import sys class Tide: @@ -31,7 +30,6 @@ def __init__(self): self.data = pd.DataFrame(columns=['datetimes', 'water_level']) self.constituents = Constituents() - def reconstruct_tide(self, loc, times, model=ResourceManager.DEFAULT_RESOURCE, cons=[], positive_ph=False, offset=None): """Rescontruct a tide signal water levels at the given location and times @@ -48,15 +46,16 @@ def reconstruct_tide(self, loc, times, model=ResourceManager.DEFAULT_RESOURCE, """ # get constituent information - self.constituents.get_components(loc, model, cons, positive_ph) + self.constituents.model = model + self.constituents.get_components([loc], cons, positive_ph) - ncons = len(self.constituents.data) + (1 if offset is not None else 0) + ncons = len(self.constituents.data[0]) + (1 if offset is not None else 0) tide_model = np.zeros(ncons, dtype=pyTide.dtype) # load specified model constituent components into pytides model object - for i, key in enumerate(self.constituents.data.index.values): + for i, key in enumerate(self.constituents.data[0].index.values): tide_model[i]['constituent'] = eval('pycons._{}'.format(self.PYTIDES_CON_MAPPER.get(key, key))) - tide_model[i]['amplitude'] = self.constituents.data.loc[key].amplitude - tide_model[i]['phase'] = self.constituents.data.loc[key].phase + tide_model[i]['amplitude'] = self.constituents.data[0].loc[key].amplitude + tide_model[i]['phase'] = self.constituents.data[0].loc[key].phase # if an offset is provided then add as spoofed constituent Z0 if offset is not None: tide_model[-1]['constituent'] = pycons._Z0 @@ -69,7 +68,6 @@ def reconstruct_tide(self, loc, times, model=ResourceManager.DEFAULT_RESOURCE, return self - def deconstruct_tide(self, water_level, times, cons=[], n_period=6, positive_ph=False): """Method to use pytides to deconstruct the tides and reorganize results back into the class structure. @@ -90,12 +88,11 @@ def deconstruct_tide(self, water_level, times, cons=[], n_period=6, positive_ph= cons = pycons.noaa else: cons = [eval('pycons._{}'.format(self.PYTIDES_CON_MAPPER.get(c, c))) for c in cons - if c in self.constituents.NOAA_SPEEDS] + if c in NOAA_SPEEDS] self.model_to_dataframe(pyTide.decompose(water_level, times, constituents=cons, n_period=n_period), times[0], positive_ph=positive_ph) return self - def model_to_dataframe(self, tide, t0=datetime.now(), positive_ph=False): """Method to reorganize data from the pytides tide model format into the native dataframe format. @@ -118,8 +115,8 @@ def extractor(c): cons = np.asarray(np.vectorize(extractor)(tide.model[tide.model['constituent'] != pycons._Z0])).T # convert into dataframe df = pd.DataFrame(cons[:,1:], index=cons[:,0], columns=['amplitude', 'phase', 'speed'], dtype=float) - self.constituents.data = pd.concat([self.constituents.data, df], axis=0, join='inner') + self.constituents.data[0] = pd.concat([self.constituents.data[0], df], axis=0, join='inner') # convert phase if necessary if not positive_ph: - self.constituents.data['phase'] = np.where(self.constituents.data['phase'] > 180., - self.constituents.data['phase'] - 360., self.constituents.data['phase']) \ No newline at end of file + self.constituents.data[0]['phase'] = np.where(self.constituents.data[0]['phase'] > 180., + self.constituents.data[0]['phase'] - 360., self.constituents.data[0]['phase']) \ No newline at end of file diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 73058e0..03fdac0 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -6,14 +6,10 @@ import math import os -import shutil -import urllib.request -from zipfile import ZipFile import pandas as pd -from .tidal_constituents import NOAA_SPEEDS -from .tidal_database import convert_coords, TidalDB +from .tidal_database import convert_coords, NOAA_SPEEDS, TidalDB class LeProvostDB(TidalDB): @@ -21,61 +17,22 @@ class LeProvostDB(TidalDB): Attributes: cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the LeProvost database - leprovost_path (str): Fully qualified path to a folder containing the LeProvost *.legi files + resource_dir (str): Fully qualified path to a folder containing the LeProvost *.legi files data (:obj:`list` of :obj:`pandas.DataFrame`): List of the constituent component DataFrames with one per point location requested from get_components(). Intended return value of get_components(). """ - def __init__(self, leprovost_path): + def __init__(self, resource_dir=None): """Constructor for database extractor Args: - leprovost_path (str): Fully qualified path to a folder containing the LeProvost *.legi files + resource_dir (str): Fully qualified path to a folder containing the LeProvost *.legi files """ - TidalDB.__init__(self) + # self.debugger = open("debug.txt", "w") + super().__init__("leprovost") + self.resource_dir = self.resources.download_model(resource_dir) self.cons = ['M2', 'S2', 'N2', 'K1', 'O1', 'NU2', 'MU2', '2N2', 'Q1', 'T2', 'P1', 'L2', 'K2'] - self.leprovost_path = leprovost_path - self.data = [] - self.validate_files() - - def validate_files(self): - """Check to ensure that the LeProvost folder exists and contains a complete set of files. - - If the database is incomplete or nonexistent, a new LeProvost database will be downloaded from Aquaveo.com. - - """ - legi_files = ['M2.legi', 'S2.legi', 'N2.legi', 'K1.legi', 'O1.legi', 'NU2.legi', 'MU2.legi', '2N2.legi', - 'Q1.legi', 'T2.legi', 'P1.legi', 'L2.legi', 'K2.legi'] - - if not self.leprovost_path: # Use the working directory if none provided - local_leprovost = os.path.join(os.getcwd(), "LeProvost") - if all([os.path.isfile(os.path.join(local_leprovost, f)) for f in legi_files]): - self.leprovost_path = local_leprovost - return - else: - self.leprovost_path = os.getcwd() - - if not os.path.isdir(self.leprovost_path): - os.mkdir(self.leprovost_path) - - # Make sure all the database files exist in the directory - if not all([os.path.isfile(os.path.join(self.leprovost_path, f)) for f in legi_files]): - # Download from the Aquaveo website - leprovost_url = 'http://sms.aquaveo.com/ADCIRC_Essentials.zip' - zip_file = os.path.join(self.leprovost_path, "ADCIRC_Essentials.zip") - print("Downloading resource: {}".format(leprovost_url)) - with urllib.request.urlopen(leprovost_url) as response, open(zip_file, 'wb') as out_file: - shutil.copyfileobj(response, out_file) - # Unzip the files - print("Unzipping files to: {}".format(self.leprovost_path)) - with ZipFile(zip_file, 'r') as unzipper: - for file in unzipper.namelist(): - if file.startswith('LeProvost/'): - unzipper.extract(file, self.leprovost_path) - print("Deleting zip file: {}".format(zip_file)) - os.remove(zip_file) # delete the zip file - self.leprovost_path = os.path.join(self.leprovost_path, "LeProvost") def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed of specified constituents at specified point locations. @@ -111,10 +68,10 @@ def get_components(self, locs, cons=None, positive_ph=False): # read the file for each constituent for con in cons: if self.have_constituent(con): - con = con.lower() + con = con.upper() else: continue - filename = os.path.join(self.leprovost_path, con + '.legi') + filename = os.path.join(self.resource_dir, self.resources.model_atts["consts"][0][con]) with open(filename, 'r') as f: # 30 columns in the file lon_min, lon_max = map(float, f.readline().split()) @@ -179,7 +136,7 @@ def get_components(self, locs, cons=None, positive_ph=False): skip = True if skip: - self.data[i].loc[con.upper()] = [0.0, 0.0, 0.0] + self.data[i].loc[con] = [0.0, 0.0, 0.0] else: xratio = (x_lon - xlonlo) / d_lon yratio = (y_lat - ylatlo) / d_lat @@ -221,7 +178,7 @@ def get_components(self, locs, cons=None, positive_ph=False): if xsin < 0.0: phase = 360.0 - phase phase += (360. if positive_ph and phase < 0 else 0) - self.data[i].loc[con.upper()] = [amp, phase, NOAA_SPEEDS[con.upper()]] + self.data[i].loc[con] = [amp, phase, NOAA_SPEEDS[con]] return self diff --git a/harmonica/resource.py b/harmonica/resource.py index 8620f58..b2d67c1 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -1,9 +1,13 @@ -from harmonica import config -from urllib.request import urlopen -import os.path -import string +import os +import shutil +import urllib.request +from zipfile import ZipFile + import xarray as xr +from harmonica import config + + class ResourceManager(object): """Harmonica resource manager to retrieve and access tide models""" @@ -17,7 +21,7 @@ class ResourceManager(object): 'dataset_atts': { 'units_multiplier': 1., # meters }, - 'consts': [{ # grouped by dimensionally compatiable files + 'consts': [{ # grouped by dimensionally compatiable files '2N2': 'tpxo9_netcdf/h_tpxo9.v1.nc', 'K1': 'tpxo9_netcdf/h_tpxo9.v1.nc', 'K2': 'tpxo9_netcdf/h_tpxo9.v1.nc', @@ -33,7 +37,7 @@ class ResourceManager(object): 'Q1': 'tpxo9_netcdf/h_tpxo9.v1.nc', 'S1': 'tpxo9_netcdf/h_tpxo9.v1.nc', 'S2': 'tpxo9_netcdf/h_tpxo9.v1.nc', - },], + }, ], }, 'tpxo8': { 'resource_atts': { @@ -44,7 +48,7 @@ class ResourceManager(object): 'units_multiplier': 0.001, # mm to meter }, 'consts': [ # grouped by dimensionally compatiable files - { # 1/30 degree + { # 1/30 degree 'K1': 'hf.k1_tpxo8_atlas_30c_v1.nc', 'K2': 'hf.k2_tpxo8_atlas_30c_v1.nc', 'M2': 'hf.m2_tpxo8_atlas_30c_v1.nc', @@ -55,7 +59,7 @@ class ResourceManager(object): 'Q1': 'hf.q1_tpxo8_atlas_30c_v1.nc', 'S2': 'hf.s2_tpxo8_atlas_30c_v1.nc', }, - { # 1/6 degree + { # 1/6 degree 'MF': 'hf.mf_tpxo8_atlas_6.nc', 'MM': 'hf.mm_tpxo8_atlas_6.nc', 'MN4': 'hf.mn4_tpxo8_atlas_6.nc', @@ -71,7 +75,7 @@ class ResourceManager(object): 'dataset_atts': { 'units_multiplier': 1., # meter }, - 'consts': [{ # grouped by dimensionally compatiable files + 'consts': [{ # grouped by dimensionally compatiable files 'K1': 'DATA/h_tpxo7.2.nc', 'K2': 'DATA/h_tpxo7.2.nc', 'M2': 'DATA/h_tpxo7.2.nc', @@ -85,7 +89,71 @@ class ResourceManager(object): 'P1': 'DATA/h_tpxo7.2.nc', 'Q1': 'DATA/h_tpxo7.2.nc', 'S2': 'DATA/h_tpxo7.2.nc', - },], + }, ], + }, + 'leprovost': { + 'resource_atts': { + 'url': "http://sms.aquaveo.com/ADCIRC_Essentials.zip", + 'archive': 'zip', # gzip compression + }, + 'dataset_atts': { + 'units_multiplier': 1., # meter + }, + 'consts': [{ # grouped by dimensionally compatible files + 'K1': 'LeProvost/K1.legi', + 'K2': 'LeProvost/K2.legi', + 'M2': 'LeProvost/M2.legi', + 'N2': 'LeProvost/N2.legi', + 'O1': 'LeProvost/O1.legi', + 'P1': 'LeProvost/P1.legi', + 'Q1': 'LeProvost/Q1.legi', + 'S2': 'LeProvost/S2.legi', + 'NU2': 'LeProvost/NU2.legi', + 'MU2': 'LeProvost/MU2.legi', + '2N2': 'LeProvost/2N2.legi', + 'T2': 'LeProvost/T2.legi', + 'L2': 'LeProvost/L2.legi', + }, ], + }, + 'adcircnwat': { + 'resource_atts': { + 'url': 'http://sms.aquaveo.com/adcircnwattides.zip', + 'archive': 'zip', # gzip compression + }, + 'dataset_atts': { + 'units_multiplier': 1., # meter + }, + 'consts': [{ # grouped by dimensionally compatible files + 'M2': 'adcircnwattides.exe', + 'S2': 'adcircnwattides.exe', + 'N2': 'adcircnwattides.exe', + 'K1': 'adcircnwattides.exe', + 'M4': 'adcircnwattides.exe', + 'O1': 'adcircnwattides.exe', + 'M6': 'adcircnwattides.exe', + 'Q1': 'adcircnwattides.exe', + 'K2': 'adcircnwattides.exe', + }, ], + }, + 'adcircnepac': { + 'resource_atts': { + 'url': 'http://sms.aquaveo.com/adcircnepactides.zip', + 'archive': 'zip', # gzip compression + }, + 'dataset_atts': { + 'units_multiplier': 1., # meter + }, + 'consts': [{ # grouped by dimensionally compatible files + 'M2': 'adcircnepactides.exe', + 'S2': 'adcircnepactides.exe', + 'N2': 'adcircnepactides.exe', + 'K1': 'adcircnepactides.exe', + 'M4': 'adcircnepactides.exe', + 'O1': 'adcircnepactides.exe', + 'M6': 'adcircnepactides.exe', + 'Q1': 'adcircnepactides.exe', + 'K2': 'adcircnepactides.exe', + }, ], }, } DEFAULT_RESOURCE = 'tpxo9' @@ -102,15 +170,12 @@ def __del__(self): for d in self.datasets: d.close() - def available_constituents(self): # get keys from const groups as list of lists and flatten return [c for sl in [grp.keys() for grp in self.model_atts['consts']] for c in sl] - def get_units_multiplier(self): return self.model_atts['dataset_atts']['units_multiplier'] - def download(self, resource, destination_dir): """Download a specified model resource.""" @@ -123,36 +188,48 @@ def download(self, resource, destination_dir): url = "".join((url, resource)) print('Downloading resource: {}'.format(url)) - response = urlopen(url) path = os.path.join(destination_dir, resource) - if rsrc_atts['archive'] is not None: - import tarfile + with urllib.request.urlopen(url) as response: + if rsrc_atts['archive'] is not None: + if rsrc_atts['archive'] == 'gz': + import tarfile - try: - tar = tarfile.open(mode='r:{}'.format(rsrc_atts['archive']), fileobj=response) - except IOError as e: - print(str(e)) + try: + tar = tarfile.open(mode='r:{}'.format(rsrc_atts['archive']), fileobj=response) + except IOError as e: + print(str(e)) + else: + rsrcs = set(c for sl in [x.values() for x in self.model_atts['consts']] for c in sl) + tar.extractall(path = destination_dir, members = [m for m in tar.getmembers() if m.name in rsrcs]) + tar.close() + elif rsrc_atts['archive'] == 'zip': # Unzip .zip files + zip_file = os.path.join(destination_dir, os.path.basename(resource) + '.zip') + with open(zip_file, 'wb') as out_file: + shutil.copyfileobj(response, out_file) + # Unzip the files + print("Unzipping files to: {}".format(destination_dir)) + with ZipFile(zip_file, 'r') as unzipper: + # Extract all the files in the archive + unzipper.extractall(path=destination_dir) + print("Deleting zip file: {}".format(zip_file)) + os.remove(zip_file) # delete the zip file else: - rsrcs = set(c for sl in [x.values() for x in self.model_atts['consts']] for c in sl) - tar.extractall(path = destination_dir, members = [m for m in tar.getmembers() if m.name in rsrcs]) - tar.close() - else: - with open(path, 'wb') as f: - f.write(response.read()) + with open(path, 'wb') as f: + f.write(response.read()) return path - - def download_model(self): + def download_model(self, resource_dir=None): """Download all of the model's resources for later use.""" resources = set(r for sl in [grp.values() for grp in self.model_atts['consts']] for r in sl) - resource_dir = os.path.join(config['data_dir'], self.model) + if not resource_dir: + resource_dir = os.path.join(config['data_dir'], self.model) for r in resources: path = os.path.join(resource_dir, r) if not os.path.exists(path): self.download(r, resource_dir) - + return resource_dir def remove_model(self): """Remove all of the model's resources.""" @@ -162,7 +239,6 @@ def remove_model(self): shutil.rmtree(resource_dir, ignore_errors=True) - def get_datasets(self, constituents): """Returns a list of xarray datasets.""" available = self.available_constituents() @@ -174,7 +250,7 @@ def get_datasets(self, constituents): rsrcs = set(const_group[const] for const in set(constituents) & set(const_group)) paths = set() - if (config['pre_existing_data_dir']): + if config['pre_existing_data_dir']: missing = set() for r in rsrcs: path = os.path.join(config['pre_existing_data_dir'], self.model, r) @@ -194,4 +270,4 @@ def get_datasets(self, constituents): if paths: self.datasets.append(xr.open_mfdataset(paths, engine='netcdf4', concat_dim='nc')) - return self.datasets \ No newline at end of file + return self.datasets diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index 81a8e37..d4a9738 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -1,68 +1,22 @@ from .resource import ResourceManager +from .tidal_database import NOAA_SPEEDS, TidalDB + from bisect import bisect -import os import numpy as np import pandas as pd -import sys - -# Dictionary of NOAA constituent speed constants (deg/hr) -# Source: https://tidesandcurrents.noaa.gov -# The speed is the rate change in the phase of a constituent, and is equal to 360 degrees divided by the -# constituent period expressed in hours -NOAA_SPEEDS = { - 'OO1': 16.139101, - '2Q1': 12.854286, - '2MK3': 42.92714, - '2N2': 27.895355, - '2SM2': 31.015896, - 'K1': 15.041069, - 'K2': 30.082138, - 'J1': 15.5854435, - 'L2': 29.528479, - 'LAM2': 29.455626, - 'M1': 14.496694, - 'M2': 28.984104, - 'M3': 43.47616, - 'M4': 57.96821, - 'M6': 86.95232, - 'M8': 115.93642, - 'MF': 1.0980331, - 'MK3': 44.025173, - 'MM': 0.5443747, - 'MN4': 57.423832, - 'MS4': 58.984104, - 'MSF': 1.0158958, - 'MU2': 27.968208, - 'N2': 28.43973, - 'NU2': 28.512583, - 'O1': 13.943035, - 'P1': 14.958931, - 'Q1': 13.398661, - 'R2': 30.041067, - 'RHO': 13.471515, - 'S1': 15.0, - 'S2': 30.0, - 'S4': 60.0, - 'S6': 90.0, - 'SA': 0.0410686, - 'SSA': 0.0821373, - 'T2': 29.958933, -} - -class Constituents: +class Constituents(TidalDB): """Harmonica tidal constituents.""" - def __init__(self): + def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): # constituent information dataframe: # amplitude (meters) # phase (degrees) # speed (degrees/hour, UTC/GMT) - self.data = pd.DataFrame(columns=['amplitude', 'phase', 'speed']) - + super().__init__(model) - def get_components(self, loc, model=ResourceManager.DEFAULT_RESOURCE, cons=[], positive_ph=False): + def get_components(self, locs, cons=None, positive_ph=False): """Query the a tide model database and return amplitude, phase and speed for a location. Currently written to query tpxo7, tpxo8, and tpxo9 tide models. @@ -79,23 +33,13 @@ def get_components(self, loc, model=ResourceManager.DEFAULT_RESOURCE, cons=[], p speed (degrees/hour, UTC/GMT) """ + self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] - # ensure lower case - model = model.lower() - if model == 'tpxo7_2': - model = 'tpxo7' - - lat, lon = loc - # check the phase of the longitude - if lon < 0: - lon = lon + 360. - - resources = ResourceManager(model=model) # if no constituents were requested, return all available if cons is None or not len(cons): - cons = resources.available_constituents() + cons = self.resources.available_constituents() # open the netcdf database(s) - for d in resources.get_datasets(cons): + for d in self.resources.get_datasets(cons): # remove unnecessary data array dimensions if present (e.g. tpxo7.2) if 'nx' in d.lat_z.dims: d['lat_z'] = d.lat_z.sel(nx=0, drop=True) @@ -104,39 +48,45 @@ def get_components(self, loc, model=ResourceManager.DEFAULT_RESOURCE, cons=[], p # get the dataset constituent name array from data cube nc_names = [x.tostring().decode('utf-8').strip().upper() for x in d.con.values] for c in set(cons) & set(nc_names): - # get constituent and bounding indices within the data cube - idx = {'con': nc_names.index(c)} - idx['top'] = bisect(d.lat_z[idx['con']], lat) - idx['right'] = bisect(d.lon_z[idx['con']], lon) - idx['bottom'] = idx['top'] - 1 - idx['left'] = idx['right'] - 1 - # get distance from the bottom left to the requested point - dx = (lon - d.lon_z.values[idx['con'], idx['left']]) / \ - (d.lon_z.values[idx['con'], idx['right']] - d.lon_z.values[idx['con'], idx['left']]) - dy = (lat - d.lat_z.values[idx['con'], idx['bottom']]) / \ - (d.lat_z.values[idx['con'], idx['top']] - d.lat_z.values[idx['con'], idx['bottom']]) - # calculate weights for bilinear spline - weights = np.array([ - (1. - dx) * (1. - dy), # w00 :: bottom left - (1. - dx) * dy, # w01 :: bottom right - dx * (1. - dy), # w10 :: top left - dx * dy # w11 :: top right - ]).reshape((2,2)) - weights = weights / weights.sum() - # devise the slice to subset surrounding values - query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] - # calculate the weighted tide from real and imaginary components - h = np.complex((d.hRe.values[query] * weights).sum(), -(d.hIm.values[query] * weights).sum()) - # get the phase and amplitude - ph = np.angle(h, deg=True) - # place info into data table - self.data.loc[c] = [ - # amplitude - np.absolute(h) * resources.get_units_multiplier(), - # phase - ph + (360. if positive_ph and ph < 0 else 0), - # speed - NOAA_SPEEDS[c] - ] + for i, loc in enumerate(locs): + lat, lon = loc + # check the phase of the longitude + if lon < 0: + lon = lon + 360. + + # get constituent and bounding indices within the data cube + idx = {'con': nc_names.index(c)} + idx['top'] = bisect(d.lat_z[idx['con']], lat) + idx['right'] = bisect(d.lon_z[idx['con']], lon) + idx['bottom'] = idx['top'] - 1 + idx['left'] = idx['right'] - 1 + # get distance from the bottom left to the requested point + dx = (lon - d.lon_z.values[idx['con'], idx['left']]) / \ + (d.lon_z.values[idx['con'], idx['right']] - d.lon_z.values[idx['con'], idx['left']]) + dy = (lat - d.lat_z.values[idx['con'], idx['bottom']]) / \ + (d.lat_z.values[idx['con'], idx['top']] - d.lat_z.values[idx['con'], idx['bottom']]) + # calculate weights for bilinear spline + weights = np.array([ + (1. - dx) * (1. - dy), # w00 :: bottom left + (1. - dx) * dy, # w01 :: bottom right + dx * (1. - dy), # w10 :: top left + dx * dy # w11 :: top right + ]).reshape((2,2)) + weights = weights / weights.sum() + # devise the slice to subset surrounding values + query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] + # calculate the weighted tide from real and imaginary components + h = np.complex((d.hRe.values[query] * weights).sum(), -(d.hIm.values[query] * weights).sum()) + # get the phase and amplitude + ph = np.angle(h, deg=True) + # place info into data table + self.data[i].loc[c] = [ + # amplitude + np.absolute(h) * self.resources.get_units_multiplier(), + # phase + ph + (360. if positive_ph and ph < 0 else 0), + # speed + NOAA_SPEEDS[c] + ] - return self \ No newline at end of file + return self diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index 2cb9b53..9388250 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -6,9 +6,54 @@ import pandas as pd +from .resource import ResourceManager NCNST = 37 +# Dictionary of NOAA constituent speed constants (deg/hr) +# Source: https://tidesandcurrents.noaa.gov +# The speed is the rate change in the phase of a constituent, and is equal to 360 degrees divided by the +# constituent period expressed in hours +NOAA_SPEEDS = { + 'OO1': 16.139101, + '2Q1': 12.854286, + '2MK3': 42.92714, + '2N2': 27.895355, + '2SM2': 31.015896, + 'K1': 15.041069, + 'K2': 30.082138, + 'J1': 15.5854435, + 'L2': 29.528479, + 'LAM2': 29.455626, + 'M1': 14.496694, + 'M2': 28.984104, + 'M3': 43.47616, + 'M4': 57.96821, + 'M6': 86.95232, + 'M8': 115.93642, + 'MF': 1.0980331, + 'MK3': 44.025173, + 'MM': 0.5443747, + 'MN4': 57.423832, + 'MS4': 58.984104, + 'MSF': 1.0158958, + 'MU2': 27.968208, + 'N2': 28.43973, + 'NU2': 28.512583, + 'O1': 13.943035, + 'P1': 14.958931, + 'Q1': 13.398661, + 'R2': 30.041067, + 'RHO': 13.471515, + 'S1': 15.0, + 'S2': 30.0, + 'S4': 60.0, + 'S6': 90.0, + 'SA': 0.0410686, + 'SSA': 0.0821373, + 'T2': 29.958933, +} + def convert_coords(coords): """Convert latitude coordinates to [-180, 180]. @@ -43,7 +88,6 @@ class TidalDBEnum(Enum): TIDAL_DB_ADCIRC = 1 - class OrbitVariables(object): def __init__(self): self.dh = 0.0 @@ -77,18 +121,37 @@ class TidalDB(object): day_t = [0.0, 31.0, 59.0, 90.0, 120.0, 151.0, 181.0, 212.0, 243.0, 273.0, 304.0, 334.0] pi180 = math.pi/180.0 - def __init__(self): + def __init__(self, model): self.orbit = OrbitVariables() + self.data = [] + self.resources = None + self._model = None + self.change_model(model) __metaclass__ = ABCMeta + @property + def model(self): + return self._model + + @model.setter + def model(self, value): + self.change_model(value) + + def change_model(self, model): + model = model.lower() + if model == 'tpxo7_2': + model = 'tpxo7' + if model != self._model: + self._model = model + self.resources = ResourceManager(self._model) + @abstractmethod - def get_components(self, a_constituents, a_points): + def get_components(self, locs, cons, positive_ph): pass - @abstractmethod def have_constituent(self, a_name): - pass + return a_name.upper() in self.resources.available_constituents() def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): """Get the nodal factor for specified constituents at a specified time. diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index 9499830..3898229 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -7,17 +7,14 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("-w", "--work_dir", default=os.getcwd(), - help="directory to save output and temporary files") - parser.add_argument("-a", "--adcirc_atlantic", default="", + parser.add_argument("-a", "--adcirc_atlantic", default=None, help="path to the ADCIRC Atlantic database") - parser.add_argument("-p", "--adcirc_pacific", default="", + parser.add_argument("-p", "--adcirc_pacific", default=None, help="path to the ADCIRC Pacific database") - parser.add_argument("-l", "--leprovost", default="", + parser.add_argument("-l", "--leprovost", default=None, help="path to the LeProvost database folder") args = vars(parser.parse_args()) - - work_dir = args["work_dir"] + adcirc_atlantic = args["adcirc_atlantic"] adcir_pacific = args["adcirc_pacific"] leprovost = args["leprovost"] @@ -39,23 +36,21 @@ all_points.extend(pacific) good_cons = ['M2', 'S2', 'N2', 'K1'] - # Create an ADCIRC database for the Atlantic locations. - ad_alantic_db = harmonica.adcirc_database.AdcircDB(work_dir, adcirc_atlantic, - harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NWAT) + # Create an ADCIRC database for the Atlantic locations (default). + ad_alantic_db = harmonica.adcirc_database.AdcircDB(adcirc_atlantic) # Create an ADCIRC database for the Pacific locations. - ad_pacific_db = harmonica.adcirc_database.AdcircDB(work_dir, adcir_pacific, - harmonica.adcirc_database.TidalDBAdcircEnum.TIDE_NEPAC) + ad_pacific_db = harmonica.adcirc_database.AdcircDB(adcir_pacific, "adcircnepac") # Create a LeProvost database for all locations. leprovost_db = harmonica.leprovost_database.LeProvostDB(leprovost) # Create a TPXO database for all locations. - tpxo_db = harmonica.tidal_constituents.Constituents() + tpxo_db = harmonica.tidal_constituents.Constituents('tpxo8') # Get nodal factor data from the ADCIRC and LeProvost tidal databases ad_al_nodal_factor = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) ad_pa_nodal_factor = ad_pacific_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) leprovost_nodal_factor = leprovost_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) - f = open(os.path.join(work_dir, "tidal_test.out"), "w") + f = open(os.path.join(os.getcwd(), "tidal_test.out"), "w") f.write("ADCIRC Atlantic nodal factor:\n") f.write(ad_al_nodal_factor.to_string() + "\n\n") f.write("ADCIRC Pacific nodal factor:\n") @@ -69,6 +64,7 @@ ad_atlantic_comps = ad_alantic_db.get_components(atlantic, good_cons) ad_pacific_comps = ad_pacific_db.get_components(pacific, good_cons) leprovost_comps = leprovost_db.get_components(all_points, good_cons) + tpxo_comps = tpxo_db.get_components(all_points, good_cons, True) f.write("ADCIRC Atlantic components:\n") for pt in ad_atlantic_comps.data: @@ -82,10 +78,12 @@ # Get tidal harmonic components for a single point using the TPXO tidal model. f.write("TPX0 components:\n") - for pt in all_points: - # Specify TPXO version when calling get_components() - components = tpxo_db.get_components(pt, 'tpxo8', good_cons, True) - f.write(components.data.to_string() + "\n\n") - f.flush() + for pt in tpxo_comps.data: + f.write(pt.to_string() + "\n\n") + #for pt in all_points: + # # Specify TPXO version when calling get_components() + # components = tpxo_db.get_components(pt, 'tpxo8', good_cons, True) + # f.write(components.data.to_string() + "\n\n") + # f.flush() f.close() \ No newline at end of file From a2f5abef803bd5215c5e5700c0c051a2766bf435 Mon Sep 17 00:00:00 2001 From: Blair Merrell Date: Thu, 4 Oct 2018 12:36:42 -0600 Subject: [PATCH 08/24] removed redundant have_constituent code, fixed doc strings, deleted unnecessary files --- harmonica/adcirc_database.py | 38 +++++--------------- harmonica/leprovost_database.py | 26 +++----------- harmonica/resource.py | 11 ++++-- harmonica/tidal_constituents.py | 27 +++++++++------ harmonica/tidal_database.py | 61 +++++++++++++++++++++++++++++---- 5 files changed, 94 insertions(+), 69 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index e652c97..3a2e792 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -15,9 +15,9 @@ class TidalDBAdcircEnum(Enum): """Enum for specifying the type of an ADCIRC database. - TIDE_NWAT = North West Atlantic database - TIDE_NEPAC = North East Pacific database - TIDE_NONE = Enum end - legacy from SMS port. + TIDE_NWAT: North West Atlantic database + TIDE_NEPAC: North East Pacific database + TIDE_NONE: Enum end - legacy from SMS port. """ TIDE_NWAT = 0 # North West Atlantic Tidal Database @@ -29,12 +29,8 @@ class AdcircDB(TidalDB): """The class for extracting tidal data, specifically amplitude and phases, from an ADCIRC database. Attributes: - work_path (str): The path of the working directory. exe_with_path (str): The path of the ADCIRC executable. db_region (:obj: `TidalDBAdcircEnum`): The type of database. - cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the ADCIRC database - data (:obj:`list` of :obj:`pandas.DataFrame`): List of the constituent component DataFrames with one - per point location requested from get_components(). Intended return value of get_components(). grid_no_path (str): Filename of *.grd file to use. harm_no_path (str): Filename of *.tdb file to use. temp_folder (str): Temporary folder to hold files in while running executables. @@ -43,16 +39,15 @@ class AdcircDB(TidalDB): """ def __init__(self, resource_dir=None, db_region="adcircnwat"): - """Get the amplitude and phase for the given constituents at the given points. + """Constructor for the ADCIRC tidal database extractor. Args: - work_path (str): The path of the working directory. - exe_with_path (str): The path of the ADCIRC executable. - db_region (:obj: `TidalDBAdcircEnum`): The db_region of database. + resource_dir (:obj:`str`, optional): Directory of the ADCIRC resources. If not provided will become a + subfolder of "data" in the harmonica package location. + db_region (:obj:`str`, optional): ADCIRC tidal database region. Valid options are 'adcircnwat' and\ + 'adcircnepac' """ - self.cons = ['M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'Q1', 'K2'] - if db_region.lower() == "adcircnwat": self.db_region = TidalDBAdcircEnum.TIDE_NWAT self.grid_no_path = 'ec2001.grd' @@ -99,7 +94,7 @@ def get_components(self, locs, cons=None, positive_ph=False): # pre-allocate the return value constituents = [] if not cons: - cons = self.cons # Get all constituents by default + cons = self.resources.available_constituents() # Get all constituents by default # Make sure point locations are valid lat/lon locs = convert_coords(locs) @@ -191,18 +186,3 @@ def get_components(self, locs, cons=None, positive_ph=False): os.rmdir(self.temp_folder) return self - - def have_constituent(self, a_name): - """Check if teh given constituent is supported by the ADCIRC tidal database. - - Args: - a_name (str): The name of the constituent to check. - - Returns: - True if the constituent is supported by ADCIRC tidal databases, False for unsupported. - - """ - if a_name.upper() in self.cons: - return True - else: - return False diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 03fdac0..6df6f67 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -16,23 +16,20 @@ class LeProvostDB(TidalDB): """Extractor class for the LeProvost tidal database. Attributes: - cons (:obj:`list` of :obj:`str`): List of the constituents that are valid for the LeProvost database resource_dir (str): Fully qualified path to a folder containing the LeProvost *.legi files - data (:obj:`list` of :obj:`pandas.DataFrame`): List of the constituent component DataFrames with one - per point location requested from get_components(). Intended return value of get_components(). """ def __init__(self, resource_dir=None): - """Constructor for database extractor + """Constructor for the LeProvost tidal database extractor. Args: - resource_dir (str): Fully qualified path to a folder containing the LeProvost *.legi files + resource_dir (:obj:`str`, optional): Fully qualified path to a folder containing the LeProvost *.legi files. + If not provided will be "harmonica/data/leprovost' where harmonica is the location of the pacakage. """ # self.debugger = open("debug.txt", "w") super().__init__("leprovost") self.resource_dir = self.resources.download_model(resource_dir) - self.cons = ['M2', 'S2', 'N2', 'K1', 'O1', 'NU2', 'MU2', '2N2', 'Q1', 'T2', 'P1', 'L2', 'K2'] def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed of specified constituents at specified point locations. @@ -54,7 +51,7 @@ def get_components(self, locs, cons=None, positive_ph=False): """ # If no constituents specified, extract all valid constituents. if not cons: - cons = self.cons + cons = self.resources.available_constituents() # Make sure point locations are valid lat/lon locs = convert_coords(locs) @@ -181,18 +178,3 @@ def get_components(self, locs, cons=None, positive_ph=False): self.data[i].loc[con] = [amp, phase, NOAA_SPEEDS[con]] return self - - def have_constituent(self, a_name): - """Checks if a constituent name is valid for the LeProvost tidal database - - Args: - a_name (str): Name of the constituent to check. - - Returns: - True if the constituent is valid for the LeProvost tidal database, False otherwise. - - """ - if a_name.upper() in self.cons: - return True - else: - return False diff --git a/harmonica/resource.py b/harmonica/resource.py index b2d67c1..46aea80 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -94,7 +94,8 @@ class ResourceManager(object): 'leprovost': { 'resource_atts': { 'url': "http://sms.aquaveo.com/ADCIRC_Essentials.zip", - 'archive': 'zip', # gzip compression + 'archive': 'zip', # zip compression + 'delete_files': ['w_mpi-rt_p_4.1.0.023.exe'], }, 'dataset_atts': { 'units_multiplier': 1., # meter @@ -118,7 +119,7 @@ class ResourceManager(object): 'adcircnwat': { 'resource_atts': { 'url': 'http://sms.aquaveo.com/adcircnwattides.zip', - 'archive': 'zip', # gzip compression + 'archive': 'zip', # zip compression }, 'dataset_atts': { 'units_multiplier': 1., # meter @@ -218,6 +219,12 @@ def download(self, resource, destination_dir): with open(path, 'wb') as f: f.write(response.read()) + if 'delete_files' in rsrc_atts: + for file in rsrc_atts['delete_files']: + deletefile = os.path.join(destination_dir, file) + if os.path.isfile(deletefile): + os.remove(deletefile) + return path def download_model(self, resource_dir=None): diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index d4a9738..fe811b2 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -10,6 +10,12 @@ class Constituents(TidalDB): """Harmonica tidal constituents.""" def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): + """Constructor for the TPXO tidal extractor. + + Args: + model (:obj:`str`, optional): The name of the TPXO model. One of: 'tpxo9', 'tpxo8', 'tpxo7'. + ResourceManager.DEFAULT_RESOURCE if not specified. + """ # constituent information dataframe: # amplitude (meters) # phase (degrees) @@ -17,21 +23,22 @@ def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): super().__init__(model) def get_components(self, locs, cons=None, positive_ph=False): - """Query the a tide model database and return amplitude, phase and speed for a location. - - Currently written to query tpxo7, tpxo8, and tpxo9 tide models. + """Get the amplitude, phase, and speed of specified constituents at specified point locations. Args: - loc (tuple(float, float)): latitude [-90, 90] and longitude [-180 180] or [0 360] of the requested point. - model (str, optional): Model name, defaults to 'tpxo8'. - cons (list(str), optional): List of constituents requested, defaults to all constituents if None or empty. + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] + of the requested points. + cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If + not supplied, all valid constituents will be extracted. positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or [-180 180] (False, the default). Returns: - A dataframe of constituent information including amplitude (meters), phase (degrees) and - speed (degrees/hour, UTC/GMT) - + :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including + amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, + where each element in the return list is the constituent data for the corresponding element in locs. + Empty list on error. Note that function uses fluent interface pattern. + """ self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] @@ -71,7 +78,7 @@ def get_components(self, locs, cons=None, positive_ph=False): (1. - dx) * dy, # w01 :: bottom right dx * (1. - dy), # w10 :: top left dx * dy # w11 :: top right - ]).reshape((2,2)) + ]).reshape((2, 2)) weights = weights / weights.sum() # devise the slice to subset surrounding values query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index 9388250..a614e7a 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -113,15 +113,25 @@ class TidalDB(object): """The base class for extracting tidal data from a database. Attributes: - attr2 (:obj:`list` of :obj:`float`): The starting days of the months (non-leap year). - pi180 (float): PI divided by 180.0. orbit (:obj:`OrbitVariables`): The orbit variables. + data (:obj:`list` of :obj:`pandas.DataFrame`): List of the constituent component DataFrames with one + per point location requested from get_components(). Intended return value of get_components(). + resources (:obj:`harmonica.resource.ResourceManager`): Manages fetching of tidal data """ + """(:obj:`list` of :obj:`float`): The starting days of the months (non-leap year).""" day_t = [0.0, 31.0, 59.0, 90.0, 120.0, 151.0, 181.0, 212.0, 243.0, 273.0, 304.0, 334.0] + """(float): PI divided by 180.0""" pi180 = math.pi/180.0 def __init__(self, model): + """Base class constructor for the tidal extractors + + Args: + model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or + 'adcircnepac' + + """ self.orbit = OrbitVariables() self.data = [] self.resources = None @@ -132,6 +142,10 @@ def __init__(self, model): @property def model(self): + """str: The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or 'adcircnepac' + + When setting the model to a different one than the current, required resources are downloaded. + """ return self._model @model.setter @@ -139,6 +153,13 @@ def model(self, value): self.change_model(value) def change_model(self, model): + """Change the extractor model, if different than the current, required resources are downloaded. + + Args: + model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or + 'adcircnepac' + + """ model = model.lower() if model == 'tpxo7_2': model = 'tpxo7' @@ -148,10 +169,36 @@ def change_model(self, model): @abstractmethod def get_components(self, locs, cons, positive_ph): + """Abstract method to get amplitude, phase, and speed of specified constituents at specified point locations. + + Args: + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] + of the requested points. + cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If + not supplied, all valid constituents will be extracted. + positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or + [-180 180] (False, the default). + + Returns: + :obj:`list` of :obj:`pandas.DataFrame`: Implementations should return a list of dataframes of constituent + information including amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is + parallel with locs, where each element in the return list is the constituent data for the corresponding + element in locs. Empty list on error. Note that function uses fluent interface pattern. + + """ pass - def have_constituent(self, a_name): - return a_name.upper() in self.resources.available_constituents() + def have_constituent(self, name): + """Determine if a constituent is valid for this tidal extractor. + + Args: + name (str): The name of the constituent. + + Returns: + bool: True if the constituent is valid, False otherwise + + """ + return name.upper() in self.resources.available_constituents() def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): """Get the nodal factor for specified constituents at a specified time. @@ -232,7 +279,8 @@ def get_eq_args(self, a_hour, a_day, a_month, a_year): self.nfacs(a_year, day_julian, hrm) self.gterms(a_year, day_julian, a_hour, hrm) - def angle(self, a_number): + @staticmethod + def angle(a_number): """Converts the angle to be within 0-360. Args: @@ -480,7 +528,8 @@ def gterms(self, a_year, a_day_julian, a_hour, a_hrm): for ih in range(0, 37): self.orbit.grterm[ih] = self.angle(self.orbit.grterm[ih]) - def arctan(self, a_top, a_bottom): + @staticmethod + def arctan(a_top, a_bottom): """Determine the arctangent and place in correct quadrant. Args: From 4dd9aba45ca95a8b9e96f911363262feec06efd7 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 4 Oct 2018 14:09:06 -0600 Subject: [PATCH 09/24] Fix docstrings and Sphinx errors, update tutorial --- doc/source/simple_python.rst | 18 ++++++---------- harmonica/adcirc_database.py | 10 ++++----- harmonica/leprovost_database.py | 2 +- harmonica/tidal_database.py | 37 +++++---------------------------- tutorials/python_api/test.py | 11 ++-------- 5 files changed, 19 insertions(+), 59 deletions(-) diff --git a/doc/source/simple_python.rst b/doc/source/simple_python.rst index 00b25ac..5da5fb1 100644 --- a/doc/source/simple_python.rst +++ b/doc/source/simple_python.rst @@ -21,7 +21,7 @@ Executing :file:`test.py` will fetch the required tidal resources from the Inter they do not already exist in the working directory. To specify existing resources use the following command line arguments: --l path Path to the LeProvost *.legi files +-l path Path to the LeProvost \*.legi files -a file Path and filename of the ADCIRC Northwest Atlantic executable -p file Path and filename of the ADCIRC Northeast Pacific executable @@ -39,32 +39,26 @@ degrees. Note that we have split locations in the Atlantic and Pacific. LeProvos TPXO support all ocean locations, but the ADCIRC database is restricted to one or the other. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 25-39 + :lines: 22-36 :lineno-match: The next section of code sets up the tidal harmonic extraction interface. For the ADCIRC extractors, the tidal region must be specified at construction. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 42-51 + :lines: 39-46 :lineno-match: The ADCIRC and LeProvost extractors can also provide frequencies, earth tidal reduction factors, amplitudes, nodal factors, and equilibrium arguments for specified constituents at a specified time. The next section of code demonstrates this. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 53-56 + :lines: 48-51 :lineno-match: -The next block uses the ADCIRC and LeProvost interfaces to extract tidal harmonic constituents for a list of +The last block uses the ADCIRC, LeProvost, and TPXO interfaces to extract tidal harmonic constituents for a list of locations and constituents. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 67-71 - :lineno-match: - -Finally, the TPXO interface extracts tidal harmonic constituents for a single location at a time. - -.. literalinclude:: ../../tutorials/python_api/test.py - :lines: 83-87 + :lines: 62-80 :lineno-match: \ No newline at end of file diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 3a2e792..f3e1ca5 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -30,9 +30,9 @@ class AdcircDB(TidalDB): Attributes: exe_with_path (str): The path of the ADCIRC executable. - db_region (:obj: `TidalDBAdcircEnum`): The type of database. - grid_no_path (str): Filename of *.grd file to use. - harm_no_path (str): Filename of *.tdb file to use. + db_region (:obj:`harmonica.adcirc_database.TidalDBAdcircEnum`): The type of database. + grid_no_path (str): Filename of \*.grd file to use. + harm_no_path (str): Filename of \*.tdb file to use. temp_folder (str): Temporary folder to hold files in while running executables. tide_in (str): Temporary 'tides.in' filename with path. tide_out (str): Temporary 'tides.out' filename with path. @@ -44,7 +44,7 @@ def __init__(self, resource_dir=None, db_region="adcircnwat"): Args: resource_dir (:obj:`str`, optional): Directory of the ADCIRC resources. If not provided will become a subfolder of "data" in the harmonica package location. - db_region (:obj:`str`, optional): ADCIRC tidal database region. Valid options are 'adcircnwat' and\ + db_region (:obj:`str`, optional): ADCIRC tidal database region. Valid options are 'adcircnwat' and 'adcircnepac' """ @@ -85,7 +85,7 @@ def get_components(self, locs, cons=None, positive_ph=False): [-180 180] (False, the default). Returns: - :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including + (:obj:`list` of :obj:`pandas.DataFrame`): A list of dataframes of constituent information including amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, where each element in the return list is the constituent data for the corresponding element in locs. Note that function uses fluent interface pattern. diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 6df6f67..cfdbad9 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -16,7 +16,7 @@ class LeProvostDB(TidalDB): """Extractor class for the LeProvost tidal database. Attributes: - resource_dir (str): Fully qualified path to a folder containing the LeProvost *.legi files + resource_dir (str): Fully qualified path to a folder containing the LeProvost \*.legi files """ def __init__(self, resource_dir=None): diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index a614e7a..b031e24 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -128,7 +128,7 @@ def __init__(self, model): """Base class constructor for the tidal extractors Args: - model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or + model (str): The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or 'adcircnepac' """ @@ -142,9 +142,10 @@ def __init__(self, model): @property def model(self): - """str: The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or 'adcircnepac' + """str: The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost', 'adcircnwat', or 'adcircnepac' When setting the model to a different one than the current, required resources are downloaded. + """ return self._model @@ -153,7 +154,7 @@ def model(self, value): self.change_model(value) def change_model(self, model): - """Change the extractor model, if different than the current, required resources are downloaded. + """Change the extractor model. If different than the current, required resources are downloaded. Args: model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or @@ -502,7 +503,7 @@ def gterms(self, a_year, a_day_julian, a_hour, a_hrm): pc = self.orbit.dpc*self.pi180 top = (5.0*math.cos(fi)-1.0)*math.sin(pc) bottom = (7.0*math.cos(fi)+1.0)*math.cos(pc) - q = self.arctan(top, bottom) + q = math.degrees(math.atan2(top, bottom)) self.orbit.grterm[17] = t-s+h-90.0+xi-nu+q self.orbit.grterm[18] = t+s+h-p-90.0-nu self.orbit.grterm[19] = s-p @@ -527,31 +528,3 @@ def gterms(self, a_year, a_day_julian, a_hour, a_hrm): self.orbit.grterm[36] = 2.0*(2.0*t-s+h)+2.0*(xi-nu) for ih in range(0, 37): self.orbit.grterm[ih] = self.angle(self.orbit.grterm[ih]) - - @staticmethod - def arctan(a_top, a_bottom): - """Determine the arctangent and place in correct quadrant. - - Args: - TODO Find out what these are - a_top: The first parameter. - a_bottom: The second parameter. - - Returns: - The arc tangent in degrees in the correct quadrant. - - """ - if a_bottom == 0.0: - if a_top < 0.0: - value = 270.0 - elif a_top == 0.0: - value = 0.0 - else: - value = 90.0 - return value - value = math.atan(a_top/a_bottom)*57.2957795 - if a_bottom < 0.0: - value += 180.0 - else: - value += 360.0 - return value diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index 3898229..38e4b61 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -59,8 +59,8 @@ f.write(leprovost_nodal_factor.to_string() + "\n\n") f.flush() - # Get tidal harmonic components for a list of points using the ADCIRC and - # LeProvost databases. + # Get tidal harmonic components for a list of points using the ADCIRC, + # LeProvost, and TPXO databases. ad_atlantic_comps = ad_alantic_db.get_components(atlantic, good_cons) ad_pacific_comps = ad_pacific_db.get_components(pacific, good_cons) leprovost_comps = leprovost_db.get_components(all_points, good_cons) @@ -75,15 +75,8 @@ f.write("LeProvost components:\n") for pt in leprovost_comps.data: f.write(pt.to_string() + "\n\n") - - # Get tidal harmonic components for a single point using the TPXO tidal model. f.write("TPX0 components:\n") for pt in tpxo_comps.data: f.write(pt.to_string() + "\n\n") - #for pt in all_points: - # # Specify TPXO version when calling get_components() - # components = tpxo_db.get_components(pt, 'tpxo8', good_cons, True) - # f.write(components.data.to_string() + "\n\n") - # f.flush() f.close() \ No newline at end of file From 20248e17ec62d6ae724a9b87ae94c7990d2d5fd6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 6 Oct 2018 19:05:03 -0600 Subject: [PATCH 10/24] Converted LeProvost legi files to a NetCDF format I think matches 2014 FES files. Use ResourceManager to load LeProvost NetCDF datasets. Changed LeProvost code to only load the values it needs from file instead of entire dataset. ADCIRC will get these TPXO unifications after its executables are converted. Sorted test script output for compare. LeProvost interface will be broken with this commit until the new NetCDF file is hosted. --- doc/source/simple_python.rst | 2 +- harmonica/leprovost_database.py | 223 +++++++++---------- harmonica/resource.py | 60 ++--- harmonica/tidal_constituents.py | 1 + tutorials/python_api/expected_tidal_test.out | 50 ++--- tutorials/python_api/test.py | 21 +- 6 files changed, 176 insertions(+), 181 deletions(-) diff --git a/doc/source/simple_python.rst b/doc/source/simple_python.rst index 5da5fb1..0a778e5 100644 --- a/doc/source/simple_python.rst +++ b/doc/source/simple_python.rst @@ -60,5 +60,5 @@ The last block uses the ADCIRC, LeProvost, and TPXO interfaces to extract tidal locations and constituents. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 62-80 + :lines: 62-85 :lineno-match: \ No newline at end of file diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index cfdbad9..d7624c9 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -5,7 +5,7 @@ """ import math -import os +import numpy import pandas as pd @@ -52,6 +52,8 @@ def get_components(self, locs, cons=None, positive_ph=False): # If no constituents specified, extract all valid constituents. if not cons: cons = self.resources.available_constituents() + else: # Be case-insensitive + cons = [con.upper() for con in cons] # Make sure point locations are valid lat/lon locs = convert_coords(locs) @@ -59,122 +61,107 @@ def get_components(self, locs, cons=None, positive_ph=False): return self # ERROR: Not in latitude/longitude self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] - - deg2rad = 1.0 / 180.0 * math.pi - rad2deg = 1.0 / deg2rad - # read the file for each constituent - for con in cons: - if self.have_constituent(con): - con = con.upper() - else: - continue - filename = os.path.join(self.resource_dir, self.resources.model_atts["consts"][0][con]) - with open(filename, 'r') as f: - # 30 columns in the file - lon_min, lon_max = map(float, f.readline().split()) - lat_min, lat_max = map(float, f.readline().split()) - d_lon, d_lat = map(float, f.readline().split()) - n_lon, n_lat = map(int, f.readline().split()) - undef_a, undef_p = map(float, f.readline().split()) - if undef_a != undef_p: - # there was an error, the undefined values should be the same - return [] - else: - undef = undef_a - all_lines = f.readlines() - g_amp = [[0.0 for _ in range(n_lat)] for _ in range(n_lon)] - g_pha = [[0.0 for _ in range(n_lat)] for _ in range(n_lon)] - cur_line = -1 - for lat in range(n_lat): - for lon in range(0, n_lon-1, 30): - cur_line += 1 - cur_val = 0 - line_vals = all_lines[cur_line].split() - for k in range(lon, lon+30): - g_amp[k][lat] = float(line_vals[cur_val]) - cur_val += 1 - cur_line += 1 - cur_val = 0 - line_vals = all_lines[cur_line].split() - for k in range(lon, lon+30): - g_pha[k][lat] = float(line_vals[cur_val]) - cur_val += 1 - - # Extract components for each point for this constituent - for i, pt in enumerate(locs): - y_lat = pt[0] - x_lon = pt[1] - if x_lon < 0.0: - x_lon = x_lon + 360.0 - if x_lon > 180.0: - x_lon = x_lon - 360.0 - ixlo = int((x_lon - lon_min) / d_lon) + 1 - xlonlo = lon_min + (ixlo - 1) * d_lon - ixhi = ixlo + 1 - if ixlo == n_lon: - ixhi = 1 - iylo = int((y_lat - lat_min) / d_lon) + 1 - ylatlo = lat_min + (iylo - 1) * d_lat - iyhi = iylo + 1 - ixlo -= 1 - ixhi -= 1 - iylo -= 1 - iyhi -= 1 - skip = False - - if (ixlo > n_lon or ixhi > n_lon or iyhi > n_lat or iylo > n_lat or ixlo < 0 or ixhi < 0 or iyhi < 0 or - iylo < 0): - skip = True - elif (g_amp[ixlo][iyhi] == undef and g_amp[ixhi][iyhi] == undef and g_amp[ixlo][iylo] == undef and - g_amp[ixhi][iylo] == undef): - skip = True - elif (g_pha[ixlo][iyhi] == undef and g_pha[ixhi][iyhi] == undef and g_pha[ixlo][iylo] == undef and - g_pha[ixhi][iylo] == undef): - skip = True - - if skip: - self.data[i].loc[con] = [0.0, 0.0, 0.0] - else: - xratio = (x_lon - xlonlo) / d_lon - yratio = (y_lat - ylatlo) / d_lat - xcos1 = g_amp[ixlo][iyhi] * math.cos(deg2rad * g_pha[ixlo][iyhi]) - xcos2 = g_amp[ixhi][iyhi] * math.cos(deg2rad * g_pha[ixhi][iyhi]) - xcos3 = g_amp[ixlo][iylo] * math.cos(deg2rad * g_pha[ixlo][iylo]) - xcos4 = g_amp[ixhi][iylo] * math.cos(deg2rad * g_pha[ixhi][iylo]) - xsin1 = g_amp[ixlo][iyhi] * math.sin(deg2rad * g_pha[ixlo][iyhi]) - xsin2 = g_amp[ixhi][iyhi] * math.sin(deg2rad * g_pha[ixhi][iyhi]) - xsin3 = g_amp[ixlo][iylo] * math.sin(deg2rad * g_pha[ixlo][iylo]) - xsin4 = g_amp[ixhi][iylo] * math.sin(deg2rad * g_pha[ixhi][iylo]) - - xcos = 0.0 - xsin = 0.0 - denom = 0.0 - if g_amp[ixlo][iyhi] != undef and g_pha[ixlo][iyhi] != undef: - xcos = xcos + xcos1 * (1.0 - xratio) * yratio - xsin = xsin + xsin1 * (1.0 - xratio) * yratio - denom = denom + (1.0 - xratio) * yratio - if g_amp[ixhi][iyhi] != undef and g_pha[ixhi][iyhi] != undef: - xcos = xcos + xcos2 * xratio * yratio - xsin = xsin + xsin2 * xratio * yratio - denom = denom + xratio * yratio - if g_amp[ixlo][iylo] != undef and g_pha[ixlo][iylo] != undef: - xcos = xcos + xcos3 * (1.0 - xratio) * (1 - yratio) - xsin = xsin + xsin3 * (1.0 - xratio) * (1 - yratio) - denom = denom + (1.0 - xratio) * (1.0 - yratio) - if g_amp[ixhi][iylo] != undef and g_pha[ixhi][iylo] != undef: - xcos = xcos + xcos4 * (1.0 - yratio) * xratio - xsin = xsin + xsin4 * (1.0 - yratio) * xratio - denom = denom + (1.0 - yratio) * xratio - - xcos = xcos / denom - xsin = xsin / denom - - amp = math.sqrt(xcos * xcos + xsin * xsin) - phase = rad2deg * math.acos(xcos / amp) - amp /= 100.0 - if xsin < 0.0: - phase = 360.0 - phase - phase += (360. if positive_ph and phase < 0 else 0) - self.data[i].loc[con] = [amp, phase, NOAA_SPEEDS[con]] + dataset_atts = self.resources.model_atts['dataset_atts'] + + n_lat = dataset_atts['num_lats'] + n_lon = dataset_atts['num_lons'] + lat_min = -90.0 + lon_min = -180.0 + d_lat = 180.0 / (n_lat - 1) + d_lon = 360.0 / n_lon + + for d in self.resources.get_datasets(cons): + nc_names = [x.strip().upper() for x in d.spectrum.values[0]] + for con in set(cons) & set(nc_names): + # Extract components for each point for this constituent + con_idx = nc_names.index(con) + for i, pt in enumerate(locs): + y_lat = pt[0] + x_lon = pt[1] + if x_lon < 0.0: + x_lon = x_lon + 360.0 + if x_lon > 180.0: + x_lon = x_lon - 360.0 + xlo = int((x_lon - lon_min) / d_lon) + 1 + xlonlo = lon_min + (xlo - 1) * d_lon + xhi = xlo + 1 + if xlo == n_lon: + xhi = 1 + ylo = int((y_lat - lat_min) / d_lon) + 1 + ylatlo = lat_min + (ylo - 1) * d_lat + yhi = ylo + 1 + xlo -= 1 + xhi -= 1 + ylo -= 1 + yhi -= 1 + skip = False + + # Make sure lat/lon coordinate is in the domain. + if (xlo > n_lon or xhi > n_lon or yhi > n_lat or ylo > n_lat or xlo < 0 or xhi < 0 or yhi < 0 + or ylo < 0): + skip = True + else: # Make sure we have at least one neighbor with an active amplitude value. + # Read potential contributing amplitudes from the file. + xlo_yhi_amp = d.Ha[0][con_idx][xlo][yhi] + xlo_ylo_amp = d.Ha[0][con_idx][xlo][ylo] + xhi_yhi_amp = d.Ha[0][con_idx][xhi][yhi] + xhi_ylo_amp = d.Ha[0][con_idx][xhi][ylo] + if (numpy.isnan(xlo_yhi_amp) and numpy.isnan(xhi_yhi_amp) and + numpy.isnan(xlo_ylo_amp) and numpy.isnan(xhi_ylo_amp)): + skip = True + else: # Make sure we have at least one neighbor with an active phase value. + # Read potential contributing phases from the file. + xlo_yhi_phase = d.Hg[0][con_idx][xlo][yhi] + xlo_ylo_phase = d.Hg[0][con_idx][xlo][ylo] + xhi_yhi_phase = d.Hg[0][con_idx][xhi][yhi] + xhi_ylo_phase = d.Hg[0][con_idx][xhi][ylo] + if (numpy.isnan(xlo_yhi_phase) and numpy.isnan(xhi_yhi_phase) and + numpy.isnan(xlo_ylo_phase) and numpy.isnan(xhi_ylo_phase)): + skip = True + + if skip: + self.data[i].loc[con] = [0.0, 0.0, 0.0] + else: + xratio = (x_lon - xlonlo) / d_lon + yratio = (y_lat - ylatlo) / d_lat + xcos1 = xlo_yhi_amp * math.cos(math.radians(xlo_yhi_phase)) + xcos2 = xhi_yhi_amp * math.cos(math.radians(xhi_yhi_phase)) + xcos3 = xlo_ylo_amp * math.cos(math.radians(xlo_ylo_phase)) + xcos4 = xhi_ylo_amp * math.cos(math.radians(xhi_ylo_phase)) + xsin1 = xlo_yhi_amp * math.sin(math.radians(xlo_yhi_phase)) + xsin2 = xhi_yhi_amp * math.sin(math.radians(xhi_yhi_phase)) + xsin3 = xlo_ylo_amp * math.sin(math.radians(xlo_ylo_phase)) + xsin4 = xhi_ylo_amp * math.sin(math.radians(xhi_ylo_phase)) + + xcos = 0.0 + xsin = 0.0 + denom = 0.0 + if not numpy.isnan(xlo_yhi_amp) and not numpy.isnan(xlo_yhi_phase): + xcos = xcos + xcos1 * (1.0 - xratio) * yratio + xsin = xsin + xsin1 * (1.0 - xratio) * yratio + denom = denom + (1.0 - xratio) * yratio + if not numpy.isnan(xhi_yhi_amp) and not numpy.isnan(xhi_yhi_phase): + xcos = xcos + xcos2 * xratio * yratio + xsin = xsin + xsin2 * xratio * yratio + denom = denom + xratio * yratio + if not numpy.isnan(xlo_ylo_amp) and not numpy.isnan(xlo_ylo_phase): + xcos = xcos + xcos3 * (1.0 - xratio) * (1 - yratio) + xsin = xsin + xsin3 * (1.0 - xratio) * (1 - yratio) + denom = denom + (1.0 - xratio) * (1.0 - yratio) + if not numpy.isnan(xhi_ylo_amp) and not numpy.isnan(xhi_ylo_phase): + xcos = xcos + xcos4 * (1.0 - yratio) * xratio + xsin = xsin + xsin4 * (1.0 - yratio) * xratio + denom = denom + (1.0 - yratio) * xratio + + xcos = xcos / denom + xsin = xsin / denom + + amp = math.sqrt(xcos * xcos + xsin * xsin) + phase = math.degrees(math.acos(xcos / amp)) + amp /= 100.0 + if xsin < 0.0: + phase = 360.0 - phase + phase += (360. if positive_ph and phase < 0 else 0) + self.data[i].loc[con] = [amp, phase, NOAA_SPEEDS[con]] return self diff --git a/harmonica/resource.py b/harmonica/resource.py index 46aea80..9b8c18b 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -11,7 +11,7 @@ class ResourceManager(object): """Harmonica resource manager to retrieve and access tide models""" - # Dictionay of model information + # Dictionary of model information RESOURCES = { 'tpxo9': { 'resource_atts': { @@ -19,9 +19,9 @@ class ResourceManager(object): 'archive': 'gz', }, 'dataset_atts': { - 'units_multiplier': 1., # meters + 'units_multiplier': 1., # meters }, - 'consts': [{ # grouped by dimensionally compatiable files + 'consts': [{ # grouped by dimensionally compatible files '2N2': 'tpxo9_netcdf/h_tpxo9.v1.nc', 'K1': 'tpxo9_netcdf/h_tpxo9.v1.nc', 'K2': 'tpxo9_netcdf/h_tpxo9.v1.nc', @@ -45,9 +45,9 @@ class ResourceManager(object): 'archive': None, }, 'dataset_atts': { - 'units_multiplier': 0.001, # mm to meter + 'units_multiplier': 0.001, # mm to meter }, - 'consts': [ # grouped by dimensionally compatiable files + 'consts': [ # grouped by dimensionally compatible files { # 1/30 degree 'K1': 'hf.k1_tpxo8_atlas_30c_v1.nc', 'K2': 'hf.k2_tpxo8_atlas_30c_v1.nc', @@ -70,12 +70,12 @@ class ResourceManager(object): 'tpxo7': { 'resource_atts': { 'url': "ftp://ftp.oce.orst.edu/dist/tides/Global/tpxo7.2_netcdf.tar.Z", - 'archive': 'gz', # gzip compression + 'archive': 'gz', # gzip compression }, 'dataset_atts': { - 'units_multiplier': 1., # meter + 'units_multiplier': 1., # meter }, - 'consts': [{ # grouped by dimensionally compatiable files + 'consts': [{ # grouped by dimensionally compatible files 'K1': 'DATA/h_tpxo7.2.nc', 'K2': 'DATA/h_tpxo7.2.nc', 'M2': 'DATA/h_tpxo7.2.nc', @@ -95,25 +95,27 @@ class ResourceManager(object): 'resource_atts': { 'url': "http://sms.aquaveo.com/ADCIRC_Essentials.zip", 'archive': 'zip', # zip compression - 'delete_files': ['w_mpi-rt_p_4.1.0.023.exe'], + 'delete_files': ['w_mpi-rt_p_4.1.0.023.exe'], # This is should go away when we host new NetCDF file. }, 'dataset_atts': { 'units_multiplier': 1., # meter + 'num_lats': 361, + 'num_lons': 720 }, 'consts': [{ # grouped by dimensionally compatible files - 'K1': 'LeProvost/K1.legi', - 'K2': 'LeProvost/K2.legi', - 'M2': 'LeProvost/M2.legi', - 'N2': 'LeProvost/N2.legi', - 'O1': 'LeProvost/O1.legi', - 'P1': 'LeProvost/P1.legi', - 'Q1': 'LeProvost/Q1.legi', - 'S2': 'LeProvost/S2.legi', - 'NU2': 'LeProvost/NU2.legi', - 'MU2': 'LeProvost/MU2.legi', - '2N2': 'LeProvost/2N2.legi', - 'T2': 'LeProvost/T2.legi', - 'L2': 'LeProvost/L2.legi', + 'K1': 'LeProvost/leprovost_tidal_db.nc', + 'K2': 'LeProvost/leprovost_tidal_db.nc', + 'M2': 'LeProvost/leprovost_tidal_db.nc', + 'N2': 'LeProvost/leprovost_tidal_db.nc', + 'O1': 'LeProvost/leprovost_tidal_db.nc', + 'P1': 'LeProvost/leprovost_tidal_db.nc', + 'Q1': 'LeProvost/leprovost_tidal_db.nc', + 'S2': 'LeProvost/leprovost_tidal_db.nc', + 'NU2': 'LeProvost/leprovost_tidal_db.nc', + 'MU2': 'LeProvost/leprovost_tidal_db.nc', + '2N2': 'LeProvost/leprovost_tidal_db.nc', + 'T2': 'LeProvost/leprovost_tidal_db.nc', + 'L2': 'LeProvost/leprovost_tidal_db.nc', }, ], }, 'adcircnwat': { @@ -122,7 +124,7 @@ class ResourceManager(object): 'archive': 'zip', # zip compression }, 'dataset_atts': { - 'units_multiplier': 1., # meter + 'units_multiplier': 1., # mete }, 'consts': [{ # grouped by dimensionally compatible files 'M2': 'adcircnwattides.exe', @@ -139,7 +141,7 @@ class ResourceManager(object): 'adcircnepac': { 'resource_atts': { 'url': 'http://sms.aquaveo.com/adcircnepactides.zip', - 'archive': 'zip', # gzip compression + 'archive': 'zip', # zip compression }, 'dataset_atts': { 'units_multiplier': 1., # meter @@ -166,7 +168,6 @@ def __init__(self, model=DEFAULT_RESOURCE): self.model_atts = self.RESOURCES[self.model] self.datasets = [] - def __del__(self): for d in self.datasets: d.close() @@ -202,7 +203,7 @@ def download(self, resource, destination_dir): print(str(e)) else: rsrcs = set(c for sl in [x.values() for x in self.model_atts['consts']] for c in sl) - tar.extractall(path = destination_dir, members = [m for m in tar.getmembers() if m.name in rsrcs]) + tar.extractall(path=destination_dir, members=[m for m in tar.getmembers() if m.name in rsrcs]) tar.close() elif rsrc_atts['archive'] == 'zip': # Unzip .zip files zip_file = os.path.join(destination_dir, os.path.basename(resource) + '.zip') @@ -219,11 +220,12 @@ def download(self, resource, destination_dir): with open(path, 'wb') as f: f.write(response.read()) + # This is should go away when we host new NetCDF file. if 'delete_files' in rsrc_atts: for file in rsrc_atts['delete_files']: - deletefile = os.path.join(destination_dir, file) - if os.path.isfile(deletefile): - os.remove(deletefile) + delete_file = os.path.join(destination_dir, file) + if os.path.isfile(delete_file): + os.remove(delete_file) return path diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index fe811b2..1294561 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -80,6 +80,7 @@ def get_components(self, locs, cons=None, positive_ph=False): dx * dy # w11 :: top right ]).reshape((2, 2)) weights = weights / weights.sum() + # TODO: This is slick. We should be doing this everywhere we can. # devise the slice to subset surrounding values query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] # calculate the weighted tide from real and imaginary components diff --git a/tutorials/python_api/expected_tidal_test.out b/tutorials/python_api/expected_tidal_test.out index 5a6271d..0aed68c 100644 --- a/tutorials/python_api/expected_tidal_test.out +++ b/tutorials/python_api/expected_tidal_test.out @@ -23,93 +23,93 @@ ADCIRC Atlantic components: amplitude phase speed K1 0.094224 174.103 15.041069 M2 0.574420 351.285 28.984104 -S2 0.116490 12.800 30.000000 N2 0.132220 337.764 28.439730 +S2 0.116490 12.800 30.000000 amplitude phase speed K1 0.10643 207.605 15.041069 M2 1.42770 114.565 28.984104 -S2 0.21213 150.550 30.000000 N2 0.29871 83.796 28.439730 +S2 0.21213 150.550 30.000000 amplitude phase speed K1 0.12156 198.872 15.041069 M2 1.99330 96.345 28.984104 -S2 0.29833 131.891 30.000000 N2 0.40195 66.496 28.439730 +S2 0.29833 131.891 30.000000 ADCIRC Pacific components: amplitude phase speed K1 0.44356 234.182 15.041069 M2 0.84280 221.629 28.984104 -S2 0.22670 245.825 30.000000 N2 0.17248 195.838 28.439730 +S2 0.22670 245.825 30.000000 amplitude phase speed K1 0.45602 238.807 15.041069 M2 0.94421 230.499 28.984104 -S2 0.26453 256.892 30.000000 N2 0.19271 205.061 28.439730 +S2 0.26453 256.892 30.000000 LeProvost components: amplitude phase speed +K1 0.080152 171.446949 15.041069 M2 0.589858 353.748502 28.984104 -S2 0.083580 20.950538 30.000000 N2 0.136323 337.950345 28.439730 -K1 0.080152 171.446949 15.041069 +S2 0.083580 20.950538 30.000000 amplitude phase speed +K1 0.0 0.0 0.0 M2 0.0 0.0 0.0 -S2 0.0 0.0 0.0 N2 0.0 0.0 0.0 -K1 0.0 0.0 0.0 +S2 0.0 0.0 0.0 amplitude phase speed +K1 0.0 0.0 0.0 M2 0.0 0.0 0.0 -S2 0.0 0.0 0.0 N2 0.0 0.0 0.0 -K1 0.0 0.0 0.0 +S2 0.0 0.0 0.0 amplitude phase speed +K1 0.428306 233.851748 15.041069 M2 0.770437 224.199371 28.984104 -S2 0.209673 249.043022 30.000000 N2 0.159378 202.381338 28.439730 -K1 0.428306 233.851748 15.041069 +S2 0.209673 249.043022 30.000000 amplitude phase speed +K1 0.441807 238.246609 15.041069 M2 0.858488 232.029922 28.984104 -S2 0.242000 258.787649 30.000000 N2 0.175845 210.476586 28.439730 -K1 0.441807 238.246609 15.041069 +S2 0.242000 258.787649 30.000000 TPX0 components: amplitude phase speed -S2 0.106576 18.615143 30.000000 -M2 0.560701 352.265551 28.984104 K1 0.092135 176.901563 15.041069 +M2 0.560701 352.265551 28.984104 N2 0.139185 346.750468 28.439730 +S2 0.106576 18.615143 30.000000 amplitude phase speed -S2 0.0 0.0 30.000000 -M2 0.0 0.0 28.984104 K1 0.0 0.0 15.041069 +M2 0.0 0.0 28.984104 N2 0.0 0.0 28.439730 +S2 0.0 0.0 30.000000 amplitude phase speed -S2 0.0 0.0 30.000000 -M2 0.0 0.0 28.984104 K1 0.0 0.0 15.041069 +M2 0.0 0.0 28.984104 N2 0.0 0.0 28.439730 +S2 0.0 0.0 30.000000 amplitude phase speed -S2 0.219652 246.510256 30.000000 -M2 0.809024 222.339913 28.984104 K1 0.412604 232.880147 15.041069 +M2 0.809024 222.339913 28.984104 N2 0.169892 196.376201 28.439730 +S2 0.219652 246.510256 30.000000 amplitude phase speed -S2 0.253107 260.181916 30.000000 -M2 0.910663 234.117743 28.984104 K1 0.428817 239.531238 15.041069 +M2 0.910663 234.117743 28.984104 N2 0.188528 206.717292 28.439730 +S2 0.253107 260.181916 30.000000 diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index 38e4b61..505ed38 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -62,21 +62,26 @@ # Get tidal harmonic components for a list of points using the ADCIRC, # LeProvost, and TPXO databases. ad_atlantic_comps = ad_alantic_db.get_components(atlantic, good_cons) - ad_pacific_comps = ad_pacific_db.get_components(pacific, good_cons) - leprovost_comps = leprovost_db.get_components(all_points, good_cons) - tpxo_comps = tpxo_db.get_components(all_points, good_cons, True) - f.write("ADCIRC Atlantic components:\n") for pt in ad_atlantic_comps.data: - f.write(pt.to_string() + "\n\n") + f.write(pt.sort_index().to_string() + "\n\n") + f.write("ADCIRC Pacific components:\n") + f.flush() + ad_pacific_comps = ad_pacific_db.get_components(pacific, good_cons) for pt in ad_pacific_comps.data: - f.write(pt.to_string() + "\n\n") + f.write(pt.sort_index().to_string() + "\n\n") + f.write("LeProvost components:\n") + f.flush() + leprovost_comps = leprovost_db.get_components(all_points, good_cons) for pt in leprovost_comps.data: - f.write(pt.to_string() + "\n\n") + f.write(pt.sort_index().to_string() + "\n\n") + f.write("TPX0 components:\n") + f.flush() + tpxo_comps = tpxo_db.get_components(all_points, good_cons, True) for pt in tpxo_comps.data: - f.write(pt.to_string() + "\n\n") + f.write(pt.sort_index().to_string() + "\n\n") f.close() \ No newline at end of file From ebacf9e1b6577fd05525f8865e8269accf7c5c09 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 8 Oct 2018 08:55:57 -0600 Subject: [PATCH 11/24] Removed unused 'resource_dir' variable from LeProvostDB. Transition to harmonica interface made it unnecessary. --- harmonica/leprovost_database.py | 17 ++--------------- tutorials/python_api/test.py | 5 +---- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index d7624c9..0a1382d 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -15,21 +15,13 @@ class LeProvostDB(TidalDB): """Extractor class for the LeProvost tidal database. - Attributes: - resource_dir (str): Fully qualified path to a folder containing the LeProvost \*.legi files - """ - def __init__(self, resource_dir=None): + def __init__(self): """Constructor for the LeProvost tidal database extractor. - Args: - resource_dir (:obj:`str`, optional): Fully qualified path to a folder containing the LeProvost *.legi files. - If not provided will be "harmonica/data/leprovost' where harmonica is the location of the pacakage. - """ # self.debugger = open("debug.txt", "w") super().__init__("leprovost") - self.resource_dir = self.resources.download_model(resource_dir) def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed of specified constituents at specified point locations. @@ -76,12 +68,7 @@ def get_components(self, locs, cons=None, positive_ph=False): # Extract components for each point for this constituent con_idx = nc_names.index(con) for i, pt in enumerate(locs): - y_lat = pt[0] - x_lon = pt[1] - if x_lon < 0.0: - x_lon = x_lon + 360.0 - if x_lon > 180.0: - x_lon = x_lon - 360.0 + y_lat, x_lon = pt # lat,lon not x,y xlo = int((x_lon - lon_min) / d_lon) + 1 xlonlo = lon_min + (xlo - 1) * d_lon xhi = xlo + 1 diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index 505ed38..79d3dba 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -11,13 +11,10 @@ help="path to the ADCIRC Atlantic database") parser.add_argument("-p", "--adcirc_pacific", default=None, help="path to the ADCIRC Pacific database") - parser.add_argument("-l", "--leprovost", default=None, - help="path to the LeProvost database folder") args = vars(parser.parse_args()) adcirc_atlantic = args["adcirc_atlantic"] adcir_pacific = args["adcirc_pacific"] - leprovost = args["leprovost"] # Need to be in (lat, lon), not (x, y) # Invert commented list declarations to test [-180, 180] vs. [0, 360] ranges. @@ -41,7 +38,7 @@ # Create an ADCIRC database for the Pacific locations. ad_pacific_db = harmonica.adcirc_database.AdcircDB(adcir_pacific, "adcircnepac") # Create a LeProvost database for all locations. - leprovost_db = harmonica.leprovost_database.LeProvostDB(leprovost) + leprovost_db = harmonica.leprovost_database.LeProvostDB() # Create a TPXO database for all locations. tpxo_db = harmonica.tidal_constituents.Constituents('tpxo8') From aab85802f5d2866ee79222da9ea20435f794aa72 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 8 Oct 2018 09:54:26 -0600 Subject: [PATCH 12/24] Removed unused delete_files attribute from ResourceManager --- harmonica/resource.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/harmonica/resource.py b/harmonica/resource.py index 9b8c18b..629bf87 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -95,7 +95,6 @@ class ResourceManager(object): 'resource_atts': { 'url': "http://sms.aquaveo.com/ADCIRC_Essentials.zip", 'archive': 'zip', # zip compression - 'delete_files': ['w_mpi-rt_p_4.1.0.023.exe'], # This is should go away when we host new NetCDF file. }, 'dataset_atts': { 'units_multiplier': 1., # meter @@ -220,13 +219,6 @@ def download(self, resource, destination_dir): with open(path, 'wb') as f: f.write(response.read()) - # This is should go away when we host new NetCDF file. - if 'delete_files' in rsrc_atts: - for file in rsrc_atts['delete_files']: - delete_file = os.path.join(destination_dir, file) - if os.path.isfile(delete_file): - os.remove(delete_file) - return path def download_model(self, resource_dir=None): @@ -253,7 +245,7 @@ def get_datasets(self, constituents): available = self.available_constituents() if any(const not in available for const in constituents): raise ValueError('Constituent not recognized.') - # handle compatiable files together + # handle compatible files together self.datasets = [] for const_group in self.model_atts['consts']: rsrcs = set(const_group[const] for const in set(constituents) & set(const_group)) From 33ce591dcf4ecf875d93568e7a11b5fc67318a22 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 8 Oct 2018 14:28:38 -0600 Subject: [PATCH 13/24] Added support for FES2014 NetCDF4 files. One file per constituent. We will keep the legacy constituents that we distribute all in one file. Made the format of our legacy NetCDF file more compatible with the newer format. --- doc/source/simple_python.rst | 6 +- harmonica/leprovost_database.py | 46 +++++++++---- harmonica/resource.py | 113 +++++++++++++++++++++++++++----- harmonica/tidal_constituents.py | 1 - harmonica/tidal_database.py | 16 +++-- tutorials/python_api/test.py | 9 +++ 6 files changed, 151 insertions(+), 40 deletions(-) diff --git a/doc/source/simple_python.rst b/doc/source/simple_python.rst index 0a778e5..84fa1f1 100644 --- a/doc/source/simple_python.rst +++ b/doc/source/simple_python.rst @@ -39,14 +39,14 @@ degrees. Note that we have split locations in the Atlantic and Pacific. LeProvos TPXO support all ocean locations, but the ADCIRC database is restricted to one or the other. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 22-36 + :lines: 19-33 :lineno-match: -The next section of code sets up the tidal harmonic extraction interface. For the ADCIRC +The next section of code sets up the tidal harmonic extraction interfaces. For the ADCIRC extractors, the tidal region must be specified at construction. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 39-46 + :lines: 36-43 :lineno-match: The ADCIRC and LeProvost extractors can also provide frequencies, earth tidal reduction factors, amplitudes, nodal factors, diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 0a1382d..a5b6c6e 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -6,6 +6,7 @@ import math import numpy +import os import pandas as pd @@ -16,12 +17,15 @@ class LeProvostDB(TidalDB): """Extractor class for the LeProvost tidal database. """ - def __init__(self): + def __init__(self, model="leprovost"): """Constructor for the LeProvost tidal database extractor. """ + # Make sure this is a valid LeProvost version ('leprovost' or 'fes2014') + if model.lower() not in ['leprovost', 'fes2014']: + raise ValueError("{} is not a supported LeProvost model. Must be 'leprovost' or 'fes2014'.") # self.debugger = open("debug.txt", "w") - super().__init__("leprovost") + super().__init__(model) def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed of specified constituents at specified point locations. @@ -48,7 +52,7 @@ def get_components(self, locs, cons=None, positive_ph=False): cons = [con.upper() for con in cons] # Make sure point locations are valid lat/lon - locs = convert_coords(locs) + locs = convert_coords(locs, self.model == "fes2014") if not locs: return self # ERROR: Not in latitude/longitude @@ -58,17 +62,24 @@ def get_components(self, locs, cons=None, positive_ph=False): n_lat = dataset_atts['num_lats'] n_lon = dataset_atts['num_lons'] lat_min = -90.0 - lon_min = -180.0 + lon_min = dataset_atts['min_lon'] d_lat = 180.0 / (n_lat - 1) d_lon = 360.0 / n_lon - for d in self.resources.get_datasets(cons): - nc_names = [x.strip().upper() for x in d.spectrum.values[0]] + for file_idx, d in enumerate(self.resources.get_datasets(cons)): + if self.model == 'leprovost': + nc_names = [x.strip().upper() for x in d.spectrum.values[0]] + else: + # TODO: Probably need to find a better way to get the constituent name. _file_obj is undocumented, so + # TODO: there is no guarantee this functionality will be maintained. + nc_names = [os.path.splitext(os.path.basename(dset.ds.filepath()))[0].upper() for + dset in d._file_obj.file_objs] for con in set(cons) & set(nc_names): # Extract components for each point for this constituent con_idx = nc_names.index(con) for i, pt in enumerate(locs): y_lat, x_lon = pt # lat,lon not x,y + print("y_lat, x_lon=({}, {})".format(y_lat, x_lon)) xlo = int((x_lon - lon_min) / d_lon) + 1 xlonlo = lon_min + (xlo - 1) * d_lon xhi = xlo + 1 @@ -88,20 +99,27 @@ def get_components(self, locs, cons=None, positive_ph=False): or ylo < 0): skip = True else: # Make sure we have at least one neighbor with an active amplitude value. + if self.model == 'leprovost': # All constituents in single file + amp_dset = d.amplitude[0] + phase_dset = d.phase[0] + else: # FES 2014 format - file per constituent + amp_dset = d.amplitude + phase_dset = d.phase + # Read potential contributing amplitudes from the file. - xlo_yhi_amp = d.Ha[0][con_idx][xlo][yhi] - xlo_ylo_amp = d.Ha[0][con_idx][xlo][ylo] - xhi_yhi_amp = d.Ha[0][con_idx][xhi][yhi] - xhi_ylo_amp = d.Ha[0][con_idx][xhi][ylo] + xlo_yhi_amp = amp_dset[con_idx][yhi][xlo] + xlo_ylo_amp = amp_dset[con_idx][ylo][xlo] + xhi_yhi_amp = amp_dset[con_idx][yhi][xhi] + xhi_ylo_amp = amp_dset[con_idx][ylo][xhi] if (numpy.isnan(xlo_yhi_amp) and numpy.isnan(xhi_yhi_amp) and numpy.isnan(xlo_ylo_amp) and numpy.isnan(xhi_ylo_amp)): skip = True else: # Make sure we have at least one neighbor with an active phase value. # Read potential contributing phases from the file. - xlo_yhi_phase = d.Hg[0][con_idx][xlo][yhi] - xlo_ylo_phase = d.Hg[0][con_idx][xlo][ylo] - xhi_yhi_phase = d.Hg[0][con_idx][xhi][yhi] - xhi_ylo_phase = d.Hg[0][con_idx][xhi][ylo] + xlo_yhi_phase = phase_dset[con_idx][yhi][xlo] + xlo_ylo_phase = phase_dset[con_idx][ylo][xlo] + xhi_yhi_phase = phase_dset[con_idx][yhi][xhi] + xhi_ylo_phase = phase_dset[con_idx][ylo][xhi] if (numpy.isnan(xlo_yhi_phase) and numpy.isnan(xhi_yhi_phase) and numpy.isnan(xlo_ylo_phase) and numpy.isnan(xhi_ylo_phase)): skip = True diff --git a/harmonica/resource.py b/harmonica/resource.py index 629bf87..41d5181 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -93,28 +93,77 @@ class ResourceManager(object): }, 'leprovost': { 'resource_atts': { - 'url': "http://sms.aquaveo.com/ADCIRC_Essentials.zip", + 'url': "https://filecloud.aquaveo.com/public.php/webdav", 'archive': 'zip', # zip compression }, 'dataset_atts': { 'units_multiplier': 1., # meter 'num_lats': 361, - 'num_lons': 720 + 'num_lons': 720, + 'min_lon': -180.0 }, 'consts': [{ # grouped by dimensionally compatible files - 'K1': 'LeProvost/leprovost_tidal_db.nc', - 'K2': 'LeProvost/leprovost_tidal_db.nc', - 'M2': 'LeProvost/leprovost_tidal_db.nc', - 'N2': 'LeProvost/leprovost_tidal_db.nc', - 'O1': 'LeProvost/leprovost_tidal_db.nc', - 'P1': 'LeProvost/leprovost_tidal_db.nc', - 'Q1': 'LeProvost/leprovost_tidal_db.nc', - 'S2': 'LeProvost/leprovost_tidal_db.nc', - 'NU2': 'LeProvost/leprovost_tidal_db.nc', - 'MU2': 'LeProvost/leprovost_tidal_db.nc', - '2N2': 'LeProvost/leprovost_tidal_db.nc', - 'T2': 'LeProvost/leprovost_tidal_db.nc', - 'L2': 'LeProvost/leprovost_tidal_db.nc', + 'K1': 'leprovost_tidal_db.nc', + 'K2': 'leprovost_tidal_db.nc', + 'M2': 'leprovost_tidal_db.nc', + 'N2': 'leprovost_tidal_db.nc', + 'O1': 'leprovost_tidal_db.nc', + 'P1': 'leprovost_tidal_db.nc', + 'Q1': 'leprovost_tidal_db.nc', + 'S2': 'leprovost_tidal_db.nc', + 'NU2': 'leprovost_tidal_db.nc', + 'MU2': 'leprovost_tidal_db.nc', + '2N2': 'leprovost_tidal_db.nc', + 'T2': 'leprovost_tidal_db.nc', + 'L2': 'leprovost_tidal_db.nc', + }, ], + }, + 'fes2014': { # Resources must already exist. Licensing restrictions prevent + 'resource_atts': { + 'url': None, + 'archive': None, + }, + 'dataset_atts': { + 'units_multiplier': 1., # meter + 'num_lats': 2881, + 'num_lons': 5760, + 'min_lon': 0.0 + }, + 'consts': [{ # grouped by dimensionally compatible files + '2N2': '2n2.nc', + 'EPS2': 'eps2.nc', + 'J1': 'j1.nc', + 'K1': 'k1.nc', + 'K2': 'k2.nc', + 'L2': 'l2.nc', + 'LA2': 'la2.nc', + 'M2': 'm2.nc', + 'M3': 'm3.nc', + 'M4': 'm4.nc', + 'M6': 'm6.nc', + 'M8': 'm8.nc', + 'MF': 'mf.nc', + 'MKS2': 'mks2.nc', + 'MM': 'mm.nc', + 'MN4': 'mn4.nc', + 'MS4': 'ms4.nc', + 'MSF': 'msf.nc', + 'MSQM': 'msqm.nc', + 'MTM': 'mtm.nc', + 'MU2': 'mu2.nc', + 'N2': 'n2.nc', + 'N4': 'n4.nc', + 'NU2': 'nu2.nc', + 'O1': 'o1.nc', + 'P1': 'p1.nc', + 'Q1': 'q1.nc', + 'R2': 'r2.nc', + 'S1': 's1.nc', + 'S2': 's2.nc', + 'S4': 's4.nc', + 'SA': 'sa.nc', + 'SSA': 'ssa.nc', + 'T2': 't2.nc', }, ], }, 'adcircnwat': { @@ -185,17 +234,49 @@ def download(self, resource, destination_dir): rsrc_atts = self.model_atts['resource_atts'] url = rsrc_atts['url'] + # Check if we can download resources for this model. + if url is None: + raise ValueError("Automatic fetching of resources is not available for the {} model.".format(self.model)) + if rsrc_atts['archive'] is None: url = "".join((url, resource)) print('Downloading resource: {}'.format(url)) + #path = os.path.join(destination_dir, resource) + #with urllib.request.urlopen(url) as response: + # if rsrc_atts['archive'] is not None: + # if rsrc_atts['archive'] == 'gz': + # import tarfile + # + # try: + # tar = tarfile.open(mode='r:{}'.format(rsrc_atts['archive']), fileobj=response) + # except IOError as e: + # print(str(e)) + # else: + # rsrcs = set(c for sl in [x.values() for x in self.model_atts['consts']] for c in sl) + # tar.extractall(path=destination_dir, members=[m for m in tar.getmembers() if m.name in rsrcs]) + # tar.close() + # elif rsrc_atts['archive'] == 'zip': # Unzip .zip files + # zip_file = os.path.join(destination_dir, os.path.basename(resource) + '.zip') + # with open(zip_file, 'wb') as out_file: + # shutil.copyfileobj(response, out_file) + # # Unzip the files + # print("Unzipping files to: {}".format(destination_dir)) + # with ZipFile(zip_file, 'r') as unzipper: + # # Extract all the files in the archive + # unzipper.extractall(path=destination_dir) + # print("Deleting zip file: {}".format(zip_file)) + # os.remove(zip_file) # delete the zip file + # else: + # with open(path, 'wb') as f: + # f.write(response.read()) + path = os.path.join(destination_dir, resource) with urllib.request.urlopen(url) as response: if rsrc_atts['archive'] is not None: if rsrc_atts['archive'] == 'gz': import tarfile - try: tar = tarfile.open(mode='r:{}'.format(rsrc_atts['archive']), fileobj=response) except IOError as e: diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index 1294561..fe811b2 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -80,7 +80,6 @@ def get_components(self, locs, cons=None, positive_ph=False): dx * dy # w11 :: top right ]).reshape((2, 2)) weights = weights / weights.sum() - # TODO: This is slick. We should be doing this everywhere we can. # devise the slice to subset surrounding values query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] # calculate the weighted tide from real and imaginary components diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index b031e24..80c7883 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -55,12 +55,14 @@ } -def convert_coords(coords): - """Convert latitude coordinates to [-180, 180]. +def convert_coords(coords, zero_to_360=False): + """Convert latitude coordinates to [-180, 180] or [0, 360]. Args: coords (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] of the requested point. + zero_to_360 (:obj:`bool`, optional) If True, coordinates will be converted to the [0, 360] range. If False, + coordinates will be converted to the [-180, 180] range. Returns: :obj:`list` of :obj:`tuple` of :obj:`float`: The list of converted coordinates. None if a coordinate out of @@ -72,10 +74,12 @@ def convert_coords(coords): y_lat = pt[0] x_lon = pt[1] if x_lon < 0.0: - x_lon = x_lon + 360.0 - if x_lon > 180.0: - x_lon = x_lon - 360.0 - if x_lon > 180.0 or x_lon < -180.0 or y_lat > 90.0 or y_lat < -90.0: + x_lon += 360.0 + if not zero_to_360 and x_lon > 180.0: + x_lon -= 360.0 + if y_lat > 90.0 or y_lat < -90.0 or ( # Invalid latitude + not zero_to_360 and (x_lon > 180.0 or x_lon < -180.0)) or ( # Invalid [-180, 180] + zero_to_360 and (x_lon > 360.0 or x_lon < 0.0)): # Invalid [0, 360] # ERROR: Not in latitude/longitude return None else: diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index 79d3dba..b5b9a8b 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -41,6 +41,9 @@ leprovost_db = harmonica.leprovost_database.LeProvostDB() # Create a TPXO database for all locations. tpxo_db = harmonica.tidal_constituents.Constituents('tpxo8') + # Create a FES2014 database for all locations. License restrictions prevent us + # from distributing the FES2014 resources. Must have local files already to use. + # fes2014_db = harmonica.leprovost_database.LeProvostDB('fes2014') # Get nodal factor data from the ADCIRC and LeProvost tidal databases ad_al_nodal_factor = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) @@ -81,4 +84,10 @@ for pt in tpxo_comps.data: f.write(pt.sort_index().to_string() + "\n\n") + # f.write("FES2014 components:\n") + # f.flush() + # fes2014_comps = fes2014_db.get_components(all_points, good_cons) + # for pt in fes2014_comps.data: + # f.write(pt.sort_index().to_string() + "\n\n") + f.close() \ No newline at end of file From 1f88e4a838ab345a360be4fb3a557a9bd81e4fa7 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 8 Oct 2018 14:53:58 -0600 Subject: [PATCH 14/24] Updated LeProvost download link to point at the new NetCDF4 version. Currently is bypassing certificate verification until the link works properly. --- harmonica/resource.py | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/harmonica/resource.py b/harmonica/resource.py index 41d5181..6ce345c 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -1,5 +1,6 @@ import os import shutil +import ssl import urllib.request from zipfile import ZipFile @@ -93,7 +94,7 @@ class ResourceManager(object): }, 'leprovost': { 'resource_atts': { - 'url': "https://filecloud.aquaveo.com/public.php/webdav", + 'url': 'https://sms.aquaveo.com.s3.amazonaws.com/leprovost_tidal_db.zip', 'archive': 'zip', # zip compression }, 'dataset_atts': { @@ -243,37 +244,12 @@ def download(self, resource, destination_dir): print('Downloading resource: {}'.format(url)) - #path = os.path.join(destination_dir, resource) - #with urllib.request.urlopen(url) as response: - # if rsrc_atts['archive'] is not None: - # if rsrc_atts['archive'] == 'gz': - # import tarfile - # - # try: - # tar = tarfile.open(mode='r:{}'.format(rsrc_atts['archive']), fileobj=response) - # except IOError as e: - # print(str(e)) - # else: - # rsrcs = set(c for sl in [x.values() for x in self.model_atts['consts']] for c in sl) - # tar.extractall(path=destination_dir, members=[m for m in tar.getmembers() if m.name in rsrcs]) - # tar.close() - # elif rsrc_atts['archive'] == 'zip': # Unzip .zip files - # zip_file = os.path.join(destination_dir, os.path.basename(resource) + '.zip') - # with open(zip_file, 'wb') as out_file: - # shutil.copyfileobj(response, out_file) - # # Unzip the files - # print("Unzipping files to: {}".format(destination_dir)) - # with ZipFile(zip_file, 'r') as unzipper: - # # Extract all the files in the archive - # unzipper.extractall(path=destination_dir) - # print("Deleting zip file: {}".format(zip_file)) - # os.remove(zip_file) # delete the zip file - # else: - # with open(path, 'wb') as f: - # f.write(response.read()) - path = os.path.join(destination_dir, resource) - with urllib.request.urlopen(url) as response: + # TODO: We probably don't want to disable SSL certificate verification. Don't pass in a SSL context once + # TODO: the link on the Aquaveo website works without bypassing certificate verification. + ctx = ssl.create_default_context() + ctx.check_hostname = False + with urllib.request.urlopen(url, context=ctx) as response: if rsrc_atts['archive'] is not None: if rsrc_atts['archive'] == 'gz': import tarfile From 828b75343afd9fbd35de1f48d178c9b953ed4c3b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 9 Oct 2018 09:11:22 -0600 Subject: [PATCH 15/24] Update LeProvost resource URL --- harmonica/resource.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/harmonica/resource.py b/harmonica/resource.py index 6ce345c..8754fe1 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -1,6 +1,5 @@ import os import shutil -import ssl import urllib.request from zipfile import ZipFile @@ -94,7 +93,7 @@ class ResourceManager(object): }, 'leprovost': { 'resource_atts': { - 'url': 'https://sms.aquaveo.com.s3.amazonaws.com/leprovost_tidal_db.zip', + 'url': 'http://sms.aquaveo.com/leprovost_tidal_db.zip', 'archive': 'zip', # zip compression }, 'dataset_atts': { @@ -245,11 +244,7 @@ def download(self, resource, destination_dir): print('Downloading resource: {}'.format(url)) path = os.path.join(destination_dir, resource) - # TODO: We probably don't want to disable SSL certificate verification. Don't pass in a SSL context once - # TODO: the link on the Aquaveo website works without bypassing certificate verification. - ctx = ssl.create_default_context() - ctx.check_hostname = False - with urllib.request.urlopen(url, context=ctx) as response: + with urllib.request.urlopen(url) as response: if rsrc_atts['archive'] is not None: if rsrc_atts['archive'] == 'gz': import tarfile From 2ab1b351aa0c258ba7ac96acf794d43b73f89087 Mon Sep 17 00:00:00 2001 From: acreer-aquaveo Date: Wed, 10 Oct 2018 18:01:19 -0600 Subject: [PATCH 16/24] Added changes for an updated ADCIRC tidal database. This includes moving to a netcdf file for the source of the tidal data. Also, removed a print statement from the LeProvost tidal database extractor. --- harmonica/adcirc_database.py | 308 ++++++++++++++++++++++---------- harmonica/leprovost_database.py | 1 - harmonica/resource.py | 72 +++++--- 3 files changed, 266 insertions(+), 115 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index f3e1ca5..8584217 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -1,6 +1,7 @@ #! python3 from enum import Enum +import math import os import subprocess import random @@ -8,6 +9,10 @@ import shutil import pandas as pd +import xarray as xr +import xmsinterp_py + +from harmonica import config from .tidal_database import convert_coords, NOAA_SPEEDS, TidalDB @@ -50,8 +55,8 @@ def __init__(self, resource_dir=None, db_region="adcircnwat"): """ if db_region.lower() == "adcircnwat": self.db_region = TidalDBAdcircEnum.TIDE_NWAT - self.grid_no_path = 'ec2001.grd' - self.harm_no_path = 'ec2001.tdb' + self.grid_no_path = 'ec2012_v3d_chk.grd' + self.harm_no_path = 'ec2012_v3d_otis3_fort.53' elif db_region.lower() == "adcircnepac": self.db_region = TidalDBAdcircEnum.TIDE_NEPAC self.grid_no_path = 'enpac2003.grd' @@ -60,18 +65,20 @@ def __init__(self, resource_dir=None, db_region="adcircnwat"): raise ValueError('unrecognized ADCIRC database region.') super().__init__(db_region.lower()) resource_dir = self.resources.download_model(resource_dir) - self.exe_with_path = os.path.join(resource_dir, self.resources.model_atts["consts"][0]["M2"]) + # self.db_with_path = os.path.join(resource_dir, self.resources.model_atts["consts"][0]["M2"]) + self.grid_with_path = os.path.join(resource_dir, self.grid_no_path) + self.harm_with_path = os.path.join(resource_dir, self.harm_no_path) # Build the temp working folder name - src_list = list(string.ascii_uppercase + string.digits) - rand_str = random.choice(src_list) - self.temp_folder = os.path.join(resource_dir, '_'.join(rand_str)) - # check that the folder does not exist - while os.path.isdir(self.temp_folder): - rand_str = random.choice(src_list) - self.temp_folder = self.temp_folder + rand_str - self.tide_in = os.path.join(self.temp_folder, 'tides.in') - self.tide_out = os.path.join(self.temp_folder, 'tides.out') + # src_list = list(string.ascii_uppercase + string.digits) + # rand_str = random.choice(src_list) + # self.temp_folder = os.path.join(resource_dir, '_'.join(rand_str)) + # # check that the folder does not exist + # while os.path.isdir(self.temp_folder): + # rand_str = random.choice(src_list) + # self.temp_folder = self.temp_folder + rand_str + # self.tide_in = os.path.join(self.temp_folder, 'tides.in') + # self.tide_out = os.path.join(self.temp_folder, 'tides.out') def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed for the given constituents at the given points. @@ -92,9 +99,10 @@ def get_components(self, locs, cons=None, positive_ph=False): """ # pre-allocate the return value - constituents = [] if not cons: cons = self.resources.available_constituents() # Get all constituents by default + else: + cons = [con.upper() for con in cons] # Make sure point locations are valid lat/lon locs = convert_coords(locs) @@ -102,87 +110,203 @@ def get_components(self, locs, cons=None, positive_ph=False): return self # ERROR: Not in latitude/longitude self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + + # Step 1: read the file and get geometry: + con_dsets = self.resources.get_datasets(cons)[0] + #con_dsets.element + #con_dsets.x + #con_dsets.y + tri_search = xmsinterp_py.geometry.GmTriSearch() + print(str(len(con_dsets.x[0]))) + con_x = con_dsets.x[0].values + con_y = con_dsets.y[0].values + #for idx in range(len(con_dsets.x[0])): + # print("idx: {:10} X: {:8f} Y: {:8f}".format(idx, float(con_x[idx]), float(con_y[idx]))) + # for idx, (the_x, the_y) in enumerate(zip(con_x, con_y)): + # print("idx: {:10} X: {:8f} Y: {:8f}".format(idx, the_x, the_y)) + # exit() + mesh_pts = [(float(con_y[idx]), float(con_x[idx]), 0.0) for idx in range(len(con_x))] + print("Built mesh_pts") + tri_list = con_dsets.element.values.flatten().tolist() + print("len: {} {} {}".format(len(mesh_pts), len(con_dsets.element[0]), len(tri_list))) + # print(str(mesh_pts[:10])) + tri_search.tris_to_search(mesh_pts, tri_list) + points_and_weights = [] + elem_dset = con_dsets.element[0] + max_tri = len(elem_dset) + for pt in locs: + tri_idx = tri_search.tri_containing_pt(pt) + if tri_idx != -1 and tri_idx < max_tri: + print("Point found! {} {}".format(pt, tri_idx)) + # print("eLen: {}".format(type(elem_dset[tri_idx][0]))) + pt_1 = int(elem_dset[tri_idx][0]) + pt_2 = int(elem_dset[tri_idx][1]) + pt_3 = int(elem_dset[tri_idx][2]) + x1, y1, z1 = mesh_pts[pt_1] + x2, y2, z2 = mesh_pts[pt_2] + x3, y3, z3 = mesh_pts[pt_3] + x = pt[0] + y = pt[1] + s1 = abs((x2*y3-x3*y2)-(x*y3-x3*y)+(x*y2-x2*y)) + s2 = abs((x*y3-x3*y)-(x1*y3-x3*y1)+(x1*y-x*y1)) + s3 = abs((x2*y-x*y2)-(x1*y-x*y1)+(x1*y2-x2*y1)) + ta = abs((x2*y3-x3*y2)-(x1*y3-x3*y1)+(x1*y2-x2*y1)) + w1 = ((x-x3)*(y2-y3)+(x2-x3)*(y3-y))/ta + w2 = ((x-x1)*(y3-y1)-(y-y1)*(x3-x1))/ta + w3 = ((y-y1)*(x2-x1)-(x-x1)*(y2-y1))/ta + points_and_weights.append(((pt_1, pt_2, pt_3), (w1, w2, w3))) + else: + print("Not found! {}".format(pt)) + for con in cons: - if self.have_constituent(con): - con = con.lower() - constituents.append(con) + con_amp_name = con + "_amplitude" + con_pha_name = con + "_phase" + for i, (pts, weights) in enumerate(points_and_weights): + # print("AMP: {} {}".format(con_amp_name, con_dsets.keys())) + con_amp = con_dsets[con_amp_name][0] + con_pha = con_dsets[con_pha_name][0] + # print("pts0: {} {}".format(pts[0], len(con_amp))) + amp1 = con_amp[pts[0]] + amp2 = con_amp[pts[1]] + amp3 = con_amp[pts[2]] + pha1 = con_pha[pts[0]] + pha2 = con_pha[pts[1]] + pha3 = con_pha[pts[2]] + + self.data[i].loc[con] = [0.0, 0.0, 0.0] + C1R=amp1*math.cos(pha1) + C1I=amp1*math.sin(pha1) + C2R=amp2*math.cos(pha2) + C2I=amp2*math.sin(pha2) + C3R=amp3*math.cos(pha3) + C3I=amp3*math.sin(pha3) + CTR=C1R*weights[0]+C2R*weights[1]+C3R*weights[2] + CTI=C1I*weights[0]+C2I*weights[1]+C3I*weights[2] + + new_amp = math.sqrt(CTR*CTR+CTI*CTI) + self.data[i].loc[con]['amplitude'] = new_amp + if new_amp == 0.0: + new_phase = 0.0 + else: + new_phase = 180.0 * math.acos(CTR / new_amp) / math.pi + if CTI < 0.0: + new_phase = 360.0 - new_phase + self.data[i].loc[con]['phase'] = new_phase + + # con_dset_file = self.resources[self.model]["consts"][cons[0]] + # if config['pre_existing_data_dir']: + # path = os.path.join(config['pre_existing_data_dir'], self.model, con_dset_file) + # else: + # path = os.path.join(config['data_dir'], self.model, con_dset_file) + # geom = xr.open_dataset(path, group='element') + # pt_x = xr.open_dataset(path, group='x') + # pt_y = xr.open_dataset(path, group='y') + # df = element.to_dataframe() + # all_grid = pd.read_table(self.grid_with_path, delim_whitespace=True, header=None, skiprows=skiprows, + # names=('row_type', 'cmp1', 'cmp2', 'cmp3', 'val'), index_col=1) + # conns = all_grid[all_grid['row_type'].str.lower() == 'e3t'][['cmp1', 'cmp2', 'cmp3']].values.astype(int) - 1 + # pts = all_grid[all_grid['row_type'].str.lower() == 'nd'][['cmp1', 'cmp2', 'cmp3']].values.astype(float) + # pts[:, 2] *= -1 + # verts = pd.DataFrame(pts, columns=['x', 'y', 'z']) + # tris = pd.DataFrame(conns, columns=['v0', 'v1', 'v2']) + + # convert to list of tuples: + # subset = data_set[['data_date', 'data_1', 'data_2']] + # tuples = [tuple(x) for x in subset.values] + + # feed to xmsinterp: + # element and weights (maybe, if not compute) + + # read the harmonic files: + # use pandas + # with open(self.harm_with_path) as f: + # num_cons = int(f.readline()) + # for i in range(num_cons): + # con_harm = pd.read_table(self.harm_with_path, delim_whitespace=True, header=None, skiprows=lambda x: x < num_cons+2 and x % i, + # names=('cmp1', 'cmp2')) + + # find the values at the point locations: + # ComputeHarmonics + + # return as a list + # create the temp directory - os.makedirs(self.temp_folder) - os.chdir(self.temp_folder) - try: - # write tides.in into the temp directory - with open(self.tide_in, 'w') as f: - f.write("{}\n".format(str(len(locs)))) - for pt in locs: - # 15.10f - f.write("{:15.10f}{:15.10f}\n".format(pt[1], pt[0])) - # copy the executable and .grd and .tdb file to the temp folder - temp_exe_with_path = os.path.join(self.temp_folder, os.path.basename(self.exe_with_path)) - temp_grd_with_path = os.path.join(self.temp_folder, self.grid_no_path) - temp_tdb_with_path = os.path.join(self.temp_folder, self.harm_no_path) - - old_exe_path = os.path.dirname(self.exe_with_path) - old_grd_with_path = os.path.join(old_exe_path, self.grid_no_path) - old_tdb_with_path = os.path.join(old_exe_path, self.harm_no_path) - - shutil.copyfile(self.exe_with_path, temp_exe_with_path) - shutil.copyfile(old_grd_with_path, temp_grd_with_path) - shutil.copyfile(old_tdb_with_path, temp_tdb_with_path) - # run the executable - subprocess.run([temp_exe_with_path]) - # read tides.out from the temp directory - with open(self.tide_out, 'r') as f: - all_lines = f.readlines() - last_name_line = 0 - is_first = True - used_constituent = False - con_name = '' - column_count = 0 - curr_pt = 0 - for count, line in enumerate(all_lines): - line = line.strip() - if not line: - continue - elif any(not(c.isdigit() or c.isspace() or c == 'e' or c == 'E' - or c == '.' or c == '-' or c == '+') for c in line): - last_name_line = count - is_first = True - curr_pt = 0 - continue - elif is_first: - con_name = all_lines[last_name_line].strip().lower() - is_first = False - if any(con_name in name for name in constituents): - used_constituent = True - else: - used_constituent = False - - if used_constituent: - if column_count == 0: - line_nums = [float(num) for num in line.split()] - column_count = len(line_nums) - if column_count == 2: - amp, pha = map(float, line.split()) - elif column_count == 4: - junk, junk, amp, pha = map(float, line.split()) - elif column_count == 6: - amp, pha, junk, junk, junk, junk = map(float, line.split()) - elif column_count == 8: - junk, junk, amp, pha, junk, junk, junk, junk = map(float, line.split()) - else: - # we have a problem - continue - self.data[curr_pt].loc[con_name.upper()] = [amp, - pha + (360.0 if positive_ph and pha < 0 else 0), - NOAA_SPEEDS[con_name.upper()]] - curr_pt += 1 - finally: - # delete the temp directory - del_files = os.listdir(self.temp_folder) - for del_file in del_files: - os.remove(del_file) - os.chdir(os.path.dirname(self.temp_folder)) - os.rmdir(self.temp_folder) + # os.makedirs(self.temp_folder) + # os.chdir(self.temp_folder) + # try: + # # write tides.in into the temp directory + # with open(self.tide_in, 'w') as f: + # f.write("{}\n".format(str(len(locs)))) + # for pt in locs: + # # 15.10f + # f.write("{:15.10f}{:15.10f}\n".format(pt[1], pt[0])) + # # copy the executable and .grd and .tdb file to the temp folder + # temp_exe_with_path = os.path.join(self.temp_folder, os.path.basename(self.exe_with_path)) + # temp_grd_with_path = os.path.join(self.temp_folder, self.grid_no_path) + # temp_tdb_with_path = os.path.join(self.temp_folder, self.harm_no_path) + + # old_exe_path = os.path.dirname(self.exe_with_path) + # old_grd_with_path = os.path.join(old_exe_path, self.grid_no_path) + # old_tdb_with_path = os.path.join(old_exe_path, self.harm_no_path) + + # shutil.copyfile(self.exe_with_path, temp_exe_with_path) + # shutil.copyfile(old_grd_with_path, temp_grd_with_path) + # shutil.copyfile(old_tdb_with_path, temp_tdb_with_path) + # # run the executable + # subprocess.run([temp_exe_with_path]) + # # read tides.out from the temp directory + # with open(self.tide_out, 'r') as f: + # all_lines = f.readlines() + # last_name_line = 0 + # is_first = True + # used_constituent = False + # con_name = '' + # column_count = 0 + # curr_pt = 0 + # for count, line in enumerate(all_lines): + # line = line.strip() + # if not line: + # continue + # elif any(not(c.isdigit() or c.isspace() or c == 'e' or c == 'E' + # or c == '.' or c == '-' or c == '+') for c in line): + # last_name_line = count + # is_first = True + # curr_pt = 0 + # continue + # elif is_first: + # con_name = all_lines[last_name_line].strip().lower() + # is_first = False + # if any(con_name in name for name in constituents): + # used_constituent = True + # else: + # used_constituent = False + + # if used_constituent: + # if column_count == 0: + # line_nums = [float(num) for num in line.split()] + # column_count = len(line_nums) + # if column_count == 2: + # amp, pha = map(float, line.split()) + # elif column_count == 4: + # junk, junk, amp, pha = map(float, line.split()) + # elif column_count == 6: + # amp, pha, junk, junk, junk, junk = map(float, line.split()) + # elif column_count == 8: + # junk, junk, amp, pha, junk, junk, junk, junk = map(float, line.split()) + # else: + # # we have a problem + # continue + # self.data[curr_pt].loc[con_name.upper()] = [amp, + # pha + (360.0 if positive_ph and pha < 0 else 0), + # NOAA_SPEEDS[con_name.upper()]] + # curr_pt += 1 + # finally: + # # delete the temp directory + # del_files = os.listdir(self.temp_folder) + # for del_file in del_files: + # os.remove(del_file) + # os.chdir(os.path.dirname(self.temp_folder)) + # os.rmdir(self.temp_folder) return self diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index a5b6c6e..415d805 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -79,7 +79,6 @@ def get_components(self, locs, cons=None, positive_ph=False): con_idx = nc_names.index(con) for i, pt in enumerate(locs): y_lat, x_lon = pt # lat,lon not x,y - print("y_lat, x_lon=({}, {})".format(y_lat, x_lon)) xlo = int((x_lon - lon_min) / d_lon) + 1 xlonlo = lon_min + (xlo - 1) * d_lon xhi = xlo + 1 diff --git a/harmonica/resource.py b/harmonica/resource.py index 8754fe1..93f5ee8 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -168,42 +168,70 @@ class ResourceManager(object): }, 'adcircnwat': { 'resource_atts': { - 'url': 'http://sms.aquaveo.com/adcircnwattides.zip', - 'archive': 'zip', # zip compression + 'url': '', + 'archive': 'tar', # tar compression }, 'dataset_atts': { - 'units_multiplier': 1., # mete + 'units_multiplier': 1., # meter }, 'consts': [{ # grouped by dimensionally compatible files - 'M2': 'adcircnwattides.exe', - 'S2': 'adcircnwattides.exe', - 'N2': 'adcircnwattides.exe', - 'K1': 'adcircnwattides.exe', - 'M4': 'adcircnwattides.exe', - 'O1': 'adcircnwattides.exe', - 'M6': 'adcircnwattides.exe', - 'Q1': 'adcircnwattides.exe', - 'K2': 'adcircnwattides.exe', + 'M2': 'all_adcirc.nc', + 'S2': 'all_adcirc.nc', + 'N2': 'all_adcirc.nc', + 'K1': 'all_adcirc.nc', + 'M4': 'all_adcirc.nc', + 'O1': 'all_adcirc.nc', + 'M6': 'all_adcirc.nc', + 'Q1': 'all_adcirc.nc', + 'K2': 'all_adcirc.nc', + 'L2': 'all_adcirc.nc', + '2N2': 'all_adcirc.nc', + 'R2': 'all_adcirc.nc', + 'T2': 'all_adcirc.nc', + 'LAMBDA2': 'all_adcirc.nc', + 'MU2': 'all_adcirc.nc', + 'NU2': 'all_adcirc.nc', + 'J1': 'all_adcirc.nc', + 'M1': 'all_adcirc.nc', + 'OO1': 'all_adcirc.nc', + 'P1': 'all_adcirc.nc', + '2Q1': 'all_adcirc.nc', + 'RHO1': 'all_adcirc.nc', + 'M8': 'all_adcirc.nc', + 'S4': 'all_adcirc.nc', + 'S6': 'all_adcirc.nc', + 'M3': 'all_adcirc.nc', + 'S1': 'all_adcirc.nc', + 'MK3': 'all_adcirc.nc', + '2MK3': 'all_adcirc.nc', + 'MN4': 'all_adcirc.nc', + 'MS4': 'all_adcirc.nc', + '2SM2': 'all_adcirc.nc', + 'MF': 'all_adcirc.nc', + 'MSF': 'all_adcirc.nc', + 'MM': 'all_adcirc.nc', + 'SA': 'all_adcirc.nc', + 'SSA': 'all_adcirc.nc', }, ], }, 'adcircnepac': { 'resource_atts': { - 'url': 'http://sms.aquaveo.com/adcircnepactides.zip', + 'url': '', 'archive': 'zip', # zip compression }, 'dataset_atts': { 'units_multiplier': 1., # meter }, 'consts': [{ # grouped by dimensionally compatible files - 'M2': 'adcircnepactides.exe', - 'S2': 'adcircnepactides.exe', - 'N2': 'adcircnepactides.exe', - 'K1': 'adcircnepactides.exe', - 'M4': 'adcircnepactides.exe', - 'O1': 'adcircnepactides.exe', - 'M6': 'adcircnepactides.exe', - 'Q1': 'adcircnepactides.exe', - 'K2': 'adcircnepactides.exe', + 'M2': 'all_adcirc.nc', + 'S2': 'all_adcirc.nc', + 'N2': 'all_adcirc.nc', + 'K1': 'all_adcirc.nc', + 'M4': 'all_adcirc.nc', + 'O1': 'all_adcirc.nc', + 'M6': 'all_adcirc.nc', + 'Q1': 'all_adcirc.nc', + 'K2': 'all_adcirc.nc', }, ], }, } From 0ebbc0f3e4ac2826e325b741365590e95cd8682c Mon Sep 17 00:00:00 2001 From: acreer-aquaveo Date: Wed, 17 Oct 2018 16:53:27 -0600 Subject: [PATCH 17/24] Converted the phase to radians for the calculation. Cleaned up commented out code and code used for debugging. --- harmonica/adcirc_database.py | 172 +++++------------------------------ 1 file changed, 21 insertions(+), 151 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 8584217..5a8ed3e 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -113,40 +113,29 @@ def get_components(self, locs, cons=None, positive_ph=False): # Step 1: read the file and get geometry: con_dsets = self.resources.get_datasets(cons)[0] - #con_dsets.element - #con_dsets.x - #con_dsets.y tri_search = xmsinterp_py.geometry.GmTriSearch() - print(str(len(con_dsets.x[0]))) con_x = con_dsets.x[0].values con_y = con_dsets.y[0].values - #for idx in range(len(con_dsets.x[0])): - # print("idx: {:10} X: {:8f} Y: {:8f}".format(idx, float(con_x[idx]), float(con_y[idx]))) - # for idx, (the_x, the_y) in enumerate(zip(con_x, con_y)): - # print("idx: {:10} X: {:8f} Y: {:8f}".format(idx, the_x, the_y)) - # exit() - mesh_pts = [(float(con_y[idx]), float(con_x[idx]), 0.0) for idx in range(len(con_x))] - print("Built mesh_pts") + + mesh_pts = [(float(con_x[idx]), float(con_y[idx]), 0.0) for idx in range(len(con_x))] tri_list = con_dsets.element.values.flatten().tolist() - print("len: {} {} {}".format(len(mesh_pts), len(con_dsets.element[0]), len(tri_list))) - # print(str(mesh_pts[:10])) tri_search.tris_to_search(mesh_pts, tri_list) + points_and_weights = [] elem_dset = con_dsets.element[0] max_tri = len(elem_dset) for pt in locs: - tri_idx = tri_search.tri_containing_pt(pt) - if tri_idx != -1 and tri_idx < max_tri: - print("Point found! {} {}".format(pt, tri_idx)) - # print("eLen: {}".format(type(elem_dset[tri_idx][0]))) - pt_1 = int(elem_dset[tri_idx][0]) - pt_2 = int(elem_dset[tri_idx][1]) - pt_3 = int(elem_dset[tri_idx][2]) + pt_flip = (pt[1], pt[0]) + tri_idx = tri_search.tri_containing_pt(pt_flip) + if tri_idx != -1: + pt_1 = int(tri_list[tri_idx]) + pt_2 = int(tri_list[tri_idx+1]) + pt_3 = int(tri_list[tri_idx+2]) x1, y1, z1 = mesh_pts[pt_1] x2, y2, z2 = mesh_pts[pt_2] x3, y3, z3 = mesh_pts[pt_3] - x = pt[0] - y = pt[1] + x = pt_flip[0] + y = pt_flip[1] s1 = abs((x2*y3-x3*y2)-(x*y3-x3*y)+(x*y2-x2*y)) s2 = abs((x*y3-x3*y)-(x1*y3-x3*y1)+(x1*y-x*y1)) s3 = abs((x2*y-x*y2)-(x1*y-x*y1)+(x1*y2-x2*y1)) @@ -155,23 +144,19 @@ def get_components(self, locs, cons=None, positive_ph=False): w2 = ((x-x1)*(y3-y1)-(y-y1)*(x3-x1))/ta w3 = ((y-y1)*(x2-x1)-(x-x1)*(y2-y1))/ta points_and_weights.append(((pt_1, pt_2, pt_3), (w1, w2, w3))) - else: - print("Not found! {}".format(pt)) for con in cons: con_amp_name = con + "_amplitude" con_pha_name = con + "_phase" + con_amp = con_dsets[con_amp_name][0] + con_pha = con_dsets[con_pha_name][0] for i, (pts, weights) in enumerate(points_and_weights): - # print("AMP: {} {}".format(con_amp_name, con_dsets.keys())) - con_amp = con_dsets[con_amp_name][0] - con_pha = con_dsets[con_pha_name][0] - # print("pts0: {} {}".format(pts[0], len(con_amp))) - amp1 = con_amp[pts[0]] - amp2 = con_amp[pts[1]] - amp3 = con_amp[pts[2]] - pha1 = con_pha[pts[0]] - pha2 = con_pha[pts[1]] - pha3 = con_pha[pts[2]] + amp1 = float(con_amp[pts[0]]) + amp2 = float(con_amp[pts[1]]) + amp3 = float(con_amp[pts[2]]) + pha1 = math.radians(float(con_pha[pts[0]])) + pha2 = math.radians(float(con_pha[pts[1]])) + pha3 = math.radians(float(con_pha[pts[2]])) self.data[i].loc[con] = [0.0, 0.0, 0.0] C1R=amp1*math.cos(pha1) @@ -188,125 +173,10 @@ def get_components(self, locs, cons=None, positive_ph=False): if new_amp == 0.0: new_phase = 0.0 else: - new_phase = 180.0 * math.acos(CTR / new_amp) / math.pi + new_phase = math.degrees(math.acos(CTR / new_amp)) if CTI < 0.0: new_phase = 360.0 - new_phase self.data[i].loc[con]['phase'] = new_phase - - # con_dset_file = self.resources[self.model]["consts"][cons[0]] - # if config['pre_existing_data_dir']: - # path = os.path.join(config['pre_existing_data_dir'], self.model, con_dset_file) - # else: - # path = os.path.join(config['data_dir'], self.model, con_dset_file) - # geom = xr.open_dataset(path, group='element') - # pt_x = xr.open_dataset(path, group='x') - # pt_y = xr.open_dataset(path, group='y') - # df = element.to_dataframe() - # all_grid = pd.read_table(self.grid_with_path, delim_whitespace=True, header=None, skiprows=skiprows, - # names=('row_type', 'cmp1', 'cmp2', 'cmp3', 'val'), index_col=1) - # conns = all_grid[all_grid['row_type'].str.lower() == 'e3t'][['cmp1', 'cmp2', 'cmp3']].values.astype(int) - 1 - # pts = all_grid[all_grid['row_type'].str.lower() == 'nd'][['cmp1', 'cmp2', 'cmp3']].values.astype(float) - # pts[:, 2] *= -1 - # verts = pd.DataFrame(pts, columns=['x', 'y', 'z']) - # tris = pd.DataFrame(conns, columns=['v0', 'v1', 'v2']) - - # convert to list of tuples: - # subset = data_set[['data_date', 'data_1', 'data_2']] - # tuples = [tuple(x) for x in subset.values] - - # feed to xmsinterp: - # element and weights (maybe, if not compute) - - # read the harmonic files: - # use pandas - # with open(self.harm_with_path) as f: - # num_cons = int(f.readline()) - # for i in range(num_cons): - # con_harm = pd.read_table(self.harm_with_path, delim_whitespace=True, header=None, skiprows=lambda x: x < num_cons+2 and x % i, - # names=('cmp1', 'cmp2')) - - # find the values at the point locations: - # ComputeHarmonics - - # return as a list - - - # create the temp directory - # os.makedirs(self.temp_folder) - # os.chdir(self.temp_folder) - # try: - # # write tides.in into the temp directory - # with open(self.tide_in, 'w') as f: - # f.write("{}\n".format(str(len(locs)))) - # for pt in locs: - # # 15.10f - # f.write("{:15.10f}{:15.10f}\n".format(pt[1], pt[0])) - # # copy the executable and .grd and .tdb file to the temp folder - # temp_exe_with_path = os.path.join(self.temp_folder, os.path.basename(self.exe_with_path)) - # temp_grd_with_path = os.path.join(self.temp_folder, self.grid_no_path) - # temp_tdb_with_path = os.path.join(self.temp_folder, self.harm_no_path) - - # old_exe_path = os.path.dirname(self.exe_with_path) - # old_grd_with_path = os.path.join(old_exe_path, self.grid_no_path) - # old_tdb_with_path = os.path.join(old_exe_path, self.harm_no_path) - - # shutil.copyfile(self.exe_with_path, temp_exe_with_path) - # shutil.copyfile(old_grd_with_path, temp_grd_with_path) - # shutil.copyfile(old_tdb_with_path, temp_tdb_with_path) - # # run the executable - # subprocess.run([temp_exe_with_path]) - # # read tides.out from the temp directory - # with open(self.tide_out, 'r') as f: - # all_lines = f.readlines() - # last_name_line = 0 - # is_first = True - # used_constituent = False - # con_name = '' - # column_count = 0 - # curr_pt = 0 - # for count, line in enumerate(all_lines): - # line = line.strip() - # if not line: - # continue - # elif any(not(c.isdigit() or c.isspace() or c == 'e' or c == 'E' - # or c == '.' or c == '-' or c == '+') for c in line): - # last_name_line = count - # is_first = True - # curr_pt = 0 - # continue - # elif is_first: - # con_name = all_lines[last_name_line].strip().lower() - # is_first = False - # if any(con_name in name for name in constituents): - # used_constituent = True - # else: - # used_constituent = False - - # if used_constituent: - # if column_count == 0: - # line_nums = [float(num) for num in line.split()] - # column_count = len(line_nums) - # if column_count == 2: - # amp, pha = map(float, line.split()) - # elif column_count == 4: - # junk, junk, amp, pha = map(float, line.split()) - # elif column_count == 6: - # amp, pha, junk, junk, junk, junk = map(float, line.split()) - # elif column_count == 8: - # junk, junk, amp, pha, junk, junk, junk, junk = map(float, line.split()) - # else: - # # we have a problem - # continue - # self.data[curr_pt].loc[con_name.upper()] = [amp, - # pha + (360.0 if positive_ph and pha < 0 else 0), - # NOAA_SPEEDS[con_name.upper()]] - # curr_pt += 1 - # finally: - # # delete the temp directory - # del_files = os.listdir(self.temp_folder) - # for del_file in del_files: - # os.remove(del_file) - # os.chdir(os.path.dirname(self.temp_folder)) - # os.rmdir(self.temp_folder) + self.data[i].loc[con]['speed'] = NOAA_SPEEDS[con.upper()] return self From afe31aae028fa40c1532817bd2168f85c70ff46a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 18 Oct 2018 13:50:53 -0600 Subject: [PATCH 18/24] Added a level of obstruction to the Constituents interface so the user can switch models with ease. Moved TPXO implementation from tidal_database.py to tpxo_database.py. Fixed resource fetching for the new ADCIRC NetCDF file. --- harmonica/adcirc_database.py | 100 +++---------- harmonica/cli/main_constituents.py | 6 +- harmonica/cli/main_reconstruct.py | 2 +- harmonica/harmonica.py | 4 +- harmonica/leprovost_database.py | 2 + harmonica/resource.py | 94 ++++++++++--- harmonica/tidal_constituents.py | 132 +++++++---------- harmonica/tidal_database.py | 23 ++- harmonica/tpxo_database.py | 99 +++++++++++++ tutorials/python_api/expected_tidal_test.out | 140 +++++++++++-------- tutorials/python_api/test.py | 92 ++++-------- 11 files changed, 360 insertions(+), 334 deletions(-) create mode 100644 harmonica/tpxo_database.py diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 5a8ed3e..a6b4566 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -1,84 +1,23 @@ #! python3 -from enum import Enum import math -import os -import subprocess -import random -import string -import shutil import pandas as pd -import xarray as xr import xmsinterp_py -from harmonica import config - from .tidal_database import convert_coords, NOAA_SPEEDS, TidalDB -class TidalDBAdcircEnum(Enum): - """Enum for specifying the type of an ADCIRC database. - - TIDE_NWAT: North West Atlantic database - TIDE_NEPAC: North East Pacific database - TIDE_NONE: Enum end - legacy from SMS port. - - """ - TIDE_NWAT = 0 # North West Atlantic Tidal Database - TIDE_NEPAC = 1 # North East Pacific Tidal Database - TIDE_NONE = 2 # Used if data not within any ADCIRC database domain - - class AdcircDB(TidalDB): """The class for extracting tidal data, specifically amplitude and phases, from an ADCIRC database. - Attributes: - exe_with_path (str): The path of the ADCIRC executable. - db_region (:obj:`harmonica.adcirc_database.TidalDBAdcircEnum`): The type of database. - grid_no_path (str): Filename of \*.grd file to use. - harm_no_path (str): Filename of \*.tdb file to use. - temp_folder (str): Temporary folder to hold files in while running executables. - tide_in (str): Temporary 'tides.in' filename with path. - tide_out (str): Temporary 'tides.out' filename with path. - """ - def __init__(self, resource_dir=None, db_region="adcircnwat"): + def __init__(self): """Constructor for the ADCIRC tidal database extractor. - Args: - resource_dir (:obj:`str`, optional): Directory of the ADCIRC resources. If not provided will become a - subfolder of "data" in the harmonica package location. - db_region (:obj:`str`, optional): ADCIRC tidal database region. Valid options are 'adcircnwat' and - 'adcircnepac' - """ - if db_region.lower() == "adcircnwat": - self.db_region = TidalDBAdcircEnum.TIDE_NWAT - self.grid_no_path = 'ec2012_v3d_chk.grd' - self.harm_no_path = 'ec2012_v3d_otis3_fort.53' - elif db_region.lower() == "adcircnepac": - self.db_region = TidalDBAdcircEnum.TIDE_NEPAC - self.grid_no_path = 'enpac2003.grd' - self.harm_no_path = 'enpac2003.tdb' - else: - raise ValueError('unrecognized ADCIRC database region.') - super().__init__(db_region.lower()) - resource_dir = self.resources.download_model(resource_dir) - # self.db_with_path = os.path.join(resource_dir, self.resources.model_atts["consts"][0]["M2"]) - self.grid_with_path = os.path.join(resource_dir, self.grid_no_path) - self.harm_with_path = os.path.join(resource_dir, self.harm_no_path) - - # Build the temp working folder name - # src_list = list(string.ascii_uppercase + string.digits) - # rand_str = random.choice(src_list) - # self.temp_folder = os.path.join(resource_dir, '_'.join(rand_str)) - # # check that the folder does not exist - # while os.path.isdir(self.temp_folder): - # rand_str = random.choice(src_list) - # self.temp_folder = self.temp_folder + rand_str - # self.tide_in = os.path.join(self.temp_folder, 'tides.in') - # self.tide_out = os.path.join(self.temp_folder, 'tides.out') + super().__init__('adcirc') + self.resources.download_model(None) def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed for the given constituents at the given points. @@ -122,8 +61,6 @@ def get_components(self, locs, cons=None, positive_ph=False): tri_search.tris_to_search(mesh_pts, tri_list) points_and_weights = [] - elem_dset = con_dsets.element[0] - max_tri = len(elem_dset) for pt in locs: pt_flip = (pt[1], pt[0]) tri_idx = tri_search.tri_containing_pt(pt_flip) @@ -136,9 +73,6 @@ def get_components(self, locs, cons=None, positive_ph=False): x3, y3, z3 = mesh_pts[pt_3] x = pt_flip[0] y = pt_flip[1] - s1 = abs((x2*y3-x3*y2)-(x*y3-x3*y)+(x*y2-x2*y)) - s2 = abs((x*y3-x3*y)-(x1*y3-x3*y1)+(x1*y-x*y1)) - s3 = abs((x2*y-x*y2)-(x1*y-x*y1)+(x1*y2-x2*y1)) ta = abs((x2*y3-x3*y2)-(x1*y3-x3*y1)+(x1*y2-x2*y1)) w1 = ((x-x3)*(y2-y3)+(x2-x3)*(y3-y))/ta w2 = ((x-x1)*(y3-y1)-(y-y1)*(x3-x1))/ta @@ -159,23 +93,23 @@ def get_components(self, locs, cons=None, positive_ph=False): pha3 = math.radians(float(con_pha[pts[2]])) self.data[i].loc[con] = [0.0, 0.0, 0.0] - C1R=amp1*math.cos(pha1) - C1I=amp1*math.sin(pha1) - C2R=amp2*math.cos(pha2) - C2I=amp2*math.sin(pha2) - C3R=amp3*math.cos(pha3) - C3I=amp3*math.sin(pha3) - CTR=C1R*weights[0]+C2R*weights[1]+C3R*weights[2] - CTI=C1I*weights[0]+C2I*weights[1]+C3I*weights[2] - - new_amp = math.sqrt(CTR*CTR+CTI*CTI) + c1r = amp1*math.cos(pha1) + c1i = amp1*math.sin(pha1) + c2r = amp2*math.cos(pha2) + c2i = amp2*math.sin(pha2) + c3r = amp3*math.cos(pha3) + c3i = amp3*math.sin(pha3) + ctr = c1r*weights[0]+c2r*weights[1]+c3r*weights[2] + cti = c1i*weights[0]+c2i*weights[1]+c3i*weights[2] + + new_amp = math.sqrt(ctr*ctr+cti*cti) self.data[i].loc[con]['amplitude'] = new_amp if new_amp == 0.0: - new_phase = 0.0 + new_phase = 0.0 else: - new_phase = math.degrees(math.acos(CTR / new_amp)) - if CTI < 0.0: - new_phase = 360.0 - new_phase + new_phase = math.degrees(math.acos(ctr / new_amp)) + if cti < 0.0: + new_phase = 360.0 - new_phase self.data[i].loc[con]['phase'] = new_phase self.data[i].loc[con]['speed'] = NOAA_SPEEDS[con.upper()] diff --git a/harmonica/cli/main_constituents.py b/harmonica/cli/main_constituents.py index 45510c6..72af0d0 100644 --- a/harmonica/cli/main_constituents.py +++ b/harmonica/cli/main_constituents.py @@ -1,4 +1,4 @@ -from ..tidal_constituents import Constituents +from ..tpxo_database import TpxoDB from .common import add_common_args, add_loc_model_args, add_const_out_args import argparse import sys @@ -37,8 +37,8 @@ def parse_args(args): def execute(args): - cons = Constituents().get_components([args.lat, args.lon], model=args.model, cons=args.cons, - positive_ph=args.positive_phase) + cons = TpxoDB().get_components([args.lat, args.lon], model=args.model, cons=args.cons, + positive_ph=args.positive_phase) out = cons.data.to_csv(args.output, sep='\t', header=True, index=True, index_label='constituent') if args.output is None: print(out) diff --git a/harmonica/cli/main_reconstruct.py b/harmonica/cli/main_reconstruct.py index e8c0ca8..b6b3d97 100644 --- a/harmonica/cli/main_reconstruct.py +++ b/harmonica/cli/main_reconstruct.py @@ -1,4 +1,4 @@ -from ..tidal_constituents import Constituents +from ..tpxo_database import TpxoDB from ..harmonica import Tide from .common import add_common_args, add_loc_model_args, add_const_out_args from pytides.tide import Tide as pyTide diff --git a/harmonica/harmonica.py b/harmonica/harmonica.py index 51910ba..f38367c 100644 --- a/harmonica/harmonica.py +++ b/harmonica/harmonica.py @@ -1,4 +1,4 @@ -from .tidal_constituents import Constituents, NOAA_SPEEDS +from .tpxo_database import TpxoDB, NOAA_SPEEDS from .resource import ResourceManager from pytides.astro import astro from pytides.tide import Tide as pyTide @@ -28,7 +28,7 @@ def __init__(self): # tide dataframe: # date_times (year, month, day, hour, minute, second; UTC/GMT) self.data = pd.DataFrame(columns=['datetimes', 'water_level']) - self.constituents = Constituents() + self.constituents = TpxoDB() def reconstruct_tide(self, loc, times, model=ResourceManager.DEFAULT_RESOURCE, cons=[], positive_ph=False, offset=None): diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 415d805..3aaa6f0 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -37,6 +37,8 @@ def get_components(self, locs, cons=None, positive_ph=False): not supplied, all valid constituents will be extracted. positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or [-180 180] (False, the default). + model (:obj:`str`, optional): Name of the tidal model to use to query for the data. If not provided, current + model will be used. If a model other than the current is provided, current model is switched. Returns: :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including diff --git a/harmonica/resource.py b/harmonica/resource.py index 93f5ee8..b031662 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -166,10 +166,10 @@ class ResourceManager(object): 'T2': 't2.nc', }, ], }, - 'adcircnwat': { + 'adcirc': { 'resource_atts': { - 'url': '', - 'archive': 'tar', # tar compression + 'url': 'http://sms.aquaveo.com/', + 'archive': None, # Uncompressed NetCDF file }, 'dataset_atts': { 'units_multiplier': 1., # meter @@ -214,26 +214,74 @@ class ResourceManager(object): 'SSA': 'all_adcirc.nc', }, ], }, - 'adcircnepac': { - 'resource_atts': { - 'url': '', - 'archive': 'zip', # zip compression - }, - 'dataset_atts': { - 'units_multiplier': 1., # meter - }, - 'consts': [{ # grouped by dimensionally compatible files - 'M2': 'all_adcirc.nc', - 'S2': 'all_adcirc.nc', - 'N2': 'all_adcirc.nc', - 'K1': 'all_adcirc.nc', - 'M4': 'all_adcirc.nc', - 'O1': 'all_adcirc.nc', - 'M6': 'all_adcirc.nc', - 'Q1': 'all_adcirc.nc', - 'K2': 'all_adcirc.nc', - }, ], - }, + # 'adcircnwat': { + # 'resource_atts': { + # 'url': '', + # 'archive': 'tar', # tar compression + # }, + # 'dataset_atts': { + # 'units_multiplier': 1., # meter + # }, + # 'consts': [{ # grouped by dimensionally compatible files + # 'M2': 'all_adcirc.nc', + # 'S2': 'all_adcirc.nc', + # 'N2': 'all_adcirc.nc', + # 'K1': 'all_adcirc.nc', + # 'M4': 'all_adcirc.nc', + # 'O1': 'all_adcirc.nc', + # 'M6': 'all_adcirc.nc', + # 'Q1': 'all_adcirc.nc', + # 'K2': 'all_adcirc.nc', + # 'L2': 'all_adcirc.nc', + # '2N2': 'all_adcirc.nc', + # 'R2': 'all_adcirc.nc', + # 'T2': 'all_adcirc.nc', + # 'LAMBDA2': 'all_adcirc.nc', + # 'MU2': 'all_adcirc.nc', + # 'NU2': 'all_adcirc.nc', + # 'J1': 'all_adcirc.nc', + # 'M1': 'all_adcirc.nc', + # 'OO1': 'all_adcirc.nc', + # 'P1': 'all_adcirc.nc', + # '2Q1': 'all_adcirc.nc', + # 'RHO1': 'all_adcirc.nc', + # 'M8': 'all_adcirc.nc', + # 'S4': 'all_adcirc.nc', + # 'S6': 'all_adcirc.nc', + # 'M3': 'all_adcirc.nc', + # 'S1': 'all_adcirc.nc', + # 'MK3': 'all_adcirc.nc', + # '2MK3': 'all_adcirc.nc', + # 'MN4': 'all_adcirc.nc', + # 'MS4': 'all_adcirc.nc', + # '2SM2': 'all_adcirc.nc', + # 'MF': 'all_adcirc.nc', + # 'MSF': 'all_adcirc.nc', + # 'MM': 'all_adcirc.nc', + # 'SA': 'all_adcirc.nc', + # 'SSA': 'all_adcirc.nc', + # }, ], + # }, + # 'adcircnepac': { + # 'resource_atts': { + # 'url': '', + # 'archive': 'zip', # zip compression + # }, + # 'dataset_atts': { + # 'units_multiplier': 1., # meter + # }, + # 'consts': [{ # grouped by dimensionally compatible files + # 'M2': 'all_adcirc.nc', + # 'S2': 'all_adcirc.nc', + # 'N2': 'all_adcirc.nc', + # 'K1': 'all_adcirc.nc', + # 'M4': 'all_adcirc.nc', + # 'O1': 'all_adcirc.nc', + # 'M6': 'all_adcirc.nc', + # 'Q1': 'all_adcirc.nc', + # 'K2': 'all_adcirc.nc', + # }, ], + # }, } DEFAULT_RESOURCE = 'tpxo9' diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index fe811b2..2c219ba 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -1,29 +1,39 @@ -from .resource import ResourceManager -from .tidal_database import NOAA_SPEEDS, TidalDB +from .adcirc_database import AdcircDB +from .leprovost_database import LeProvostDB +from .tpxo_database import TpxoDB -from bisect import bisect -import numpy as np -import pandas as pd +class Constituents: + tpxo_models = ['tpxo7', 'tpxo8', 'tpxo9'] + leprovost_models = ['fes2014', 'leprovost'] -class Constituents(TidalDB): - """Harmonica tidal constituents.""" + def __init__(self, model): + self.current_model = None + self.switch_model(model.lower()) - def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): - """Constructor for the TPXO tidal extractor. + def switch_model(self, new_model): + if self.current_model and self.current_model.model == new_model: + return # Already have the correct impl for this model, nothing to do. - Args: - model (:obj:`str`, optional): The name of the TPXO model. One of: 'tpxo9', 'tpxo8', 'tpxo7'. - ResourceManager.DEFAULT_RESOURCE if not specified. - """ - # constituent information dataframe: - # amplitude (meters) - # phase (degrees) - # speed (degrees/hour, UTC/GMT) - super().__init__(model) + if new_model in self.tpxo_models: # Switch to a TPXO model + # If we already have a TPXO impl, change its version if necessary. + if self.current_model and self.current_model.model in self.tpxo_models: + self.current_model.change_model(new_model) + else: # Construct a new TPXO impl. + self.current_model = TpxoDB(new_model) + elif new_model in self.leprovost_models: + # If we already have a LeProvost impl, change its version if necessary. + if self.current_model and self.current_model.model in self.leprovost_models: + self.current_model.change_model(new_model) + else: # Construct a new LeProvost impl. + self.current_model = LeProvostDB(new_model) + elif new_model == 'adcirc': + self.current_model = AdcircDB() + else: + raise ValueError("Model not supported - {}".format(new_model)) - def get_components(self, locs, cons=None, positive_ph=False): - """Get the amplitude, phase, and speed of specified constituents at specified point locations. + def get_components(self, locs, cons=None, positive_ph=False, model=None): + """Abstract method to get amplitude, phase, and speed of specified constituents at specified point locations. Args: locs (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] @@ -32,68 +42,34 @@ def get_components(self, locs, cons=None, positive_ph=False): not supplied, all valid constituents will be extracted. positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or [-180 180] (False, the default). + model (:obj:`str`, optional): Name of the tidal model to use to query for the data. If not provided, current + model will be used. If a model other than the current is provided, current model is switched. Returns: - :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including - amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, - where each element in the return list is the constituent data for the corresponding element in locs. - Empty list on error. Note that function uses fluent interface pattern. + :obj:`list` of :obj:`pandas.DataFrame`: Implementations should return a list of dataframes of constituent + information including amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is + parallel with locs, where each element in the return list is the constituent data for the corresponding + element in locs. Empty list on error. Note that function uses fluent interface pattern. """ - self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + if model and model.lower() != self.current_model.model: + self.switch_model(model.lower()) + return self.current_model.get_components(locs, cons, positive_ph) + + def get_nodal_factor(self, names, hour, day, month, year): + """Get the nodal factor for specified constituents at a specified time. - # if no constituents were requested, return all available - if cons is None or not len(cons): - cons = self.resources.available_constituents() - # open the netcdf database(s) - for d in self.resources.get_datasets(cons): - # remove unnecessary data array dimensions if present (e.g. tpxo7.2) - if 'nx' in d.lat_z.dims: - d['lat_z'] = d.lat_z.sel(nx=0, drop=True) - if 'ny' in d.lon_z.dims: - d['lon_z'] = d.lon_z.sel(ny=0, drop=True) - # get the dataset constituent name array from data cube - nc_names = [x.tostring().decode('utf-8').strip().upper() for x in d.con.values] - for c in set(cons) & set(nc_names): - for i, loc in enumerate(locs): - lat, lon = loc - # check the phase of the longitude - if lon < 0: - lon = lon + 360. + Args: + names (:obj:`list` of :obj:`str`): Names of the constituents to get nodal factors for + hour (float): The hour of the specified time. Can be fractional + day (int): The day of the specified time. + month (int): The month of the specified time. + year (int): The year of the specified time. - # get constituent and bounding indices within the data cube - idx = {'con': nc_names.index(c)} - idx['top'] = bisect(d.lat_z[idx['con']], lat) - idx['right'] = bisect(d.lon_z[idx['con']], lon) - idx['bottom'] = idx['top'] - 1 - idx['left'] = idx['right'] - 1 - # get distance from the bottom left to the requested point - dx = (lon - d.lon_z.values[idx['con'], idx['left']]) / \ - (d.lon_z.values[idx['con'], idx['right']] - d.lon_z.values[idx['con'], idx['left']]) - dy = (lat - d.lat_z.values[idx['con'], idx['bottom']]) / \ - (d.lat_z.values[idx['con'], idx['top']] - d.lat_z.values[idx['con'], idx['bottom']]) - # calculate weights for bilinear spline - weights = np.array([ - (1. - dx) * (1. - dy), # w00 :: bottom left - (1. - dx) * dy, # w01 :: bottom right - dx * (1. - dy), # w10 :: top left - dx * dy # w11 :: top right - ]).reshape((2, 2)) - weights = weights / weights.sum() - # devise the slice to subset surrounding values - query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] - # calculate the weighted tide from real and imaginary components - h = np.complex((d.hRe.values[query] * weights).sum(), -(d.hIm.values[query] * weights).sum()) - # get the phase and amplitude - ph = np.angle(h, deg=True) - # place info into data table - self.data[i].loc[c] = [ - # amplitude - np.absolute(h) * self.resources.get_units_multiplier(), - # phase - ph + (360. if positive_ph and ph < 0 else 0), - # speed - NOAA_SPEEDS[c] - ] + Returns: + :obj:`pandas.DataFrame`: Constituent data frames. Each row contains frequency, earth tidal reduction factor, + amplitude, nodal factor, and equilibrium argument for one of the specified constituents. Rows labeled by + constituent name. - return self + """ + return self.current_model.get_nodal_factor(names, hour, day, month, year) \ No newline at end of file diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index 80c7883..f92fdfc 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -1,6 +1,5 @@ #! python3 -from enum import Enum from abc import ABCMeta, abstractmethod import math @@ -87,11 +86,6 @@ def convert_coords(coords, zero_to_360=False): return coords -class TidalDBEnum(Enum): - TIDAL_DB_LEPROVOST = 0 - TIDAL_DB_ADCIRC = 1 - - class OrbitVariables(object): def __init__(self): self.dh = 0.0 @@ -168,6 +162,7 @@ def change_model(self, model): model = model.lower() if model == 'tpxo7_2': model = 'tpxo7' + if model != self._model: self._model = model self.resources = ResourceManager(self._model) @@ -205,15 +200,15 @@ def have_constituent(self, name): """ return name.upper() in self.resources.available_constituents() - def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): + def get_nodal_factor(self, names, hour, day, month, year): """Get the nodal factor for specified constituents at a specified time. Args: - a_names (:obj:`list` of :obj:`str`): Names of the constituents to get nodal factors for - a_hour (float): The hour of the specified time. Can be fractional - a_day (int): The day of the specified time. - a_month (int): The month of the specified time. - a_year (int): The year of the specified time. + names (:obj:`list` of :obj:`str`): Names of the constituents to get nodal factors for + hour (float): The hour of the specified time. Can be fractional + day (int): The day of the specified time. + month (int): The month of the specified time. + year (int): The year of the specified time. Returns: :obj:`pandas.DataFrame`: Constituent data frames. Each row contains frequency, earth tidal reduction factor, @@ -253,8 +248,8 @@ def get_nodal_factor(self, a_names, a_hour, a_day, a_month, a_year): con_data = pd.DataFrame(columns=["amplitude", "frequency", "earth_tide_reduction_factor", "equilibrium_argument", "nodal_factor"]) - self.get_eq_args(a_hour, a_day, a_month, a_year) - for idx, name in enumerate(a_names): + self.get_eq_args(hour, day, month, year) + for idx, name in enumerate(names): name = name.upper() try: name_idx = con_names.index(name) diff --git a/harmonica/tpxo_database.py b/harmonica/tpxo_database.py new file mode 100644 index 0000000..764abfa --- /dev/null +++ b/harmonica/tpxo_database.py @@ -0,0 +1,99 @@ +from .resource import ResourceManager +from .tidal_database import NOAA_SPEEDS, TidalDB + +from bisect import bisect +import numpy as np +import pandas as pd + + +class TpxoDB(TidalDB): + """Harmonica tidal constituents.""" + + def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): + """Constructor for the TPXO tidal extractor. + + Args: + model (:obj:`str`, optional): The name of the TPXO model. One of: 'tpxo9', 'tpxo8', 'tpxo7'. + ResourceManager.DEFAULT_RESOURCE if not specified. + """ + # constituent information dataframe: + # amplitude (meters) + # phase (degrees) + # speed (degrees/hour, UTC/GMT) + super().__init__(model) + + def get_components(self, locs, cons=None, positive_ph=False): + """Get the amplitude, phase, and speed of specified constituents at specified point locations. + + Args: + locs (:obj:`list` of :obj:`tuple` of :obj:`float`): latitude [-90, 90] and longitude [-180 180] or [0 360] + of the requested points. + cons (:obj:`list` of :obj:`str`, optional): List of the constituent names to get amplitude and phase for. If + not supplied, all valid constituents will be extracted. + positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or + [-180 180] (False, the default). + + Returns: + :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including + amplitude (meters), phase (degrees) and speed (degrees/hour, UTC/GMT). The list is parallel with locs, + where each element in the return list is the constituent data for the corresponding element in locs. + Empty list on error. Note that function uses fluent interface pattern. + + """ + self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] + + # if no constituents were requested, return all available + if cons is None or not len(cons): + cons = self.resources.available_constituents() + # open the netcdf database(s) + for d in self.resources.get_datasets(cons): + # remove unnecessary data array dimensions if present (e.g. tpxo7.2) + if 'nx' in d.lat_z.dims: + d['lat_z'] = d.lat_z.sel(nx=0, drop=True) + if 'ny' in d.lon_z.dims: + d['lon_z'] = d.lon_z.sel(ny=0, drop=True) + # get the dataset constituent name array from data cube + nc_names = [x.tostring().decode('utf-8').strip().upper() for x in d.con.values] + for c in set(cons) & set(nc_names): + for i, loc in enumerate(locs): + lat, lon = loc + # check the phase of the longitude + if lon < 0: + lon = lon + 360. + + # get constituent and bounding indices within the data cube + idx = {'con': nc_names.index(c)} + idx['top'] = bisect(d.lat_z[idx['con']], lat) + idx['right'] = bisect(d.lon_z[idx['con']], lon) + idx['bottom'] = idx['top'] - 1 + idx['left'] = idx['right'] - 1 + # get distance from the bottom left to the requested point + dx = (lon - d.lon_z.values[idx['con'], idx['left']]) / \ + (d.lon_z.values[idx['con'], idx['right']] - d.lon_z.values[idx['con'], idx['left']]) + dy = (lat - d.lat_z.values[idx['con'], idx['bottom']]) / \ + (d.lat_z.values[idx['con'], idx['top']] - d.lat_z.values[idx['con'], idx['bottom']]) + # calculate weights for bilinear spline + weights = np.array([ + (1. - dx) * (1. - dy), # w00 :: bottom left + (1. - dx) * dy, # w01 :: bottom right + dx * (1. - dy), # w10 :: top left + dx * dy # w11 :: top right + ]).reshape((2, 2)) + weights = weights / weights.sum() + # devise the slice to subset surrounding values + query = np.s_[idx['con'], idx['left']:idx['right']+1, idx['bottom']:idx['top']+1] + # calculate the weighted tide from real and imaginary components + h = np.complex((d.hRe.values[query] * weights).sum(), -(d.hIm.values[query] * weights).sum()) + # get the phase and amplitude + ph = np.angle(h, deg=True) + # place info into data table + self.data[i].loc[c] = [ + # amplitude + np.absolute(h) * self.resources.get_units_multiplier(), + # phase + ph + (360. if positive_ph and ph < 0 else 0), + # speed + NOAA_SPEEDS[c] + ] + + return self diff --git a/tutorials/python_api/expected_tidal_test.out b/tutorials/python_api/expected_tidal_test.out index 0aed68c..dec1253 100644 --- a/tutorials/python_api/expected_tidal_test.out +++ b/tutorials/python_api/expected_tidal_test.out @@ -1,56 +1,10 @@ -ADCIRC Atlantic nodal factor: +Nodal factor: amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor M2 0.242334 0.000141 0.693 345.151862 1.021162 S2 0.112841 0.000145 0.693 90.000000 1.000000 N2 0.046398 0.000138 0.693 77.558471 1.021162 K1 0.141565 0.000073 0.736 105.776526 0.945419 -ADCIRC Pacific nodal factor: - amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor -M2 0.242334 0.000141 0.693 345.151862 1.021162 -S2 0.112841 0.000145 0.693 90.000000 1.000000 -N2 0.046398 0.000138 0.693 77.558471 1.021162 -K1 0.141565 0.000073 0.736 105.776526 0.945419 - -LeProvost nodal factor: - amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor -M2 0.242334 0.000141 0.693 345.151862 1.021162 -S2 0.112841 0.000145 0.693 90.000000 1.000000 -N2 0.046398 0.000138 0.693 77.558471 1.021162 -K1 0.141565 0.000073 0.736 105.776526 0.945419 - -ADCIRC Atlantic components: - amplitude phase speed -K1 0.094224 174.103 15.041069 -M2 0.574420 351.285 28.984104 -N2 0.132220 337.764 28.439730 -S2 0.116490 12.800 30.000000 - - amplitude phase speed -K1 0.10643 207.605 15.041069 -M2 1.42770 114.565 28.984104 -N2 0.29871 83.796 28.439730 -S2 0.21213 150.550 30.000000 - - amplitude phase speed -K1 0.12156 198.872 15.041069 -M2 1.99330 96.345 28.984104 -N2 0.40195 66.496 28.439730 -S2 0.29833 131.891 30.000000 - -ADCIRC Pacific components: - amplitude phase speed -K1 0.44356 234.182 15.041069 -M2 0.84280 221.629 28.984104 -N2 0.17248 195.838 28.439730 -S2 0.22670 245.825 30.000000 - - amplitude phase speed -K1 0.45602 238.807 15.041069 -M2 0.94421 230.499 28.984104 -N2 0.19271 205.061 28.439730 -S2 0.26453 256.892 30.000000 - LeProvost components: amplitude phase speed K1 0.080152 171.446949 15.041069 @@ -58,11 +12,11 @@ M2 0.589858 353.748502 28.984104 N2 0.136323 337.950345 28.439730 S2 0.083580 20.950538 30.000000 - amplitude phase speed -K1 0.0 0.0 0.0 -M2 0.0 0.0 0.0 -N2 0.0 0.0 0.0 -S2 0.0 0.0 0.0 + amplitude phase speed +K1 0.1194 193.8 15.041069 +M2 1.1736 113.8 28.984104 +N2 0.1628 50.8 28.439730 +S2 0.1318 101.1 30.000000 amplitude phase speed K1 0.0 0.0 0.0 @@ -82,6 +36,37 @@ M2 0.858488 232.029922 28.984104 N2 0.175845 210.476586 28.439730 S2 0.242000 258.787649 30.000000 +ADCIRC components: + amplitude phase speed +K1 0.099715 173.295539 15.041069 +M2 0.554417 352.729485 28.984104 +N2 0.126147 335.014198 28.439730 +S2 0.106317 16.530658 30.000000 + + amplitude phase speed +K1 0.115789 195.361646 15.041069 +M2 1.190749 111.097782 28.984104 +N2 0.254679 75.426588 28.439730 +S2 0.170824 143.134712 30.000000 + + amplitude phase speed +K1 0.141171 189.620866 15.041069 +M2 4.255815 105.440818 28.984104 +N2 0.781585 74.002776 28.439730 +S2 0.631516 145.428306 30.000000 + + amplitude phase speed +K1 0.443556 234.182261 15.041069 +M2 0.842798 221.629047 28.984104 +N2 0.172478 195.837684 28.439730 +S2 0.226702 245.824817 30.000000 + + amplitude phase speed +K1 0.456021 238.806713 15.041069 +M2 0.944207 230.498719 28.984104 +N2 0.192711 205.061278 28.439730 +S2 0.264533 256.892259 30.000000 + TPX0 components: amplitude phase speed K1 0.092135 176.901563 15.041069 @@ -89,17 +74,17 @@ M2 0.560701 352.265551 28.984104 N2 0.139185 346.750468 28.439730 S2 0.106576 18.615143 30.000000 - amplitude phase speed -K1 0.0 0.0 15.041069 -M2 0.0 0.0 28.984104 -N2 0.0 0.0 28.439730 -S2 0.0 0.0 30.000000 + amplitude phase speed +K1 0.130943 197.327927 15.041069 +M2 1.150805 107.257205 28.984104 +N2 0.261782 76.929914 28.439730 +S2 0.185605 141.870518 30.000000 - amplitude phase speed -K1 0.0 0.0 15.041069 -M2 0.0 0.0 28.984104 -N2 0.0 0.0 28.439730 -S2 0.0 0.0 30.000000 + amplitude phase speed +K1 0.155800 192.680364 15.041069 +M2 4.128097 104.709481 28.984104 +N2 0.851786 79.022472 28.439730 +S2 0.648562 145.516010 30.000000 amplitude phase speed K1 0.412604 232.880147 15.041069 @@ -113,3 +98,34 @@ M2 0.910663 234.117743 28.984104 N2 0.188528 206.717292 28.439730 S2 0.253107 260.181916 30.000000 +FES2014 components: + amplitude phase speed +K1 0.090187 173.347897 15.041069 +M2 0.574708 354.674980 28.984104 +N2 0.134563 335.961855 28.439730 +S2 0.111234 15.286627 30.000000 + + amplitude phase speed +K1 0.131800 203.724442 15.041069 +M2 1.165043 107.518818 28.984104 +N2 0.262041 75.300656 28.439730 +S2 0.176183 144.626343 30.000000 + + amplitude phase speed +K1 0.148333 193.815262 15.041069 +M2 3.954875 98.342868 28.984104 +N2 0.786427 75.031938 28.439730 +S2 0.648518 146.118892 30.000000 + + amplitude phase speed +K1 0.416949 232.809029 15.041069 +M2 0.803786 220.349546 28.984104 +N2 0.169759 195.739307 28.439730 +S2 0.214817 245.327583 30.000000 + + amplitude phase speed +K1 0.428201 237.123548 15.041069 +M2 0.903397 229.333500 28.984104 +N2 0.189360 204.322635 28.439730 +S2 0.249951 255.765785 30.000000 + diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index b5b9a8b..d38e18b 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -1,93 +1,49 @@ -import argparse import os -import harmonica.adcirc_database -import harmonica.leprovost_database -import harmonica.tidal_constituents +from harmonica.tidal_constituents import Constituents if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("-a", "--adcirc_atlantic", default=None, - help="path to the ADCIRC Atlantic database") - parser.add_argument("-p", "--adcirc_pacific", default=None, - help="path to the ADCIRC Pacific database") - args = vars(parser.parse_args()) - - adcirc_atlantic = args["adcirc_atlantic"] - adcir_pacific = args["adcirc_pacific"] - # Need to be in (lat, lon), not (x, y) - # Invert commented list declarations to test [-180, 180] vs. [0, 360] ranges. - #atlantic = [(39.74, 285.93), # Not valid with ADCIRC Pacific database - # (42.32, 288.6), - # (45.44, 290.62)] - #pacific = [(43.63, 235.45), # Not valid with ADCIRC Atlantic database - # (46.18, 235.62)] - atlantic = [(39.74, -74.07), # Not valid with ADCIRC Pacific database - (42.32, -71.4), - (45.44, -69.38)] - pacific = [(43.63, -124.55), # Not valid with ADCIRC Atlantic database - (46.18, -124.38)] - all_points = [] # All locations valid with LeProvost and TPXO - all_points.extend(atlantic) - all_points.extend(pacific) + all_points = [(39.74, -74.07), + (42.32, -70.0), + (45.44, -65.0), + (43.63, -124.55), + (46.18, -124.38)] - good_cons = ['M2', 'S2', 'N2', 'K1'] - # Create an ADCIRC database for the Atlantic locations (default). - ad_alantic_db = harmonica.adcirc_database.AdcircDB(adcirc_atlantic) - # Create an ADCIRC database for the Pacific locations. - ad_pacific_db = harmonica.adcirc_database.AdcircDB(adcir_pacific, "adcircnepac") - # Create a LeProvost database for all locations. - leprovost_db = harmonica.leprovost_database.LeProvostDB() - # Create a TPXO database for all locations. - tpxo_db = harmonica.tidal_constituents.Constituents('tpxo8') - # Create a FES2014 database for all locations. License restrictions prevent us - # from distributing the FES2014 resources. Must have local files already to use. - # fes2014_db = harmonica.leprovost_database.LeProvostDB('fes2014') + cons = ['M2', 'S2', 'N2', 'K1'] + constituents = Constituents('leprovost') - # Get nodal factor data from the ADCIRC and LeProvost tidal databases - ad_al_nodal_factor = ad_alantic_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) - ad_pa_nodal_factor = ad_pacific_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) - leprovost_nodal_factor = leprovost_db.get_nodal_factor(good_cons, 15, 30, 8, 2018) + # Get astronomical nodal factor data (not dependent on the tidal model) + nodal_factors = constituents.get_nodal_factor(cons, 15, 30, 8, 2018) f = open(os.path.join(os.getcwd(), "tidal_test.out"), "w") - f.write("ADCIRC Atlantic nodal factor:\n") - f.write(ad_al_nodal_factor.to_string() + "\n\n") - f.write("ADCIRC Pacific nodal factor:\n") - f.write(ad_pa_nodal_factor.to_string() + "\n\n") - f.write("LeProvost nodal factor:\n") - f.write(leprovost_nodal_factor.to_string() + "\n\n") + f.write("Nodal factor:\n") + f.write(nodal_factors.to_string() + "\n\n") f.flush() # Get tidal harmonic components for a list of points using the ADCIRC, # LeProvost, and TPXO databases. - ad_atlantic_comps = ad_alantic_db.get_components(atlantic, good_cons) - f.write("ADCIRC Atlantic components:\n") - for pt in ad_atlantic_comps.data: - f.write(pt.sort_index().to_string() + "\n\n") - - f.write("ADCIRC Pacific components:\n") - f.flush() - ad_pacific_comps = ad_pacific_db.get_components(pacific, good_cons) - for pt in ad_pacific_comps.data: - f.write(pt.sort_index().to_string() + "\n\n") - f.write("LeProvost components:\n") f.flush() - leprovost_comps = leprovost_db.get_components(all_points, good_cons) + leprovost_comps = constituents.get_components(all_points, cons) for pt in leprovost_comps.data: f.write(pt.sort_index().to_string() + "\n\n") + ad_atlantic_comps = constituents.get_components(all_points, cons, model='adcirc') + f.write("ADCIRC components:\n") + for pt in ad_atlantic_comps.data: + f.write(pt.sort_index().to_string() + "\n\n") + f.write("TPX0 components:\n") f.flush() - tpxo_comps = tpxo_db.get_components(all_points, good_cons, True) + tpxo_comps = constituents.get_components(all_points, cons, True, 'tpxo8') for pt in tpxo_comps.data: f.write(pt.sort_index().to_string() + "\n\n") - # f.write("FES2014 components:\n") - # f.flush() - # fes2014_comps = fes2014_db.get_components(all_points, good_cons) - # for pt in fes2014_comps.data: - # f.write(pt.sort_index().to_string() + "\n\n") + f.write("FES2014 components:\n") + f.flush() + fes2014_comps = constituents.get_components(all_points, cons, model='fes2014') + for pt in fes2014_comps.data: + f.write(pt.sort_index().to_string() + "\n\n") f.close() \ No newline at end of file From 8a8d609a3e1080274c119691848684c32fe41d05 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 18 Oct 2018 13:51:54 -0600 Subject: [PATCH 19/24] Clean up commented code --- harmonica/resource.py | 68 ------------------------------------------- 1 file changed, 68 deletions(-) diff --git a/harmonica/resource.py b/harmonica/resource.py index b031662..6fe958a 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -214,74 +214,6 @@ class ResourceManager(object): 'SSA': 'all_adcirc.nc', }, ], }, - # 'adcircnwat': { - # 'resource_atts': { - # 'url': '', - # 'archive': 'tar', # tar compression - # }, - # 'dataset_atts': { - # 'units_multiplier': 1., # meter - # }, - # 'consts': [{ # grouped by dimensionally compatible files - # 'M2': 'all_adcirc.nc', - # 'S2': 'all_adcirc.nc', - # 'N2': 'all_adcirc.nc', - # 'K1': 'all_adcirc.nc', - # 'M4': 'all_adcirc.nc', - # 'O1': 'all_adcirc.nc', - # 'M6': 'all_adcirc.nc', - # 'Q1': 'all_adcirc.nc', - # 'K2': 'all_adcirc.nc', - # 'L2': 'all_adcirc.nc', - # '2N2': 'all_adcirc.nc', - # 'R2': 'all_adcirc.nc', - # 'T2': 'all_adcirc.nc', - # 'LAMBDA2': 'all_adcirc.nc', - # 'MU2': 'all_adcirc.nc', - # 'NU2': 'all_adcirc.nc', - # 'J1': 'all_adcirc.nc', - # 'M1': 'all_adcirc.nc', - # 'OO1': 'all_adcirc.nc', - # 'P1': 'all_adcirc.nc', - # '2Q1': 'all_adcirc.nc', - # 'RHO1': 'all_adcirc.nc', - # 'M8': 'all_adcirc.nc', - # 'S4': 'all_adcirc.nc', - # 'S6': 'all_adcirc.nc', - # 'M3': 'all_adcirc.nc', - # 'S1': 'all_adcirc.nc', - # 'MK3': 'all_adcirc.nc', - # '2MK3': 'all_adcirc.nc', - # 'MN4': 'all_adcirc.nc', - # 'MS4': 'all_adcirc.nc', - # '2SM2': 'all_adcirc.nc', - # 'MF': 'all_adcirc.nc', - # 'MSF': 'all_adcirc.nc', - # 'MM': 'all_adcirc.nc', - # 'SA': 'all_adcirc.nc', - # 'SSA': 'all_adcirc.nc', - # }, ], - # }, - # 'adcircnepac': { - # 'resource_atts': { - # 'url': '', - # 'archive': 'zip', # zip compression - # }, - # 'dataset_atts': { - # 'units_multiplier': 1., # meter - # }, - # 'consts': [{ # grouped by dimensionally compatible files - # 'M2': 'all_adcirc.nc', - # 'S2': 'all_adcirc.nc', - # 'N2': 'all_adcirc.nc', - # 'K1': 'all_adcirc.nc', - # 'M4': 'all_adcirc.nc', - # 'O1': 'all_adcirc.nc', - # 'M6': 'all_adcirc.nc', - # 'Q1': 'all_adcirc.nc', - # 'K2': 'all_adcirc.nc', - # }, ], - # }, } DEFAULT_RESOURCE = 'tpxo9' From 4cf2228905c28dd98e46d7629aac995745ef7504 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 18 Oct 2018 16:31:32 -0600 Subject: [PATCH 20/24] Refactored some of the common interpolation code in LeProvost and ADCIRC implementations to a single function. Return NaN for locations outside the domain. Added a version number (2015) for the current version of the ADCIRC database. --- harmonica/adcirc_database.py | 60 +++++++++++--------- harmonica/cli/main.py | 2 +- harmonica/cli/main_constituents.py | 7 ++- harmonica/cli/main_download.py | 3 +- harmonica/cli/main_reconstruct.py | 6 +- harmonica/cli/main_resources.py | 2 +- harmonica/harmonica.py | 13 ++--- harmonica/leprovost_database.py | 47 +++++++-------- harmonica/resource.py | 2 +- harmonica/tidal_constituents.py | 44 ++++++++------ harmonica/tidal_database.py | 27 +++++++-- tutorials/python_api/expected_tidal_test.out | 10 ++-- tutorials/python_api/test.py | 2 +- 13 files changed, 132 insertions(+), 93 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index a6b4566..7b34c52 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -2,10 +2,11 @@ import math +import numpy import pandas as pd import xmsinterp_py -from .tidal_database import convert_coords, NOAA_SPEEDS, TidalDB +from .tidal_database import convert_coords, get_complex_components, NOAA_SPEEDS, TidalDB class AdcircDB(TidalDB): @@ -16,7 +17,7 @@ def __init__(self): """Constructor for the ADCIRC tidal database extractor. """ - super().__init__('adcirc') + super().__init__('adcirc2015') self.resources.download_model(None) def get_components(self, locs, cons=None, positive_ph=False): @@ -61,7 +62,7 @@ def get_components(self, locs, cons=None, positive_ph=False): tri_search.tris_to_search(mesh_pts, tri_list) points_and_weights = [] - for pt in locs: + for i, pt in enumerate(locs): pt_flip = (pt[1], pt[0]) tri_idx = tri_search.tri_containing_pt(pt_flip) if tri_idx != -1: @@ -73,44 +74,51 @@ def get_components(self, locs, cons=None, positive_ph=False): x3, y3, z3 = mesh_pts[pt_3] x = pt_flip[0] y = pt_flip[1] + # Compute barocentric weights ta = abs((x2*y3-x3*y2)-(x1*y3-x3*y1)+(x1*y2-x2*y1)) w1 = ((x-x3)*(y2-y3)+(x2-x3)*(y3-y))/ta w2 = ((x-x1)*(y3-y1)-(y-y1)*(x3-x1))/ta w3 = ((y-y1)*(x2-x1)-(x-x1)*(y2-y1))/ta - points_and_weights.append(((pt_1, pt_2, pt_3), (w1, w2, w3))) + points_and_weights.append((i, (pt_1, pt_2, pt_3), (w1, w2, w3))) + else: # Outside domain, return NaN for all constituents + for con in cons: + self.data[i].loc[con] = [numpy.nan, numpy.nan, numpy.nan] for con in cons: con_amp_name = con + "_amplitude" con_pha_name = con + "_phase" con_amp = con_dsets[con_amp_name][0] con_pha = con_dsets[con_pha_name][0] - for i, (pts, weights) in enumerate(points_and_weights): - amp1 = float(con_amp[pts[0]]) - amp2 = float(con_amp[pts[1]]) - amp3 = float(con_amp[pts[2]]) - pha1 = math.radians(float(con_pha[pts[0]])) - pha2 = math.radians(float(con_pha[pts[1]])) - pha3 = math.radians(float(con_pha[pts[2]])) - - self.data[i].loc[con] = [0.0, 0.0, 0.0] - c1r = amp1*math.cos(pha1) - c1i = amp1*math.sin(pha1) - c2r = amp2*math.cos(pha2) - c2i = amp2*math.sin(pha2) - c3r = amp3*math.cos(pha3) - c3i = amp3*math.sin(pha3) - ctr = c1r*weights[0]+c2r*weights[1]+c3r*weights[2] - cti = c1i*weights[0]+c2i*weights[1]+c3i*weights[2] - - new_amp = math.sqrt(ctr*ctr+cti*cti) - self.data[i].loc[con]['amplitude'] = new_amp + for i, pts, weights in points_and_weights: + amps = [float(con_amp[pts[0]]), float(con_amp[pts[1]]), float(con_amp[pts[2]])] + phases = [ + math.radians(float(con_pha[pts[0]])), + math.radians(float(con_pha[pts[1]])), + math.radians(float(con_pha[pts[2]])), + ] + + # Get the real and imaginary components from the amplitude and phases in the file. It + # would be better if these values were stored in the file like TPXO. + complex_components = get_complex_components(amps, phases) + ctr = ( + complex_components[0][0] * weights[0] + + complex_components[1][0] * weights[1] + + complex_components[2][0] * weights[2] + ) + cti = ( + complex_components[0][1] * weights[0] + + complex_components[1][1] * weights[1] + + complex_components[2][1] * weights[2] + ) + + new_amp = math.sqrt(ctr * ctr + cti * cti) if new_amp == 0.0: new_phase = 0.0 else: new_phase = math.degrees(math.acos(ctr / new_amp)) if cti < 0.0: new_phase = 360.0 - new_phase - self.data[i].loc[con]['phase'] = new_phase - self.data[i].loc[con]['speed'] = NOAA_SPEEDS[con.upper()] + speed = NOAA_SPEEDS[con] if con in NOAA_SPEEDS else numpy.nan + self.data[i].loc[con] = [new_amp, new_phase, speed] return self diff --git a/harmonica/cli/main.py b/harmonica/cli/main.py index 9cba0f1..8ab94b2 100644 --- a/harmonica/cli/main.py +++ b/harmonica/cli/main.py @@ -34,4 +34,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/harmonica/cli/main_constituents.py b/harmonica/cli/main_constituents.py index 72af0d0..4105268 100644 --- a/harmonica/cli/main_constituents.py +++ b/harmonica/cli/main_constituents.py @@ -1,4 +1,4 @@ -from ..tpxo_database import TpxoDB +from ..tidal_constituents import Constituents from .common import add_common_args, add_loc_model_args, add_const_out_args import argparse import sys @@ -37,8 +37,9 @@ def parse_args(args): def execute(args): - cons = TpxoDB().get_components([args.lat, args.lon], model=args.model, cons=args.cons, - positive_ph=args.positive_phase) + cons = Constituents(model=args.model).get_components( + [args.lat, args.lon], cons=args.cons, positive_ph=args.positive_phase + ) out = cons.data.to_csv(args.output, sep='\t', header=True, index=True, index_label='constituent') if args.output is None: print(out) diff --git a/harmonica/cli/main_download.py b/harmonica/cli/main_download.py index 8808196..6b45575 100644 --- a/harmonica/cli/main_download.py +++ b/harmonica/cli/main_download.py @@ -9,6 +9,7 @@ harmonica download tpxo8 """ + def config_parser(p, sub=False): # Subparser info if sub: @@ -51,4 +52,4 @@ def main(args=None): except RuntimeError as e: print(str(e)) sys.exit(1) - return \ No newline at end of file + return diff --git a/harmonica/cli/main_reconstruct.py b/harmonica/cli/main_reconstruct.py index b6b3d97..2fdcfa8 100644 --- a/harmonica/cli/main_reconstruct.py +++ b/harmonica/cli/main_reconstruct.py @@ -1,4 +1,3 @@ -from ..tpxo_database import TpxoDB from ..harmonica import Tide from .common import add_common_args, add_loc_model_args, add_const_out_args from pytides.tide import Tide as pyTide @@ -15,6 +14,7 @@ harmonica reconstruct 38.375789 -74.943915 """ + def validate_date(value): try: # return date.fromisoformat(value) # python 3.7 @@ -72,7 +72,7 @@ def parse_args(args): def execute(args): times = pyTide._times(datetime.fromordinal(args.start_date.toordinal()), np.arange(args.length * 24., dtype=float)) - tide = Tide().reconstruct_tide(loc=[args.lat, args.lon], times=times, model=args.model, cons=args.cons, + tide = Tide(model=args.model).reconstruct_tide(loc=[args.lat, args.lon], times=times, cons=args.cons, positive_ph=args.positive_phase) out = tide.data.to_csv(args.output, sep='\t', header=True, index=False) if args.output is None: @@ -88,4 +88,4 @@ def main(args=None): except RuntimeError as e: print(str(e)) sys.exit(1) - return \ No newline at end of file + return diff --git a/harmonica/cli/main_resources.py b/harmonica/cli/main_resources.py index e5afcea..ee4b845 100644 --- a/harmonica/cli/main_resources.py +++ b/harmonica/cli/main_resources.py @@ -61,4 +61,4 @@ def main(args=None): except RuntimeError as e: print(str(e)) sys.exit(1) - return \ No newline at end of file + return diff --git a/harmonica/harmonica.py b/harmonica/harmonica.py index f38367c..e050227 100644 --- a/harmonica/harmonica.py +++ b/harmonica/harmonica.py @@ -1,4 +1,5 @@ -from .tpxo_database import TpxoDB, NOAA_SPEEDS +from .tidal_constituents import Constituents +from .tidal_database import NOAA_SPEEDS from .resource import ResourceManager from pytides.astro import astro from pytides.tide import Tide as pyTide @@ -24,14 +25,13 @@ class Tide: 'MU2': 'mu2', } - def __init__(self): + def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): # tide dataframe: # date_times (year, month, day, hour, minute, second; UTC/GMT) self.data = pd.DataFrame(columns=['datetimes', 'water_level']) - self.constituents = TpxoDB() + self.constituents = Constituents(model=model) - def reconstruct_tide(self, loc, times, model=ResourceManager.DEFAULT_RESOURCE, - cons=[], positive_ph=False, offset=None): + def reconstruct_tide(self, loc, times, model=None, cons=[], positive_ph=False, offset=None): """Rescontruct a tide signal water levels at the given location and times Args: @@ -46,8 +46,7 @@ def reconstruct_tide(self, loc, times, model=ResourceManager.DEFAULT_RESOURCE, """ # get constituent information - self.constituents.model = model - self.constituents.get_components([loc], cons, positive_ph) + self.constituents.get_components([loc], cons, positive_ph, model=model) ncons = len(self.constituents.data[0]) + (1 if offset is not None else 0) tide_model = np.zeros(ncons, dtype=pyTide.dtype) diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 3aaa6f0..9ec59f4 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -10,7 +10,7 @@ import pandas as pd -from .tidal_database import convert_coords, NOAA_SPEEDS, TidalDB +from .tidal_database import convert_coords, get_complex_components, NOAA_SPEEDS, TidalDB class LeProvostDB(TidalDB): @@ -117,46 +117,46 @@ def get_components(self, locs, cons=None, positive_ph=False): skip = True else: # Make sure we have at least one neighbor with an active phase value. # Read potential contributing phases from the file. - xlo_yhi_phase = phase_dset[con_idx][yhi][xlo] - xlo_ylo_phase = phase_dset[con_idx][ylo][xlo] - xhi_yhi_phase = phase_dset[con_idx][yhi][xhi] - xhi_ylo_phase = phase_dset[con_idx][ylo][xhi] + xlo_yhi_phase = math.radians(phase_dset[con_idx][yhi][xlo]) + xlo_ylo_phase = math.radians(phase_dset[con_idx][ylo][xlo]) + xhi_yhi_phase = math.radians(phase_dset[con_idx][yhi][xhi]) + xhi_ylo_phase = math.radians(phase_dset[con_idx][ylo][xhi]) if (numpy.isnan(xlo_yhi_phase) and numpy.isnan(xhi_yhi_phase) and numpy.isnan(xlo_ylo_phase) and numpy.isnan(xhi_ylo_phase)): skip = True if skip: - self.data[i].loc[con] = [0.0, 0.0, 0.0] + self.data[i].loc[con] = [numpy.nan, numpy.nan, numpy.nan] else: xratio = (x_lon - xlonlo) / d_lon yratio = (y_lat - ylatlo) / d_lat - xcos1 = xlo_yhi_amp * math.cos(math.radians(xlo_yhi_phase)) - xcos2 = xhi_yhi_amp * math.cos(math.radians(xhi_yhi_phase)) - xcos3 = xlo_ylo_amp * math.cos(math.radians(xlo_ylo_phase)) - xcos4 = xhi_ylo_amp * math.cos(math.radians(xhi_ylo_phase)) - xsin1 = xlo_yhi_amp * math.sin(math.radians(xlo_yhi_phase)) - xsin2 = xhi_yhi_amp * math.sin(math.radians(xhi_yhi_phase)) - xsin3 = xlo_ylo_amp * math.sin(math.radians(xlo_ylo_phase)) - xsin4 = xhi_ylo_amp * math.sin(math.radians(xhi_ylo_phase)) + # Get the real and imaginary components from the amplitude and phases in the file. It + # would be better if these values were stored in the file like TPXO. + complex_comps = get_complex_components( + amps=[xlo_yhi_amp, xhi_yhi_amp, xlo_ylo_amp, xhi_ylo_amp], + phases=[xlo_yhi_phase, xhi_yhi_phase, xlo_ylo_phase, xhi_ylo_phase], + ) + + # Perform bi-linear interpolation of the corners to the target point. xcos = 0.0 xsin = 0.0 denom = 0.0 if not numpy.isnan(xlo_yhi_amp) and not numpy.isnan(xlo_yhi_phase): - xcos = xcos + xcos1 * (1.0 - xratio) * yratio - xsin = xsin + xsin1 * (1.0 - xratio) * yratio + xcos = xcos + complex_comps[0][0] * (1.0 - xratio) * yratio + xsin = xsin + complex_comps[0][1] * (1.0 - xratio) * yratio denom = denom + (1.0 - xratio) * yratio if not numpy.isnan(xhi_yhi_amp) and not numpy.isnan(xhi_yhi_phase): - xcos = xcos + xcos2 * xratio * yratio - xsin = xsin + xsin2 * xratio * yratio + xcos = xcos + complex_comps[1][0] * xratio * yratio + xsin = xsin + complex_comps[1][1] * xratio * yratio denom = denom + xratio * yratio if not numpy.isnan(xlo_ylo_amp) and not numpy.isnan(xlo_ylo_phase): - xcos = xcos + xcos3 * (1.0 - xratio) * (1 - yratio) - xsin = xsin + xsin3 * (1.0 - xratio) * (1 - yratio) + xcos = xcos + complex_comps[2][0] * (1.0 - xratio) * (1 - yratio) + xsin = xsin + complex_comps[2][1] * (1.0 - xratio) * (1 - yratio) denom = denom + (1.0 - xratio) * (1.0 - yratio) if not numpy.isnan(xhi_ylo_amp) and not numpy.isnan(xhi_ylo_phase): - xcos = xcos + xcos4 * (1.0 - yratio) * xratio - xsin = xsin + xsin4 * (1.0 - yratio) * xratio + xcos = xcos + complex_comps[3][0] * (1.0 - yratio) * xratio + xsin = xsin + complex_comps[3][1] * (1.0 - yratio) * xratio denom = denom + (1.0 - yratio) * xratio xcos = xcos / denom @@ -168,6 +168,7 @@ def get_components(self, locs, cons=None, positive_ph=False): if xsin < 0.0: phase = 360.0 - phase phase += (360. if positive_ph and phase < 0 else 0) - self.data[i].loc[con] = [amp, phase, NOAA_SPEEDS[con]] + speed = NOAA_SPEEDS[con] if con in NOAA_SPEEDS else numpy.nan + self.data[i].loc[con] = [amp, phase, speed] return self diff --git a/harmonica/resource.py b/harmonica/resource.py index 6fe958a..e0209f4 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -166,7 +166,7 @@ class ResourceManager(object): 'T2': 't2.nc', }, ], }, - 'adcirc': { + 'adcirc2015': { 'resource_atts': { 'url': 'http://sms.aquaveo.com/', 'archive': None, # Uncompressed NetCDF file diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index 2c219ba..5e23b3b 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -8,27 +8,39 @@ class Constituents: leprovost_models = ['fes2014', 'leprovost'] def __init__(self, model): - self.current_model = None - self.switch_model(model.lower()) + self._current_model = None + self.change_model(model.lower()) - def switch_model(self, new_model): - if self.current_model and self.current_model.model == new_model: + @property + def data(self): + """:obj:`Pandas.DataFrame` Access the underlying Pandas data frame of this constituent object.""" + if self._current_model: + return self._current_model.data + return [] + + @data.setter + def data(self, value): + if self._current_model: + self._current_model.data = value + + def change_model(self, new_model): + if self._current_model and self._current_model.model == new_model: return # Already have the correct impl for this model, nothing to do. if new_model in self.tpxo_models: # Switch to a TPXO model # If we already have a TPXO impl, change its version if necessary. - if self.current_model and self.current_model.model in self.tpxo_models: - self.current_model.change_model(new_model) + if self._current_model and self._current_model.model in self.tpxo_models: + self._current_model.change_model(new_model) else: # Construct a new TPXO impl. - self.current_model = TpxoDB(new_model) + self._current_model = TpxoDB(new_model) elif new_model in self.leprovost_models: # If we already have a LeProvost impl, change its version if necessary. - if self.current_model and self.current_model.model in self.leprovost_models: - self.current_model.change_model(new_model) + if self._current_model and self._current_model.model in self.leprovost_models: + self._current_model.change_model(new_model) else: # Construct a new LeProvost impl. - self.current_model = LeProvostDB(new_model) - elif new_model == 'adcirc': - self.current_model = AdcircDB() + self._current_model = LeProvostDB(new_model) + elif new_model == 'adcirc2015': + self._current_model = AdcircDB() else: raise ValueError("Model not supported - {}".format(new_model)) @@ -52,9 +64,9 @@ def get_components(self, locs, cons=None, positive_ph=False, model=None): element in locs. Empty list on error. Note that function uses fluent interface pattern. """ - if model and model.lower() != self.current_model.model: - self.switch_model(model.lower()) - return self.current_model.get_components(locs, cons, positive_ph) + if model and model.lower() != self._current_model.model: + self.change_model(model.lower()) + return self._current_model.get_components(locs, cons, positive_ph) def get_nodal_factor(self, names, hour, day, month, year): """Get the nodal factor for specified constituents at a specified time. @@ -72,4 +84,4 @@ def get_nodal_factor(self, names, hour, day, month, year): constituent name. """ - return self.current_model.get_nodal_factor(names, hour, day, month, year) \ No newline at end of file + return self._current_model.get_nodal_factor(names, hour, day, month, year) \ No newline at end of file diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index f92fdfc..872d27e 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -54,6 +54,25 @@ } +def get_complex_components(amps, phases): + """Get the real and imaginary components of amplitudes and phases. + + Args: + amps (:obj:`list` of :obj:`float`): List of constituent amplitudes + phases (:obj:`list` of :obj:`float`): List of constituent phases in radians + + Returns: + :obj:`list` of :obj:`tuple` of :obj:`float`: The list of the complex components, + e.g. [[real1, imag1], [real2, imag2]] + + """ + components = [[0.0, 0.0] for _ in range(len(amps))] + for idx, (amp, phase) in enumerate(zip(amps, phases)): + components[idx][0] = amp * math.cos(phase) + components[idx][1] = amp * math.sin(phase) + return components + + def convert_coords(coords, zero_to_360=False): """Convert latitude coordinates to [-180, 180] or [0, 360]. @@ -126,8 +145,7 @@ def __init__(self, model): """Base class constructor for the tidal extractors Args: - model (str): The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or - 'adcircnepac' + model (str): The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, or 'adcirc2015' """ self.orbit = OrbitVariables() @@ -140,7 +158,7 @@ def __init__(self, model): @property def model(self): - """str: The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost', 'adcircnwat', or 'adcircnepac' + """str: The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost', or 'adcirc2015' When setting the model to a different one than the current, required resources are downloaded. @@ -155,8 +173,7 @@ def change_model(self, model): """Change the extractor model. If different than the current, required resources are downloaded. Args: - model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, 'adcircnwat', or - 'adcircnepac' + model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, or 'adcirc2015' """ model = model.lower() diff --git a/tutorials/python_api/expected_tidal_test.out b/tutorials/python_api/expected_tidal_test.out index dec1253..bb53e51 100644 --- a/tutorials/python_api/expected_tidal_test.out +++ b/tutorials/python_api/expected_tidal_test.out @@ -18,11 +18,11 @@ M2 1.1736 113.8 28.984104 N2 0.1628 50.8 28.439730 S2 0.1318 101.1 30.000000 - amplitude phase speed -K1 0.0 0.0 0.0 -M2 0.0 0.0 0.0 -N2 0.0 0.0 0.0 -S2 0.0 0.0 0.0 + amplitude phase speed +K1 NaN NaN NaN +M2 NaN NaN NaN +N2 NaN NaN NaN +S2 NaN NaN NaN amplitude phase speed K1 0.428306 233.851748 15.041069 diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index d38e18b..3321e99 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -29,7 +29,7 @@ for pt in leprovost_comps.data: f.write(pt.sort_index().to_string() + "\n\n") - ad_atlantic_comps = constituents.get_components(all_points, cons, model='adcirc') + ad_atlantic_comps = constituents.get_components(all_points, cons, model='adcirc2015') f.write("ADCIRC components:\n") for pt in ad_atlantic_comps.data: f.write(pt.sort_index().to_string() + "\n\n") From be1c54655a677bff4799f647ee73282e962d3ca0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 19 Oct 2018 11:20:30 -0600 Subject: [PATCH 21/24] Converted the ResourceManager RESOURCE dictionary values to classes. --- harmonica/adcirc_database.py | 32 +- harmonica/leprovost_database.py | 34 +- harmonica/resource.py | 564 ++++++++++++++++++++------------ harmonica/tidal_constituents.py | 52 +-- harmonica/tidal_database.py | 39 +-- harmonica/tpxo_database.py | 24 +- 6 files changed, 452 insertions(+), 293 deletions(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 7b34c52..75d60be 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -6,18 +6,31 @@ import pandas as pd import xmsinterp_py +from .resource import ResourceManager from .tidal_database import convert_coords, get_complex_components, NOAA_SPEEDS, TidalDB +DEFAULT_ADCIRC_RESOURCE = 'adcirc2015' + + class AdcircDB(TidalDB): """The class for extracting tidal data, specifically amplitude and phases, from an ADCIRC database. """ - def __init__(self): + def __init__(self, model=DEFAULT_ADCIRC_RESOURCE): """Constructor for the ADCIRC tidal database extractor. + Args: + model (:obj:`str`, optional): Name of the ADCIRC tidal database version. Currently defaults to the only + supported release, 'adcirc2015'. Expand resource.adcirc_models for future versions. + """ - super().__init__('adcirc2015') + model = model.lower() + if model not in ResourceManager.ADCIRC_MODELS: + raise ValueError("\'{}\' is not a supported ADCIRC model. Must be one of: {}.".format( + model, ", ".join(ResourceManager.ADCIRC_MODELS).strip() + )) + super().__init__(model) self.resources.download_model(None) def get_components(self, locs, cons=None, positive_ph=False): @@ -74,11 +87,11 @@ def get_components(self, locs, cons=None, positive_ph=False): x3, y3, z3 = mesh_pts[pt_3] x = pt_flip[0] y = pt_flip[1] - # Compute barocentric weights - ta = abs((x2*y3-x3*y2)-(x1*y3-x3*y1)+(x1*y2-x2*y1)) - w1 = ((x-x3)*(y2-y3)+(x2-x3)*(y3-y))/ta - w2 = ((x-x1)*(y3-y1)-(y-y1)*(x3-x1))/ta - w3 = ((y-y1)*(x2-x1)-(x-x1)*(y2-y1))/ta + # Compute barocentric area weights + ta = abs((x2 * y3 - x3 * y2) - (x1 * y3 - x3 * y1) + (x1 * y2 - x2 * y1)) + w1 = ((x - x3) * (y2 - y3) + (x2 - x3) * (y3 - y)) / ta + w2 = ((x - x1) * (y3 - y1) - (y - y1) * (x3 - x1)) / ta + w3 = ((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)) / ta points_and_weights.append((i, (pt_1, pt_2, pt_3), (w1, w2, w3))) else: # Outside domain, return NaN for all constituents for con in cons: @@ -100,6 +113,8 @@ def get_components(self, locs, cons=None, positive_ph=False): # Get the real and imaginary components from the amplitude and phases in the file. It # would be better if these values were stored in the file like TPXO. complex_components = get_complex_components(amps, phases) + + # Perform area weighted interpolation ctr = ( complex_components[0][0] * weights[0] + complex_components[1][0] * weights[1] + @@ -110,8 +125,9 @@ def get_components(self, locs, cons=None, positive_ph=False): complex_components[1][1] * weights[1] + complex_components[2][1] * weights[2] ) - new_amp = math.sqrt(ctr * ctr + cti * cti) + + # Compute interpolated phase if new_amp == 0.0: new_phase = 0.0 else: diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 9ec59f4..2a8768b 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -10,21 +10,31 @@ import pandas as pd +from .resource import ResourceManager from .tidal_database import convert_coords, get_complex_components, NOAA_SPEEDS, TidalDB +DEFAULT_LEPROVOST_RESOURCE = 'leprovost' + + class LeProvostDB(TidalDB): """Extractor class for the LeProvost tidal database. """ - def __init__(self, model="leprovost"): + def __init__(self, model=DEFAULT_LEPROVOST_RESOURCE): """Constructor for the LeProvost tidal database extractor. + + Args: + model (:obj:`str`, optional): Name of the LeProvost tidal database version. Defaults to the freely + distributed but outdated 'leprovost' version. See resource.py for additional supported models. + """ - # Make sure this is a valid LeProvost version ('leprovost' or 'fes2014') - if model.lower() not in ['leprovost', 'fes2014']: - raise ValueError("{} is not a supported LeProvost model. Must be 'leprovost' or 'fes2014'.") - # self.debugger = open("debug.txt", "w") + model = model.lower() # Be case-insensitive + if model not in ResourceManager.LEPROVOST_MODELS: # Check for valid LeProvost model + raise ValueError("\'{}\' is not a supported LeProvost model. Must be one of: {}.".format( + model, ", ".join(ResourceManager.LEPROVOST_MODELS).strip() + )) super().__init__(model) def get_components(self, locs, cons=None, positive_ph=False): @@ -37,8 +47,6 @@ def get_components(self, locs, cons=None, positive_ph=False): not supplied, all valid constituents will be extracted. positive_ph (bool, optional): Indicate if the returned phase should be all positive [0 360] (True) or [-180 180] (False, the default). - model (:obj:`str`, optional): Name of the tidal model to use to query for the data. If not provided, current - model will be used. If a model other than the current is provided, current model is switched. Returns: :obj:`list` of :obj:`pandas.DataFrame`: A list of dataframes of constituent information including @@ -59,7 +67,7 @@ def get_components(self, locs, cons=None, positive_ph=False): return self # ERROR: Not in latitude/longitude self.data = [pd.DataFrame(columns=['amplitude', 'phase', 'speed']) for _ in range(len(locs))] - dataset_atts = self.resources.model_atts['dataset_atts'] + dataset_atts = self.resources.model_atts.dataset_attributes() n_lat = dataset_atts['num_lats'] n_lon = dataset_atts['num_lons'] @@ -69,9 +77,9 @@ def get_components(self, locs, cons=None, positive_ph=False): d_lon = 360.0 / n_lon for file_idx, d in enumerate(self.resources.get_datasets(cons)): - if self.model == 'leprovost': + if self.model == 'leprovost': # All constituents in one file with constituent name dataset. nc_names = [x.strip().upper() for x in d.spectrum.values[0]] - else: + else: # FES2014 has seperate files for each constituent with no constituent name dataset. # TODO: Probably need to find a better way to get the constituent name. _file_obj is undocumented, so # TODO: there is no guarantee this functionality will be maintained. nc_names = [os.path.splitext(os.path.basename(dset.ds.filepath()))[0].upper() for @@ -138,7 +146,7 @@ def get_components(self, locs, cons=None, positive_ph=False): phases=[xlo_yhi_phase, xhi_yhi_phase, xlo_ylo_phase, xhi_ylo_phase], ) - # Perform bi-linear interpolation of the corners to the target point. + # Perform bi-linear interpolation from the four cell corners to the target point. xcos = 0.0 xsin = 0.0 denom = 0.0 @@ -158,11 +166,11 @@ def get_components(self, locs, cons=None, positive_ph=False): xcos = xcos + complex_comps[3][0] * (1.0 - yratio) * xratio xsin = xsin + complex_comps[3][1] * (1.0 - yratio) * xratio denom = denom + (1.0 - yratio) * xratio - xcos = xcos / denom xsin = xsin / denom - amp = math.sqrt(xcos * xcos + xsin * xsin) + + # Compute interpolated phase phase = math.degrees(math.acos(xcos / amp)) amp /= 100.0 if xsin < 0.0: diff --git a/harmonica/resource.py b/harmonica/resource.py index e0209f4..4f9d504 100644 --- a/harmonica/resource.py +++ b/harmonica/resource.py @@ -1,3 +1,4 @@ +from abc import ABCMeta, abstractmethod import os import shutil import urllib.request @@ -8,213 +9,351 @@ from harmonica import config +class Resources(object): + """Abstract base class for model resources + + """ + def __init__(self): + """Base constructor + + """ + pass + + __metaclass__ = ABCMeta + + @abstractmethod + def resource_attributes(self): + """Get the resource attributes of a model (e.g. web url, compression type) + + Returns: + dict: Dictionary of model resource attributes + + """ + return {} + + @abstractmethod + def dataset_attributes(self): + """Get the dataset attributes of a model (e.g. unit multiplier, grid dimensions) + + Returns: + dict: Dictionary of model dataset attributes + + """ + return {} + + @abstractmethod + def available_constituents(self): + """Get all the available constituents of a model + + Returns: + list: List of all the available constituents + + """ + return [] + + @abstractmethod + def constituent_groups(self): + """Get all the available constituents of a model grouped by compatible file types + + Returns: + list(list): 2-D list of available constituents, where the first dimension groups compatible files + + """ + return [] + + @abstractmethod + def constituent_resource(self, con): + """Get the resource name of a constituent + + Returns: + str: Name of the constituent's resource + + """ + return None + + +class Tpxo7Resources(Resources): + """TPXO7 resources + + """ + TPXO7_CONS = {'K1', 'K2', 'M2', 'M4', 'MF', 'MM', 'MN4', 'MS4', 'N2', 'O1', 'P1', 'Q1', 'S2'} + DEFAULT_RESOURCE_FILE = 'DATA/h_tpxo7.2.nc' + + def __init__(self): + super().__init__() + + def resource_attributes(self): + return { + 'url': 'ftp://ftp.oce.orst.edu/dist/tides/Global/tpxo7.2_netcdf.tar.Z', + 'archive': 'gz', # gzip compression + } + + def dataset_attributes(self): + return { + 'units_multiplier': 1.0, # meter + } + + def available_constituents(self): + return self.TPXO7_CONS + + def constituent_groups(self): + return [self.available_constituents()] + + def constituent_resource(self, con): + if con.upper() in self.TPXO7_CONS: + return self.DEFAULT_RESOURCE_FILE + else: + return None + + +class Tpxo8Resources(Resources): + """TPXO8 resources + + """ + TPXO8_CONS = [ + { # 1/30 degree + 'K1': 'hf.k1_tpxo8_atlas_30c_v1.nc', + 'K2': 'hf.k2_tpxo8_atlas_30c_v1.nc', + 'M2': 'hf.m2_tpxo8_atlas_30c_v1.nc', + 'M4': 'hf.m4_tpxo8_atlas_30c_v1.nc', + 'N2': 'hf.n2_tpxo8_atlas_30c_v1.nc', + 'O1': 'hf.o1_tpxo8_atlas_30c_v1.nc', + 'P1': 'hf.p1_tpxo8_atlas_30c_v1.nc', + 'Q1': 'hf.q1_tpxo8_atlas_30c_v1.nc', + 'S2': 'hf.s2_tpxo8_atlas_30c_v1.nc', + }, + { # 1/6 degree + 'MF': 'hf.mf_tpxo8_atlas_6.nc', + 'MM': 'hf.mm_tpxo8_atlas_6.nc', + 'MN4': 'hf.mn4_tpxo8_atlas_6.nc', + 'MS4': 'hf.ms4_tpxo8_atlas_6.nc', + }, + ] + + def __init__(self): + super().__init__() + + def resource_attributes(self): + return { + 'url': "ftp://ftp.oce.orst.edu/dist/tides/TPXO8_atlas_30_v1_nc/", + 'archive': None, + } + + def dataset_attributes(self): + return { + 'units_multiplier': 0.001, # mm to meter + } + + def available_constituents(self): + # get keys from const groups as list of lists and flatten + return [c for sl in [grp.keys() for grp in self.TPXO8_CONS] for c in sl] + + def constituent_groups(self): + return [self.TPXO8_CONS[0], self.TPXO8_CONS[1]] + + def constituent_resource(self, con): + con = con.upper() + for group in self.TPXO8_CONS: + if con in group: + return group[con] + return None + + +class Tpxo9Resources(Resources): + """TPXO9 resources + + """ + TPXO9_CONS = {'2N2', 'K1', 'K2', 'M2', 'M4', 'MF', 'MM', 'MN4', 'MS4', 'N2', 'O1', 'P1', 'Q1', 'S1', 'S2'} + DEFAULT_RESOURCE_FILE = 'tpxo9_netcdf/h_tpxo9.v1.nc' + + def __init__(self): + super().__init__() + + def resource_attributes(self): + return { + 'url': "ftp://ftp.oce.orst.edu/dist/tides/Global/tpxo9_netcdf.tar.gz", + 'archive': 'gz', + } + + def dataset_attributes(self): + return { + 'units_multiplier': 1.0, # meter + } + + def available_constituents(self): + return self.TPXO9_CONS + + def constituent_groups(self): + return [self.available_constituents()] + + def constituent_resource(self, con): + if con.upper() in self.TPXO9_CONS: + return self.DEFAULT_RESOURCE_FILE + else: + return None + + +class LeProvostResources(Resources): + """LeProvost resources + + """ + LEPROVOST_CONS = {'K1', 'K2', 'M2', 'N2', 'O1', 'P1', 'Q1', 'S2', 'NU2', 'MU2', '2N2', 'T2', 'L2'} + DEFAULT_RESOURCE_FILE = 'leprovost_tidal_db.nc' + + def __init__(self): + super().__init__() + + def resource_attributes(self): + return { + 'url': 'http://sms.aquaveo.com/leprovost_tidal_db.zip', + 'archive': 'zip', # zip compression + } + + def dataset_attributes(self): + return { + 'units_multiplier': 1.0, # meter + 'num_lats': 361, + 'num_lons': 720, + 'min_lon': -180.0, + } + + def available_constituents(self): + return self.LEPROVOST_CONS + + def constituent_groups(self): + return [self.available_constituents()] + + def constituent_resource(self, con): + if con.upper() in self.LEPROVOST_CONS: + return self.DEFAULT_RESOURCE_FILE + else: + return None + + +class FES2014Resources(Resources): + """FES2014 resources + + """ + FES2014_CONS = { + '2N2': '2n2.nc', + 'EPS2': 'eps2.nc', + 'J1': 'j1.nc', + 'K1': 'k1.nc', + 'K2': 'k2.nc', + 'L2': 'l2.nc', + 'LA2': 'la2.nc', + 'M2': 'm2.nc', + 'M3': 'm3.nc', + 'M4': 'm4.nc', + 'M6': 'm6.nc', + 'M8': 'm8.nc', + 'MF': 'mf.nc', + 'MKS2': 'mks2.nc', + 'MM': 'mm.nc', + 'MN4': 'mn4.nc', + 'MS4': 'ms4.nc', + 'MSF': 'msf.nc', + 'MSQM': 'msqm.nc', + 'MTM': 'mtm.nc', + 'MU2': 'mu2.nc', + 'N2': 'n2.nc', + 'N4': 'n4.nc', + 'NU2': 'nu2.nc', + 'O1': 'o1.nc', + 'P1': 'p1.nc', + 'Q1': 'q1.nc', + 'R2': 'r2.nc', + 'S1': 's1.nc', + 'S2': 's2.nc', + 'S4': 's4.nc', + 'SA': 'sa.nc', + 'SSA': 'ssa.nc', + 'T2': 't2.nc', + } + + def __init__(self): + super().__init__() + + def resource_attributes(self): + return { + 'url': None, # Resources must already exist. Licensing restrictions prevent hosting files. + 'archive': None, + } + + def dataset_attributes(self): + return { + 'units_multiplier': 1.0, # meter + 'num_lats': 2881, + 'num_lons': 5760, + 'min_lon': 0.0, + } + + def available_constituents(self): + return self.FES2014_CONS.keys() + + def constituent_groups(self): + return [self.available_constituents()] + + def constituent_resource(self, con): + con = con.upper() + if con in self.FES2014_CONS: + return self.FES2014_CONS[con] + else: + return None + + +class Adcirc2015Resources(Resources): + """ADCIRC (v2015) resources + + """ + ADCIRC_CONS = { + 'M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'Q1', 'K2', 'L2', '2N2', 'R2', 'T2', 'LAMBDA2', 'MU2', + 'NU2', 'J1', 'M1', 'OO1', 'P1', '2Q1', 'RHO1', 'M8', 'S4', 'S6', 'M3', 'S1', 'MK3', '2MK3', 'MN4', + 'MS4', '2SM2', 'MF', 'MSF', 'MM', 'SA', 'SSA' + } + DEFAULT_RESOURCE_FILE = 'all_adcirc.nc' + + def __init__(self): + super().__init__() + + def resource_attributes(self): + return { + 'url': 'http://sms.aquaveo.com/', + 'archive': None, # Uncompressed NetCDF file + } + + def dataset_attributes(self): + return { + 'units_multiplier': 1.0, # meter + } + + def available_constituents(self): + return self.ADCIRC_CONS + + def constituent_groups(self): + return [self.available_constituents()] + + def constituent_resource(self, con): + if con.upper() in self.ADCIRC_CONS: + return self.DEFAULT_RESOURCE_FILE + else: + return None + + class ResourceManager(object): """Harmonica resource manager to retrieve and access tide models""" - # Dictionary of model information RESOURCES = { - 'tpxo9': { - 'resource_atts': { - 'url': "ftp://ftp.oce.orst.edu/dist/tides/Global/tpxo9_netcdf.tar.gz", - 'archive': 'gz', - }, - 'dataset_atts': { - 'units_multiplier': 1., # meters - }, - 'consts': [{ # grouped by dimensionally compatible files - '2N2': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'K1': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'K2': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'M2': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'M4': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'MF': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'MM': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'MN4': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'MS4': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'N2': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'O1': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'P1': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'Q1': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'S1': 'tpxo9_netcdf/h_tpxo9.v1.nc', - 'S2': 'tpxo9_netcdf/h_tpxo9.v1.nc', - }, ], - }, - 'tpxo8': { - 'resource_atts': { - 'url': "ftp://ftp.oce.orst.edu/dist/tides/TPXO8_atlas_30_v1_nc/", - 'archive': None, - }, - 'dataset_atts': { - 'units_multiplier': 0.001, # mm to meter - }, - 'consts': [ # grouped by dimensionally compatible files - { # 1/30 degree - 'K1': 'hf.k1_tpxo8_atlas_30c_v1.nc', - 'K2': 'hf.k2_tpxo8_atlas_30c_v1.nc', - 'M2': 'hf.m2_tpxo8_atlas_30c_v1.nc', - 'M4': 'hf.m4_tpxo8_atlas_30c_v1.nc', - 'N2': 'hf.n2_tpxo8_atlas_30c_v1.nc', - 'O1': 'hf.o1_tpxo8_atlas_30c_v1.nc', - 'P1': 'hf.p1_tpxo8_atlas_30c_v1.nc', - 'Q1': 'hf.q1_tpxo8_atlas_30c_v1.nc', - 'S2': 'hf.s2_tpxo8_atlas_30c_v1.nc', - }, - { # 1/6 degree - 'MF': 'hf.mf_tpxo8_atlas_6.nc', - 'MM': 'hf.mm_tpxo8_atlas_6.nc', - 'MN4': 'hf.mn4_tpxo8_atlas_6.nc', - 'MS4': 'hf.ms4_tpxo8_atlas_6.nc', - }, - ], - }, - 'tpxo7': { - 'resource_atts': { - 'url': "ftp://ftp.oce.orst.edu/dist/tides/Global/tpxo7.2_netcdf.tar.Z", - 'archive': 'gz', # gzip compression - }, - 'dataset_atts': { - 'units_multiplier': 1., # meter - }, - 'consts': [{ # grouped by dimensionally compatible files - 'K1': 'DATA/h_tpxo7.2.nc', - 'K2': 'DATA/h_tpxo7.2.nc', - 'M2': 'DATA/h_tpxo7.2.nc', - 'M4': 'DATA/h_tpxo7.2.nc', - 'MF': 'DATA/h_tpxo7.2.nc', - 'MM': 'DATA/h_tpxo7.2.nc', - 'MN4': 'DATA/h_tpxo7.2.nc', - 'MS4': 'DATA/h_tpxo7.2.nc', - 'N2': 'DATA/h_tpxo7.2.nc', - 'O1': 'DATA/h_tpxo7.2.nc', - 'P1': 'DATA/h_tpxo7.2.nc', - 'Q1': 'DATA/h_tpxo7.2.nc', - 'S2': 'DATA/h_tpxo7.2.nc', - }, ], - }, - 'leprovost': { - 'resource_atts': { - 'url': 'http://sms.aquaveo.com/leprovost_tidal_db.zip', - 'archive': 'zip', # zip compression - }, - 'dataset_atts': { - 'units_multiplier': 1., # meter - 'num_lats': 361, - 'num_lons': 720, - 'min_lon': -180.0 - }, - 'consts': [{ # grouped by dimensionally compatible files - 'K1': 'leprovost_tidal_db.nc', - 'K2': 'leprovost_tidal_db.nc', - 'M2': 'leprovost_tidal_db.nc', - 'N2': 'leprovost_tidal_db.nc', - 'O1': 'leprovost_tidal_db.nc', - 'P1': 'leprovost_tidal_db.nc', - 'Q1': 'leprovost_tidal_db.nc', - 'S2': 'leprovost_tidal_db.nc', - 'NU2': 'leprovost_tidal_db.nc', - 'MU2': 'leprovost_tidal_db.nc', - '2N2': 'leprovost_tidal_db.nc', - 'T2': 'leprovost_tidal_db.nc', - 'L2': 'leprovost_tidal_db.nc', - }, ], - }, - 'fes2014': { # Resources must already exist. Licensing restrictions prevent - 'resource_atts': { - 'url': None, - 'archive': None, - }, - 'dataset_atts': { - 'units_multiplier': 1., # meter - 'num_lats': 2881, - 'num_lons': 5760, - 'min_lon': 0.0 - }, - 'consts': [{ # grouped by dimensionally compatible files - '2N2': '2n2.nc', - 'EPS2': 'eps2.nc', - 'J1': 'j1.nc', - 'K1': 'k1.nc', - 'K2': 'k2.nc', - 'L2': 'l2.nc', - 'LA2': 'la2.nc', - 'M2': 'm2.nc', - 'M3': 'm3.nc', - 'M4': 'm4.nc', - 'M6': 'm6.nc', - 'M8': 'm8.nc', - 'MF': 'mf.nc', - 'MKS2': 'mks2.nc', - 'MM': 'mm.nc', - 'MN4': 'mn4.nc', - 'MS4': 'ms4.nc', - 'MSF': 'msf.nc', - 'MSQM': 'msqm.nc', - 'MTM': 'mtm.nc', - 'MU2': 'mu2.nc', - 'N2': 'n2.nc', - 'N4': 'n4.nc', - 'NU2': 'nu2.nc', - 'O1': 'o1.nc', - 'P1': 'p1.nc', - 'Q1': 'q1.nc', - 'R2': 'r2.nc', - 'S1': 's1.nc', - 'S2': 's2.nc', - 'S4': 's4.nc', - 'SA': 'sa.nc', - 'SSA': 'ssa.nc', - 'T2': 't2.nc', - }, ], - }, - 'adcirc2015': { - 'resource_atts': { - 'url': 'http://sms.aquaveo.com/', - 'archive': None, # Uncompressed NetCDF file - }, - 'dataset_atts': { - 'units_multiplier': 1., # meter - }, - 'consts': [{ # grouped by dimensionally compatible files - 'M2': 'all_adcirc.nc', - 'S2': 'all_adcirc.nc', - 'N2': 'all_adcirc.nc', - 'K1': 'all_adcirc.nc', - 'M4': 'all_adcirc.nc', - 'O1': 'all_adcirc.nc', - 'M6': 'all_adcirc.nc', - 'Q1': 'all_adcirc.nc', - 'K2': 'all_adcirc.nc', - 'L2': 'all_adcirc.nc', - '2N2': 'all_adcirc.nc', - 'R2': 'all_adcirc.nc', - 'T2': 'all_adcirc.nc', - 'LAMBDA2': 'all_adcirc.nc', - 'MU2': 'all_adcirc.nc', - 'NU2': 'all_adcirc.nc', - 'J1': 'all_adcirc.nc', - 'M1': 'all_adcirc.nc', - 'OO1': 'all_adcirc.nc', - 'P1': 'all_adcirc.nc', - '2Q1': 'all_adcirc.nc', - 'RHO1': 'all_adcirc.nc', - 'M8': 'all_adcirc.nc', - 'S4': 'all_adcirc.nc', - 'S6': 'all_adcirc.nc', - 'M3': 'all_adcirc.nc', - 'S1': 'all_adcirc.nc', - 'MK3': 'all_adcirc.nc', - '2MK3': 'all_adcirc.nc', - 'MN4': 'all_adcirc.nc', - 'MS4': 'all_adcirc.nc', - '2SM2': 'all_adcirc.nc', - 'MF': 'all_adcirc.nc', - 'MSF': 'all_adcirc.nc', - 'MM': 'all_adcirc.nc', - 'SA': 'all_adcirc.nc', - 'SSA': 'all_adcirc.nc', - }, ], - }, + 'tpxo7': Tpxo7Resources(), + 'tpxo8': Tpxo8Resources(), + 'tpxo9': Tpxo9Resources(), + 'leprovost': LeProvostResources(), + 'fes2014': FES2014Resources(), + 'adcirc2015': Adcirc2015Resources(), } + TPXO_MODELS = {'tpxo7', 'tpxo8', 'tpxo9'} + LEPROVOST_MODELS = {'fes2014', 'leprovost'} + ADCIRC_MODELS = {'adcirc2015'} DEFAULT_RESOURCE = 'tpxo9' def __init__(self, model=DEFAULT_RESOURCE): @@ -229,18 +368,17 @@ def __del__(self): d.close() def available_constituents(self): - # get keys from const groups as list of lists and flatten - return [c for sl in [grp.keys() for grp in self.model_atts['consts']] for c in sl] + return self.model_atts.available_constituents() def get_units_multiplier(self): - return self.model_atts['dataset_atts']['units_multiplier'] + return self.model_atts.dataset_attributes()['units_multiplier'] def download(self, resource, destination_dir): """Download a specified model resource.""" if not os.path.isdir(destination_dir): os.makedirs(destination_dir) - rsrc_atts = self.model_atts['resource_atts'] + rsrc_atts = self.model_atts.resource_attributes() url = rsrc_atts['url'] # Check if we can download resources for this model. if url is None: @@ -261,7 +399,10 @@ def download(self, resource, destination_dir): except IOError as e: print(str(e)) else: - rsrcs = set(c for sl in [x.values() for x in self.model_atts['consts']] for c in sl) + rsrcs = set( + self.model_atts.constituent_resource(con) for con in + self.model_atts.available_constituents() + ) tar.extractall(path=destination_dir, members=[m for m in tar.getmembers() if m.name in rsrcs]) tar.close() elif rsrc_atts['archive'] == 'zip': # Unzip .zip files @@ -283,7 +424,10 @@ def download(self, resource, destination_dir): def download_model(self, resource_dir=None): """Download all of the model's resources for later use.""" - resources = set(r for sl in [grp.values() for grp in self.model_atts['consts']] for r in sl) + resources = set( + self.model_atts.constituent_resource(con) for con in + self.model_atts.available_constituents() + ) if not resource_dir: resource_dir = os.path.join(config['data_dir'], self.model) for r in resources: @@ -307,8 +451,8 @@ def get_datasets(self, constituents): raise ValueError('Constituent not recognized.') # handle compatible files together self.datasets = [] - for const_group in self.model_atts['consts']: - rsrcs = set(const_group[const] for const in set(constituents) & set(const_group)) + for const_group in self.model_atts.constituent_groups(): + rsrcs = set(self.model_atts.constituent_resource(const) for const in set(constituents) & set(const_group)) paths = set() if config['pre_existing_data_dir']: diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index 5e23b3b..10f9bcf 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -1,15 +1,27 @@ from .adcirc_database import AdcircDB from .leprovost_database import LeProvostDB +from .resource import ResourceManager from .tpxo_database import TpxoDB class Constituents: - tpxo_models = ['tpxo7', 'tpxo8', 'tpxo9'] - leprovost_models = ['fes2014', 'leprovost'] + """Class for extracting tidal constituent data - def __init__(self, model): + Attributes: + _current_model (:obj:`tidal_database.TidalDB`): The tidal model currently being used for extraction + + """ + def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): + """Constructor tidal constituent extractor interface. + + Use this class as opposed to the lower level implementations + + Args: + model (:obj:`str`, optional): Name of the tidal model. See resource.py for supported models. + + """ self._current_model = None - self.change_model(model.lower()) + self.change_model(model) @property def data(self): @@ -24,25 +36,25 @@ def data(self, value): self._current_model.data = value def change_model(self, new_model): + new_model = new_model.lower() if self._current_model and self._current_model.model == new_model: - return # Already have the correct impl for this model, nothing to do. - - if new_model in self.tpxo_models: # Switch to a TPXO model - # If we already have a TPXO impl, change its version if necessary. - if self._current_model and self._current_model.model in self.tpxo_models: - self._current_model.change_model(new_model) - else: # Construct a new TPXO impl. - self._current_model = TpxoDB(new_model) - elif new_model in self.leprovost_models: - # If we already have a LeProvost impl, change its version if necessary. - if self._current_model and self._current_model.model in self.leprovost_models: - self._current_model.change_model(new_model) - else: # Construct a new LeProvost impl. - self._current_model = LeProvostDB(new_model) - elif new_model == 'adcirc2015': + return # Already have the correct impl and resources for this model, nothing to do. + + if new_model in ResourceManager.TPXO_MODELS: # Switch to a TPXO model + self._current_model = TpxoDB(new_model) + elif new_model in ResourceManager.LEPROVOST_MODELS: + self._current_model = LeProvostDB(new_model) + elif new_model in ResourceManager.ADCIRC_MODELS: self._current_model = AdcircDB() else: - raise ValueError("Model not supported - {}".format(new_model)) + supported_models = ( + ", ".join(ResourceManager.TPXO_MODELS) + ", " + + ", ".join(ResourceManager.LEPROVOST_MODELS) + ", " + + ", ".join(ResourceManager.ADCIRC_MODELS) + ) + raise ValueError("Model not supported: \'{}\'. Must be one of: {}.".format( + new_model, supported_models.strip() + )) def get_components(self, locs, cons=None, positive_ph=False, model=None): """Abstract method to get amplitude, phase, and speed of specified constituents at specified point locations. diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index 872d27e..68e2a1e 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -145,45 +145,20 @@ def __init__(self, model): """Base class constructor for the tidal extractors Args: - model (str): The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, or 'adcirc2015' + model (str): The name of the model. See resource.py for supported models. """ self.orbit = OrbitVariables() + # constituent information dataframe: + # amplitude (meters) + # phase (degrees) + # speed (degrees/hour, UTC/GMT) self.data = [] - self.resources = None - self._model = None - self.change_model(model) + self.model = model + self.resources = ResourceManager(self.model) __metaclass__ = ABCMeta - @property - def model(self): - """str: The name of the model. One of 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost', or 'adcirc2015' - - When setting the model to a different one than the current, required resources are downloaded. - - """ - return self._model - - @model.setter - def model(self, value): - self.change_model(value) - - def change_model(self, model): - """Change the extractor model. If different than the current, required resources are downloaded. - - Args: - model (str): The name of the model. One of: 'tpxo9', 'tpxo8', 'tpxo7', 'leprovost, or 'adcirc2015' - - """ - model = model.lower() - if model == 'tpxo7_2': - model = 'tpxo7' - - if model != self._model: - self._model = model - self.resources = ResourceManager(self._model) - @abstractmethod def get_components(self, locs, cons, positive_ph): """Abstract method to get amplitude, phase, and speed of specified constituents at specified point locations. diff --git a/harmonica/tpxo_database.py b/harmonica/tpxo_database.py index 764abfa..fe3c38e 100644 --- a/harmonica/tpxo_database.py +++ b/harmonica/tpxo_database.py @@ -1,25 +1,29 @@ -from .resource import ResourceManager -from .tidal_database import NOAA_SPEEDS, TidalDB - from bisect import bisect import numpy as np import pandas as pd +from .resource import ResourceManager +from .tidal_database import NOAA_SPEEDS, TidalDB + + +DEFAULT_TPXO_RESOURCE = 'tpxo9' + class TpxoDB(TidalDB): """Harmonica tidal constituents.""" - def __init__(self, model=ResourceManager.DEFAULT_RESOURCE): + def __init__(self, model=DEFAULT_TPXO_RESOURCE): """Constructor for the TPXO tidal extractor. Args: - model (:obj:`str`, optional): The name of the TPXO model. One of: 'tpxo9', 'tpxo8', 'tpxo7'. - ResourceManager.DEFAULT_RESOURCE if not specified. + model (:obj:`str`, optional): The name of the TPXO model. See resource.py for supported models. + """ - # constituent information dataframe: - # amplitude (meters) - # phase (degrees) - # speed (degrees/hour, UTC/GMT) + model = model.lower() # Be case-insensitive + if model not in ResourceManager.TPXO_MODELS: # Check for valid TPXO model + raise ValueError("\'{}\' is not a supported TPXO model. Must be one of: {}.".format( + model, ", ".join(ResourceManager.TPXO_MODELS).strip() + )) super().__init__(model) def get_components(self, locs, cons=None, positive_ph=False): From 44e85956c88070b87f251e440d7f91c0d10fb505 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 19 Oct 2018 11:54:48 -0600 Subject: [PATCH 22/24] Fix CLI functions to work with the list of pandas.DataFrame from Constituent object as opposed to a single DataFrame. Once this becomes the outer dimension of an xarray, this will probably need to change again. --- harmonica/cli/main_constituents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/harmonica/cli/main_constituents.py b/harmonica/cli/main_constituents.py index 4105268..194db5e 100644 --- a/harmonica/cli/main_constituents.py +++ b/harmonica/cli/main_constituents.py @@ -38,9 +38,9 @@ def parse_args(args): def execute(args): cons = Constituents(model=args.model).get_components( - [args.lat, args.lon], cons=args.cons, positive_ph=args.positive_phase + [(args.lat, args.lon)], cons=args.cons, positive_ph=args.positive_phase ) - out = cons.data.to_csv(args.output, sep='\t', header=True, index=True, index_label='constituent') + out = cons.data[0].to_csv(args.output, sep='\t', header=True, index=True, index_label='constituent') if args.output is None: print(out) print("\nComplete.\n") From f9ceac05e0cc8cc3ba00e0345ba1cad341c2c4f0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 19 Oct 2018 16:56:03 -0600 Subject: [PATCH 23/24] Use pytides to get astronomical variables. Removed some unused methods as a result. Changed interface to take datetime object instead of year, month, etc. Updated tutorial. --- doc/source/simple_python.rst | 66 +-- harmonica/adcirc_database.py | 2 +- harmonica/leprovost_database.py | 2 +- harmonica/tidal_constituents.py | 11 +- harmonica/tidal_database.py | 513 ++++++++----------- harmonica/tpxo_database.py | 2 +- tutorials/python_api/expected_tidal_test.out | 41 +- tutorials/python_api/test.py | 13 +- 8 files changed, 262 insertions(+), 388 deletions(-) diff --git a/doc/source/simple_python.rst b/doc/source/simple_python.rst index 84fa1f1..4a71c27 100644 --- a/doc/source/simple_python.rst +++ b/doc/source/simple_python.rst @@ -2,11 +2,10 @@ Using the Python API ===================================== This tutorial demonstrates the use of the hamonica Python API. Amplitude, phase, -and speed tidal harmonics are extracted for a small set of points from the TPXO8, ADCIRC -(Northwest Atlantic and Northeast Pacific), and LeProvost tidal databases. The tutorial -also provides an example of obtaining the frequency, earth tidal reduction factor, amplitude, -nodal factor, and equilibrium argument of a constituent at a specified time using the ADCIRC -and LeProvost tidal databases. +and speed tidal harmonics are extracted for a small set of points from the TPXO8, ADCIRC, +and LeProvost tidal databases. The tutorial also provides an example of obtaining the +amplitude, frequency, speed, earth tidal reduction factor, equilibrium argument, and +nodal factor of a constituent at a specified time. To complete this tutorial you must have the harmonica package installed. See :ref:`Installation` for instructions on installing the harmonica package to a Python environment. @@ -18,47 +17,56 @@ The code is also provided below. :linenos: Executing :file:`test.py` will fetch the required tidal resources from the Internet if -they do not already exist in the working directory. To specify existing resources use the -following command line arguments: +they do not already exist in the harmonica data directory. Edit the config variables in +:file:`__init__.py` to change the data directory or specify a preexisting data directory. --l path Path to the LeProvost \*.legi files --a file Path and filename of the ADCIRC Northwest Atlantic executable --p file Path and filename of the ADCIRC Northeast Pacific executable +After executing the script, output from the tidal harmonic extraction can be viewed in +:file:`tidal_test.out` located in the Python API tutorial directory. -You may also specify a temporary working directory that will be used by the ADCIRC -database extractors (default is current working directory). +The following lines set up the point locations where tidal harmonic components will be +extracted. Locations should be specified as tuples of latitude and longitude degrees. --w path Path ADCIRC extractors will use as a temporary working directory +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 7-12 + :lineno-match: -After executing the script, output from the tidal harmonic extraction can be viewed in -:file:`tidal_test.out` located in the Python API tutorial directory. +In addition to the points of interest, create a list of constituents to query for. + +.. literalinclude:: ../../tutorials/python_api/test.py + :lines: 14-14 + :lineno-match: -The following lines set up the point locations we will be extracting tidal harmonic -components for. Locations should be specified as tuples of latitude and longitude -degrees. Note that we have split locations in the Atlantic and Pacific. LeProvost and -TPXO support all ocean locations, but the ADCIRC database is restricted to one or the other. +Next, construct the tidal constituent extractor interface. This example starts out by +using the 'leprovost' model. 'tpxo8' is the default model used when none is specified. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 19-33 + :lines: 15-15 :lineno-match: -The next section of code sets up the tidal harmonic extraction interfaces. For the ADCIRC -extractors, the tidal region must be specified at construction. +The next section of code demonstrates using the interface to get amplitudes, frequencies, +speeds, earth tidal reduction factors, equilibrium arguments, and nodal factors for +specified constituents at a specified time. The tidal model specified at construction has +no effect on this functionality. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 36-43 + :lines: 17-18 :lineno-match: -The ADCIRC and LeProvost extractors can also provide frequencies, earth tidal reduction factors, amplitudes, nodal factors, -and equilibrium arguments for specified constituents at a specified time. The next section of code demonstrates this. +The last block uses the ADCIRC, LeProvost, and TPXO interfaces to extract tidal harmonic +constituents for a list of locations and constituents. The optional 'model' argument can +be specified to switch between tidal models. Note that when getting the TPXO components, +we pass in a third positional argument (kwarg is 'positive_ph'). If True, all output +phases will be positive. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 48-51 + :lines: 25-42 :lineno-match: -The last block uses the ADCIRC, LeProvost, and TPXO interfaces to extract tidal harmonic constituents for a list of -locations and constituents. +The LeProvost model is freely distributed. FES2014 is an updated version of the model that +significantly increases the grid resolution and number of supported constituents. The +data files for FES2014 cannot be openly distributed, but local copies of the files +can be used if they exist. See :file:`resource.py` for the expected filenames. .. literalinclude:: ../../tutorials/python_api/test.py - :lines: 62-85 + :lines: 44-48 :lineno-match: \ No newline at end of file diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 75d60be..769c280 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -134,7 +134,7 @@ def get_components(self, locs, cons=None, positive_ph=False): new_phase = math.degrees(math.acos(ctr / new_amp)) if cti < 0.0: new_phase = 360.0 - new_phase - speed = NOAA_SPEEDS[con] if con in NOAA_SPEEDS else numpy.nan + speed = NOAA_SPEEDS[con][0] if con in NOAA_SPEEDS else numpy.nan self.data[i].loc[con] = [new_amp, new_phase, speed] return self diff --git a/harmonica/leprovost_database.py b/harmonica/leprovost_database.py index 2a8768b..0e97ef6 100644 --- a/harmonica/leprovost_database.py +++ b/harmonica/leprovost_database.py @@ -176,7 +176,7 @@ def get_components(self, locs, cons=None, positive_ph=False): if xsin < 0.0: phase = 360.0 - phase phase += (360. if positive_ph and phase < 0 else 0) - speed = NOAA_SPEEDS[con] if con in NOAA_SPEEDS else numpy.nan + speed = NOAA_SPEEDS[con][0] if con in NOAA_SPEEDS else numpy.nan self.data[i].loc[con] = [amp, phase, speed] return self diff --git a/harmonica/tidal_constituents.py b/harmonica/tidal_constituents.py index 10f9bcf..cf3c73e 100644 --- a/harmonica/tidal_constituents.py +++ b/harmonica/tidal_constituents.py @@ -80,15 +80,14 @@ def get_components(self, locs, cons=None, positive_ph=False, model=None): self.change_model(model.lower()) return self._current_model.get_components(locs, cons, positive_ph) - def get_nodal_factor(self, names, hour, day, month, year): + def get_nodal_factor(self, names, timestamp, timestamp_middle=None): """Get the nodal factor for specified constituents at a specified time. Args: names (:obj:`list` of :obj:`str`): Names of the constituents to get nodal factors for - hour (float): The hour of the specified time. Can be fractional - day (int): The day of the specified time. - month (int): The month of the specified time. - year (int): The year of the specified time. + timestamp (:obj:`datetime.datetime`): Stat date and time to extract constituent arguments at + timestamp_middle (:obj:`datetime.datetime`, optional): Date and time to consider as the middle of the + series. By default, just uses the start day with half the hours. Returns: :obj:`pandas.DataFrame`: Constituent data frames. Each row contains frequency, earth tidal reduction factor, @@ -96,4 +95,4 @@ def get_nodal_factor(self, names, hour, day, month, year): constituent name. """ - return self._current_model.get_nodal_factor(names, hour, day, month, year) \ No newline at end of file + return self._current_model.get_nodal_factor(names, timestamp, timestamp_middle) diff --git a/harmonica/tidal_database.py b/harmonica/tidal_database.py index 68e2a1e..94d59a1 100644 --- a/harmonica/tidal_database.py +++ b/harmonica/tidal_database.py @@ -1,9 +1,12 @@ #! python3 from abc import ABCMeta, abstractmethod +from datetime import datetime import math +import numpy import pandas as pd +from pytides.astro import astro from .resource import ResourceManager @@ -13,44 +16,45 @@ # Source: https://tidesandcurrents.noaa.gov # The speed is the rate change in the phase of a constituent, and is equal to 360 degrees divided by the # constituent period expressed in hours +# name: (speed, amplitude, frequency, ETRF) NOAA_SPEEDS = { - 'OO1': 16.139101, - '2Q1': 12.854286, - '2MK3': 42.92714, - '2N2': 27.895355, - '2SM2': 31.015896, - 'K1': 15.041069, - 'K2': 30.082138, - 'J1': 15.5854435, - 'L2': 29.528479, - 'LAM2': 29.455626, - 'M1': 14.496694, - 'M2': 28.984104, - 'M3': 43.47616, - 'M4': 57.96821, - 'M6': 86.95232, - 'M8': 115.93642, - 'MF': 1.0980331, - 'MK3': 44.025173, - 'MM': 0.5443747, - 'MN4': 57.423832, - 'MS4': 58.984104, - 'MSF': 1.0158958, - 'MU2': 27.968208, - 'N2': 28.43973, - 'NU2': 28.512583, - 'O1': 13.943035, - 'P1': 14.958931, - 'Q1': 13.398661, - 'R2': 30.041067, - 'RHO': 13.471515, - 'S1': 15.0, - 'S2': 30.0, - 'S4': 60.0, - 'S6': 90.0, - 'SA': 0.0410686, - 'SSA': 0.0821373, - 'T2': 29.958933, + 'OO1': (16.139101, 0.0, 0.00007824457305, 0.069), + '2Q1': (12.854286, 0.0, 0.000062319338107, 0.069), + '2MK3': (42.92714, 0.0, 0.000208116646659, 0.069), + '2N2': (27.895355, 0.0, 0.000135240496464, 0.069), + '2SM2': (31.015896, 0.0, 0.000150369306157, 0.069), + 'K1': (15.041069, 0.141565, 0.000072921158358, 0.736), + 'K2': (30.082138, 0.030704, 0.000145842317201, 0.693), + 'J1': (15.5854435, 0.0, 0.00007556036138, 0.069), + 'L2': (29.528479, 0.0, 0.000143158105531, 0.069), + 'LAM2': (29.455626, 0.0, 0.000142804901311, 0.069), + 'M1': (14.496694, 0.0, 0.000070281955336, 0.069), + 'M2': (28.984104, 0.242334, 0.000140518902509, 0.693), + 'M3': (43.47616, 0.0, 0.000210778353763, 0.069), + 'M4': (57.96821, 0.0, 0.000281037805017, 0.069), + 'M6': (86.95232, 0.0, 0.000421556708011, 0.069), + 'M8': (115.93642, 0.0, 0.000562075610519, 0.069), + 'MF': (1.0980331, 0.0, 0.000005323414692, 0.069), + 'MK3': (44.025173, 0.0, 0.000213440061351, 0.069), + 'MM': (0.5443747, 0.0, 0.000002639203022, 0.069), + 'MN4': (57.423832, 0.0, 0.000278398601995, 0.069), + 'MS4': (58.984104, 0.0, 0.000285963006842, 0.069), + 'MSF': (1.0158958, 0.0, 0.000004925201824, 0.069), + 'MU2': (27.968208, 0.0, 0.000135593700684, 0.069), + 'N2': (28.43973, 0.046398, 0.000137879699487, 0.693), + 'NU2': (28.512583, 0.0, 0.000138232903707, 0.069), + 'O1': (13.943035, 0.100514, 0.000067597744151, 0.695), + 'P1': (14.958931, 0.046834, 0.000072522945975, 0.706), + 'Q1': (13.398661, 0.019256, 0.000064958541129, 0.695), + 'R2': (30.041067, 0.0, 0.000145643201313, 0.069), + 'RHO': (13.471515, 0.0, 0.000065311745349, 0.069), + 'S1': (15.0, 0.0, 0.000072722052166, 0.069), + 'S2': (30.0, 0.112841, 0.000145444104333, 0.693), + 'S4': (60.0, 0.0, 0.000290888208666, 0.069), + 'S6': (90.0, 0.0, 0.000436332312999, 0.069), + 'SA': (0.0410686, 0.0, 0.000000199106191, 0.069), + 'SSA': (0.0821373, 0.0, 0.000000398212868, 0.069), + 'T2': (29.958933, 0.0, 0.000145245007353, 0.069), } @@ -106,24 +110,21 @@ def convert_coords(coords, zero_to_360=False): class OrbitVariables(object): + """Container for variables used in astronomical equations. + + Attributes: + astro (:obj:`pytides.astro.astro`): Orbit variables obtained from pytides. + grterm (:obj:`dict` of :obj:`float`): Dictionary of equilibrium arguments where the key is constituent name + nodfac (:obj:`dict` of :obj:`float`): Dictionary of nodal factors where the key is constituent name + + """ def __init__(self): - self.dh = 0.0 - self.di = 0.0 - self.dn = 0.0 - self.dnu = 0.0 - self.dnup = 0.0 - self.dnup2 = 0.0 - self.dp = 0.0 - self.dp1 = 0.0 - self.dpc = 0.0 - self.ds = 0.0 - self.dxi = 0.0 - self.grterm = NCNST*[0.0] - self.hour = 0.0 - self.nodfac = NCNST*[0.0] - self.day = 0 - self.month = 0 - self.year = 0 + """Construct the container + + """ + self.astro = {} + self.grterm = {con: 0.0 for con in NOAA_SPEEDS} + self.nodfac = {con: 0.0 for con in NOAA_SPEEDS} class TidalDB(object): @@ -138,8 +139,6 @@ class TidalDB(object): """ """(:obj:`list` of :obj:`float`): The starting days of the months (non-leap year).""" day_t = [0.0, 31.0, 59.0, 90.0, 120.0, 151.0, 181.0, 212.0, 243.0, 273.0, 304.0, 334.0] - """(float): PI divided by 180.0""" - pi180 = math.pi/180.0 def __init__(self, model): """Base class constructor for the tidal extractors @@ -192,15 +191,13 @@ def have_constituent(self, name): """ return name.upper() in self.resources.available_constituents() - def get_nodal_factor(self, names, hour, day, month, year): + def get_nodal_factor(self, names, timestamp, timestamp_middle): """Get the nodal factor for specified constituents at a specified time. Args: names (:obj:`list` of :obj:`str`): Names of the constituents to get nodal factors for - hour (float): The hour of the specified time. Can be fractional - day (int): The day of the specified time. - month (int): The month of the specified time. - year (int): The year of the specified time. + timestamp (:obj:`datetime.datetime`): Start date and time to extract constituent arguments at + timestamp_middle (:obj:`datetime.datetime`): Date and time to consider as the middle of the series. Returns: :obj:`pandas.DataFrame`: Constituent data frames. Each row contains frequency, earth tidal reduction factor, @@ -208,68 +205,41 @@ def get_nodal_factor(self, names, hour, day, month, year): constituent name. """ - con_names = ['M2', 'S2', 'N2', 'K1', 'M4', 'O1', 'M6', 'MK3', 'S4', 'MN4', 'NU2', 'S6', 'MU2', '2N2', 'OO1', - 'LAMBDA2', 'S1', 'M1', 'J1', 'MM', 'SSA', 'SA', 'MSF', 'MF', 'RHO1', 'Q1', 'T2', 'R2', '2Q1', - 'P1', '2SM2', 'M3', 'L2', '2MK3', 'K2', 'M8', 'MS4'] - con_freqs = [0.000140518902509, 0.000145444104333, 0.000137879699487, 0.000072921158358, 0.000281037805017, - 0.000067597744151, 0.000421556708011, 0.000213440061351, 0.000290888208666, 0.000278398601995, - 0.000138232903707, 0.000436332312999, 0.000135593700684, 0.000135240496464, 0.000078244573050, - 0.000142804901311, 0.000072722052166, 0.000070281955336, 0.000075560361380, 0.000002639203022, - 0.000000398212868, 0.000000199106191, 0.000004925201824, 0.000005323414692, 0.000065311745349, - 0.000064958541129, 0.000145245007353, 0.000145643201313, 0.000062319338107, 0.000072522945975, - 0.000150369306157, 0.000210778353763, 0.000143158105531, 0.000208116646659, 0.000145842317201, - 0.000562075610519, 0.000285963006842] - con_etrf = [0.0690 for _ in range(NCNST)] - con_etrf[3] = 0.736 # clK1 - con_etrf[5] = 0.695 # O1 - con_etrf[29] = 0.706 # P1 - con_etrf[25] = 0.695 # Q1 - con_etrf[0] = 0.693 # M2 - con_etrf[2] = 0.693 # N2 - con_etrf[1] = 0.693 # S2 - con_etrf[34] = 0.693 # K2 - con_amp = [0.0 for _ in range(NCNST)] - con_amp[3] = 0.141565 # K1 - con_amp[5] = 0.100514 # O1 - con_amp[29] = 0.046834 # P1 - con_amp[25] = 0.019256 # Q1 - con_amp[0] = 0.242334 # M2 - con_amp[2] = 0.046398 # N2 - con_amp[1] = 0.112841 # S2 - con_amp[34] = 0.030704 # K2 - - con_data = pd.DataFrame(columns=["amplitude", "frequency", "earth_tide_reduction_factor", - "equilibrium_argument", "nodal_factor"]) - self.get_eq_args(hour, day, month, year) + con_data = pd.DataFrame(columns=['amplitude', 'frequency', 'speed', 'earth_tide_reduction_factor', + 'equilibrium_argument', 'nodal_factor']) + if not timestamp_middle: + float_hours = timestamp.hour / 2.0 + hour = int(float_hours) + float_minutes = (float_hours - hour) * 60.0 + minute = int(float_minutes) + second = int((float_minutes - minute) * 60.0) + timestamp_middle = datetime(timestamp.year, timestamp.month, timestamp.day, hour, minute, second) + self.get_eq_args(timestamp, timestamp_middle) for idx, name in enumerate(names): name = name.upper() - try: - name_idx = con_names.index(name) - except ValueError: - continue - equilibrium_arg = 0.0 - nodal_factor = 0.0 - if self.orbit.nodfac[name_idx] != 0.0: - nodal_factor = self.orbit.nodfac[name_idx] - equilibrium_arg = self.orbit.grterm[name_idx] - con_data.loc[name] = [con_amp[name_idx], con_freqs[name_idx], con_etrf[name_idx], equilibrium_arg, - nodal_factor] + if name not in NOAA_SPEEDS: + con_data.loc[name] = [numpy.nan, numpy.nan, numpy.nan, numpy.nan, numpy.nan, numpy.nan] + else: + equilibrium_arg = 0.0 + nodal_factor = self.orbit.nodfac[name] + if nodal_factor != 0.0: + equilibrium_arg = self.orbit.grterm[name] + con_data.loc[name] = [ + NOAA_SPEEDS[name][1], NOAA_SPEEDS[name][2], NOAA_SPEEDS[name][0], NOAA_SPEEDS[name][3], + equilibrium_arg, nodal_factor + ] return con_data - def get_eq_args(self, a_hour, a_day, a_month, a_year): + def get_eq_args(self, timestamp, timestamp_middle): """Get equilibrium arguments at a starting time. Args: - a_hour (float): The starting hour. - a_day (int): The starting day. - a_month (int): The starting month. - a_year (int): The starting year. + timestamp (:obj:`datetime.datetime`): Date and time to extract constituent arguments at + timestamp_middle (:obj:`datetime.datetime`): Date and time to consider as the middle of the series """ - day_julian = self.get_day_julian(a_day, a_month, a_year) - hrm = a_hour / 2.0 - self.nfacs(a_year, day_julian, hrm) - self.gterms(a_year, day_julian, a_hour, hrm) + self.nfacs(timestamp_middle) + self.gterms(timestamp, timestamp_middle) @staticmethod def angle(a_number): @@ -289,233 +259,160 @@ def angle(a_number): ret_val -= 360.0 return ret_val - def get_day_julian(self, a_day, a_month, a_year): - """Get a float representing the Julian date. - - Args: - a_day (float): The day. - a_month (float): The month. - a_year (float): The year. - - Returns: - The Julian date with epoch of January 1, 1900. - - """ - days = 12*[0.0] - days[1] = 31.0 - year_offset = int(a_year-1900.0) - yrlp = float(abs(year_offset) % 4) - if year_offset < 0.0: - yrlp *= -1.0 - d_inc = 0.0 - if yrlp == 0.0: - d_inc = 1.0 - for i in range(2, 12): - days[i] = self.day_t[i] + d_inc - return days[int(a_month)-1]+a_day - - def set_orbit(self, a_year, a_day_julian, a_hour): + def set_orbit(self, timestamp): """Determination of primary and secondary orbital functions. Args: - a_year (float): The year. - a_day_julian (float): The day in julian format. - a_hour (float): The hour.. + timestamp (:obj:`datetime.datetime`): Date and time to extract constituent arguments at. """ - x = int((a_year-1901.0)/4.0) - dyr = a_year-1900.0 - dday = a_day_julian+x-1.0 - - # dn IS THE MOON'S NODE (CAPITAL N, TABLE 1, SCHUREMAN) - self.orbit.dn = 259.1560564-19.328185764*dyr-0.0529539336*dday-0.0022064139*a_hour - self.orbit.dn = self.angle(self.orbit.dn) - n = self.orbit.dn*self.pi180 - - # dp IS THE LUNAR PERIGEE (SMALL P, TABLE 1) - self.orbit.dp = 334.3837214+40.66246584*dyr+0.111404016*dday+0.004641834*a_hour - self.orbit.dp = self.angle(self.orbit.dp) - # p = self.orbit.dp*self.pi180 - fi = math.acos(0.9136949-0.0356926*math.cos(n)) - self.orbit.di = self.angle(fi/self.pi180) - - nu = math.asin(0.0897056*math.sin(n)/math.sin(fi)) - self.orbit.dnu = nu/self.pi180 - xi = n-2.0*math.atan(0.64412*math.tan(n/2.0))-nu - self.orbit.dxi = xi/self.pi180 - self.orbit.dpc = self.angle(self.orbit.dp-self.orbit.dxi) - - # dh IS THE MEAN LONGITUDE OF THE SUN (SMALL H, TABLE 1) - self.orbit.dh = 280.1895014-0.238724988*dyr+0.9856473288*dday+0.0410686387*a_hour - self.orbit.dh = self.angle(self.orbit.dh) - - # dp1 IS THE SOLAR PERIGEE (SMALL P1, TABLE 1) - self.orbit.dp1 = 281.2208569+0.01717836*dyr+0.000047064*dday+0.000001961*a_hour - self.orbit.dp1 = self.angle(self.orbit.dp1) - - # ds IS THE MEAN LONGITUDE OF THE MOON (SMALL S, TABLE 1) - self.orbit.ds = 277.0256206+129.38482032*dyr+13.176396768*dday+0.549016532*a_hour - self.orbit.ds = self.angle(self.orbit.ds) - nup = math.atan(math.sin(nu)/(math.cos(nu)+0.334766/math.sin(2.0*fi))) - self.orbit.dnup = nup/self.pi180 - nup2 = math.atan(math.sin(2.0*nu)/(math.cos(2.0*nu)+0.0726184/pow(math.sin(fi), 2.0)))/2.0 - self.orbit.dnup2 = nup2/self.pi180 - - def nfacs(self, a_year, a_day_julian, a_hour): + self.orbit.astro = astro(timestamp) + + def nfacs(self, timestamp): """Calculates node factors for constituent tidal signal. Args: - a_year (float): The year. - a_day_julian (float): The day in julian format. - a_hour (float): The hour. + timestamp (:obj:`datetime.datetime`): Date and time to extract constituent arguments at Returns: - The same values as found in table 14 of schureman. + The same values as found in table 14 of Schureman. """ - self.set_orbit(a_year, a_day_julian, a_hour) - # n = self.orbit.dn*self.pi180 - fi = self.orbit.di*self.pi180 - nu = self.orbit.dnu*self.pi180 - # xi = self.orbit.dxi*self.pi180 - # p = self.orbit.dp*self.pi180 - # pc = self.orbit.dpc*self.pi180 + self.set_orbit(timestamp) + fi = math.radians(self.orbit.astro['i'].value) + nu = math.radians(self.orbit.astro['nu'].value) sini = math.sin(fi) - sini2 = math.sin(fi/2.0) - sin2i = math.sin(2.0*fi) - cosi2 = math.cos(fi/2.0) - # tani2 = math.tan(fi/2.0) + sini2 = math.sin(fi / 2.0) + sin2i = math.sin(2.0 * fi) + cosi2 = math.cos(fi / 2.0) # EQUATION 197, SCHUREMAN # qainv = math.sqrt(2.310+1.435*math.cos(2.0*pc)) # EQUATION 213, SCHUREMAN # rainv = math.sqrt(1.0-12.0*math.pow(tani2, 2)*math.cos(2.0*pc)+36.0*math.pow(tani2, 4)) # VARIABLE NAMES REFER TO EQUATION NUMBERS IN SCHUREMAN - eq73 = (2.0/3.0-math.pow(sini, 2))/0.5021 - eq74 = math.pow(sini, 2.0)/0.1578 - eq75 = sini*math.pow(cosi2, 2.0)/0.37988 - eq76 = math.sin(2*fi)/0.7214 - eq77 = sini*math.pow(sini2, 2.0)/0.0164 - eq78 = math.pow(cosi2, 4.0)/0.91544 - eq149 = math.pow(cosi2, 6.0)/0.8758 - # eq207 = eq75*qainv - # eq215 = eq78*rainv - eq227 = math.sqrt(0.8965*math.pow(sin2i, 2.0)+0.6001*sin2i*math.cos(nu)+0.1006) - eq235 = 0.001+math.sqrt(19.0444*math.pow(sini, 4.0)+2.7702*pow(sini, 2.0)*math.cos(2.0*nu)+0.0981) + eq73 = (2.0 / 3.0 - math.pow(sini, 2)) / 0.5021 + eq74 = math.pow(sini, 2.0) / 0.1578 + eq75 = sini * math.pow(cosi2, 2.0) / 0.37988 + eq76 = math.sin(2 * fi) / 0.7214 + eq77 = sini * math.pow(sini2, 2.0) / 0.0164 + eq78 = math.pow(cosi2, 4.0) / 0.91544 + eq149 = math.pow(cosi2, 6.0) / 0.8758 + # eq207 = eq75 * qainv + # eq215 = eq78 * rainv + eq227 = math.sqrt(0.8965 * math.pow(sin2i, 2.0) + 0.6001 * sin2i * math.cos(nu) + 0.1006) + eq235 = 0.001 + math.sqrt(19.0444 * math.pow(sini, 4.0) + 2.7702 * pow(sini, 2.0) * math.cos(2.0 * nu) + 0.0981) # NODE FACTORS FOR 37 CONSTITUENTS: - self.orbit.nodfac[0] = eq78 - self.orbit.nodfac[1] = 1.0 - self.orbit.nodfac[2] = eq78 - self.orbit.nodfac[3] = eq227 - self.orbit.nodfac[4] = math.pow(self.orbit.nodfac[0], 2.0) - self.orbit.nodfac[5] = eq75 - self.orbit.nodfac[6] = math.pow(self.orbit.nodfac[0], 3.0) - self.orbit.nodfac[7] = self.orbit.nodfac[0]*self.orbit.nodfac[3] - self.orbit.nodfac[8] = 1.0 - self.orbit.nodfac[9] = math.pow(self.orbit.nodfac[0], 2.0) - self.orbit.nodfac[10] = eq78 - self.orbit.nodfac[11] = 1.0 - self.orbit.nodfac[12] = eq78 - self.orbit.nodfac[13] = eq78 - self.orbit.nodfac[14] = eq77 - self.orbit.nodfac[15] = eq78 - self.orbit.nodfac[16] = 1.0 + self.orbit.nodfac['M2'] = eq78 + self.orbit.nodfac['S2'] = 1.0 + self.orbit.nodfac['N2'] = eq78 + self.orbit.nodfac['K1'] = eq227 + self.orbit.nodfac['M4'] = math.pow(self.orbit.nodfac['M2'], 2.0) + self.orbit.nodfac['O1'] = eq75 + self.orbit.nodfac['M6'] = math.pow(self.orbit.nodfac['M2'], 3.0) + self.orbit.nodfac['MK3'] = self.orbit.nodfac['M2'] * self.orbit.nodfac['K1'] + self.orbit.nodfac['S4'] = 1.0 + self.orbit.nodfac['MN4'] = math.pow(self.orbit.nodfac['M2'], 2.0) + self.orbit.nodfac['NU2'] = eq78 + self.orbit.nodfac['S6'] = 1.0 + self.orbit.nodfac['MU2'] = eq78 + self.orbit.nodfac['2N2'] = eq78 + self.orbit.nodfac['OO1'] = eq77 + self.orbit.nodfac['LAM2'] = eq78 + self.orbit.nodfac['S1'] = 1.0 # EQUATION 207 NOT PRODUCING CORRECT ANSWER FOR M1 # SET NODE FACTOR FOR M1 = 0 UNTIL CAN FURTHER RESEARCH - self.orbit.nodfac[17] = 0.0 - self.orbit.nodfac[18] = eq76 - self.orbit.nodfac[19] = eq73 - self.orbit.nodfac[20] = 1.0 - self.orbit.nodfac[21] = 1.0 - self.orbit.nodfac[22] = eq78 - self.orbit.nodfac[23] = eq74 - self.orbit.nodfac[24] = eq75 - self.orbit.nodfac[25] = eq75 - self.orbit.nodfac[26] = 1.0 - self.orbit.nodfac[27] = 1.0 - self.orbit.nodfac[28] = eq75 - self.orbit.nodfac[29] = 1.0 - self.orbit.nodfac[30] = eq78 - self.orbit.nodfac[31] = eq149 + self.orbit.nodfac['M1'] = 0.0 + self.orbit.nodfac['J1'] = eq76 + self.orbit.nodfac['MM'] = eq73 + self.orbit.nodfac['SSA'] = 1.0 + self.orbit.nodfac['SA'] = 1.0 + self.orbit.nodfac['MSF'] = eq78 + self.orbit.nodfac['MF'] = eq74 + self.orbit.nodfac['RHO'] = eq75 + self.orbit.nodfac['Q1'] = eq75 + self.orbit.nodfac['T2'] = 1.0 + self.orbit.nodfac['R2'] = 1.0 + self.orbit.nodfac['2Q1'] = eq75 + self.orbit.nodfac['P1'] = 1.0 + self.orbit.nodfac['2SM2'] = eq78 + self.orbit.nodfac['M3'] = eq149 # EQUATION 215 NOT PRODUCING CORRECT ANSWER FOR L2 # SET NODE FACTOR FOR L2 = 0 UNTIL CAN FURTHER RESEARCH - self.orbit.nodfac[32] = 0.0 - self.orbit.nodfac[33] = math.pow(self.orbit.nodfac[0], 2.0)*self.orbit.nodfac[3] - self.orbit.nodfac[34] = eq235 - self.orbit.nodfac[35] = math.pow(self.orbit.nodfac[0], 4.0) - self.orbit.nodfac[36] = eq78 + self.orbit.nodfac['L2'] = 0.0 + self.orbit.nodfac['2MK3'] = math.pow(self.orbit.nodfac['M2'], 2.0) * self.orbit.nodfac['K1'] + self.orbit.nodfac['K2'] = eq235 + self.orbit.nodfac['M8'] = math.pow(self.orbit.nodfac['M2'], 4.0) + self.orbit.nodfac['MS4'] = eq78 - def gterms(self, a_year, a_day_julian, a_hour, a_hrm): + def gterms(self, timestamp, timestamp_middle): """Determines the Greenwich equilibrium terms. Args: - a_year (float): The year. - a_day_julian (float): The day in julian format. - a_hour (float): The hour for V0. - a_hrm (float): The hour for U. - + timestamp (:obj:`datetime.datetime`): Start date and time to extract constituent arguments at + timestamp_middle (:obj:`datetime.datetime`): Date and time to consider as the middle of the series Returns: - The same values as found in table 15 of schureman. + The same values as found in table 15 of Schureman. """ # OBTAINING ORBITAL VALUES AT BEGINNING OF SERIES FOR V0 - self.set_orbit(a_year, a_day_julian, a_hour) - s = self.orbit.ds - p = self.orbit.dp - h = self.orbit.dh - p1 = self.orbit.dp1 - t = self.angle(180.0+a_hour*(360.0/24.0)) + self.set_orbit(timestamp) + s = self.orbit.astro['s'].value + p = self.orbit.astro['p'].value + h = self.orbit.astro['h'].value + p1 = self.orbit.astro['pp'].value + t = self.angle(180.0 + timestamp.hour * (360.0 / 24.0)) # OBTAINING ORBITAL VALUES AT MIDDLE OF SERIES FOR U - self.set_orbit(a_year, a_day_julian, a_hrm) - nu = self.orbit.dnu - xi = self.orbit.dxi - nup = self.orbit.dnup - nup2 = self.orbit.dnup2 + self.set_orbit(timestamp_middle) + nu = self.orbit.astro['nu'].value + xi = self.orbit.astro['xi'].value + nup = self.orbit.astro['nup'].value + nup2 = self.orbit.astro['nupp'].value # SUMMING TERMS TO OBTAIN EQUILIBRIUM ARGUMENTS - self.orbit.grterm[0] = 2.0*(t-s+h)+2.0*(xi-nu) - self.orbit.grterm[1] = 2.0*t - self.orbit.grterm[2] = 2.0*(t+h)-3.*s+p+2.0*(xi-nu) - self.orbit.grterm[3] = t+h-90.0-nup - self.orbit.grterm[4] = 4.0*(t-s+h)+4.0*(xi-nu) - self.orbit.grterm[5] = t-2.0*s+h+90.0+2.0*xi-nu - self.orbit.grterm[6] = 6.0*(t-s+h)+6.0*(xi-nu) - self.orbit.grterm[7] = 3.0*(t+h)-2.0*s-90.0+2.0*(xi-nu)-nup - self.orbit.grterm[8] = 4.0*t - self.orbit.grterm[9] = 4.0*(t+h)-5.0*s+p+4.0*(xi-nu) - self.orbit.grterm[10] = 2.0*t-3.0*s+4.0*h-p+2.0*(xi-nu) - self.orbit.grterm[11] = 6.0*t - self.orbit.grterm[12] = 2.0*(t+2.0*(h-s))+2.0*(xi-nu) - self.orbit.grterm[13] = 2.0*(t-2.0*s+h+p)+2.0*(xi-nu) - self.orbit.grterm[14] = t+2.0*s+h-90.0-2.0*xi-nu - self.orbit.grterm[15] = 2.0*t-s+p+180.0+2.0*(xi-nu) - self.orbit.grterm[16] = t - fi = self.orbit.di*self.pi180 - pc = self.orbit.dpc*self.pi180 - top = (5.0*math.cos(fi)-1.0)*math.sin(pc) - bottom = (7.0*math.cos(fi)+1.0)*math.cos(pc) + self.orbit.grterm['M2'] = 2.0 * (t - s + h) + 2.0 * (xi - nu) + self.orbit.grterm['S2'] = 2.0 * t + self.orbit.grterm['N2'] = 2.0 * (t + h) - 3.0 * s + p + 2.0 * (xi - nu) + self.orbit.grterm['K1'] = t + h - 90.0 - nup + self.orbit.grterm['M4'] = 4.0 * (t - s + h) + 4.0 * (xi - nu) + self.orbit.grterm['O1'] = t - 2.0 * s + h + 90.0 + 2.0 * xi - nu + self.orbit.grterm['M6'] = 6.0 * (t - s + h) + 6.0 * (xi - nu) + self.orbit.grterm['MK3'] = 3.0 * (t + h) - 2.0 * s - 90.0 + 2.0 * (xi - nu) - nup + self.orbit.grterm['S4'] = 4.0 * t + self.orbit.grterm['MN4'] = 4.0 * (t + h) - 5.0 * s + p + 4.0 * (xi - nu) + self.orbit.grterm['NU2'] = 2.0 * t - 3.0 * s + 4.0 * h - p + 2.0 * (xi - nu) + self.orbit.grterm['S6'] = 6.0 * t + self.orbit.grterm['MU2'] = 2.0 * (t + 2.0 * (h - s)) + 2.0 * (xi - nu) + self.orbit.grterm['2N2'] = 2.0 * (t - 2.0 * s + h + p) + 2.0 * (xi - nu) + self.orbit.grterm['OO1'] = t + 2.0 * s + h - 90.0 - 2.0 * xi - nu + self.orbit.grterm['LAM2'] = 2.0 * t - s + p + 180.0 + 2.0 * (xi - nu) + self.orbit.grterm['S1'] = t + fi = math.radians(self.orbit.astro['i'].value) + pc = math.radians(self.orbit.astro['P'].value) + top = (5.0 * math.cos(fi) - 1.0) * math.sin(pc) + bottom = (7.0 * math.cos(fi) + 1.0) * math.cos(pc) q = math.degrees(math.atan2(top, bottom)) - self.orbit.grterm[17] = t-s+h-90.0+xi-nu+q - self.orbit.grterm[18] = t+s+h-p-90.0-nu - self.orbit.grterm[19] = s-p - self.orbit.grterm[20] = 2.0*h - self.orbit.grterm[21] = h - self.orbit.grterm[22] = 2.0*(s-h) - self.orbit.grterm[23] = 2.0*s-2.0*xi - self.orbit.grterm[24] = t+3.0*(h-s)-p+90.0+2.0*xi-nu - self.orbit.grterm[25] = t-3.0*s+h+p+90.0+2.0*xi-nu - self.orbit.grterm[26] = 2.0*t-h+p1 - self.orbit.grterm[27] = 2.0*t+h-p1+180.0 - self.orbit.grterm[28] = t-4.0*s+h+2.0*p+90.0+2.0*xi-nu - self.orbit.grterm[29] = t-h+90.0 - self.orbit.grterm[30] = 2.0*(t+s-h)+2.0*(nu-xi) - self.orbit.grterm[31] = 3.0*(t-s+h)+3.0*(xi-nu) - r = math.sin(2.0*pc)/((1.0/6.0)*math.pow((1.0/math.tan(0.5*fi)), 2)-math.cos(2.0*pc)) - r = math.atan(r)/self.pi180 - self.orbit.grterm[32] = 2.0*(t+h)-s-p+180.0+2.0*(xi-nu)-r - self.orbit.grterm[33] = 3.0*(t+h)-4.0*s+90.0+4.0*(xi-nu)+nup - self.orbit.grterm[34] = 2.0*(t+h)-2.0*nup2 - self.orbit.grterm[35] = 8.0*(t-s+h)+8.0*(xi-nu) - self.orbit.grterm[36] = 2.0*(2.0*t-s+h)+2.0*(xi-nu) - for ih in range(0, 37): - self.orbit.grterm[ih] = self.angle(self.orbit.grterm[ih]) + self.orbit.grterm['M1'] = t - s + h - 90.0 + xi - nu + q + self.orbit.grterm['J1'] = t + s + h - p - 90.0 - nu + self.orbit.grterm['MM'] = s - p + self.orbit.grterm['SSA'] = 2.0 * h + self.orbit.grterm['SA'] = h + self.orbit.grterm['MSF'] = 2.0 * (s - h) + self.orbit.grterm['MF'] = 2.0 * s - 2.0 * xi + self.orbit.grterm['RHO'] = t + 3.0 * (h - s) - p + 90.0 + 2.0 * xi - nu + self.orbit.grterm['Q1'] = t - 3.0 * s + h + p + 90.0 + 2.0 * xi - nu + self.orbit.grterm['T2'] = 2.0 * t - h + p1 + self.orbit.grterm['R2'] = 2.0 * t + h - p1 + 180.0 + self.orbit.grterm['2Q1'] = t - 4.0 * s + h + 2.0 * p + 90.0 + 2.0 * xi - nu + self.orbit.grterm['P1'] = t - h + 90.0 + self.orbit.grterm['2SM2'] = 2.0 * (t + s - h) + 2.0 * (nu - xi) + self.orbit.grterm['M3'] = 3.0 * (t - s + h) + 3.0 * (xi - nu) + r = math.sin(2.0 * pc) / ((1.0 / 6.0) * math.pow((1.0 / math.tan(0.5 * fi)), 2) - math.cos(2.0 * pc)) + r = math.degrees(math.atan(r)) + self.orbit.grterm['L2'] = 2.0 * (t + h) - s - p + 180.0 + 2.0 * (xi - nu) - r + self.orbit.grterm['2MK3'] = 3.0 * (t + h) - 4.0 * s + 90.0 + 4.0 * (xi - nu) + nup + self.orbit.grterm['K2'] = 2.0 * (t + h) - 2.0 * nup2 + self.orbit.grterm['M8'] = 8.0 * (t - s + h) + 8.0 * (xi - nu) + self.orbit.grterm['MS4'] = 2.0 * (2.0 * t - s + h) + 2.0 * (xi - nu) + for con, value in self.orbit.grterm.items(): + self.orbit.grterm[con] = self.angle(value) diff --git a/harmonica/tpxo_database.py b/harmonica/tpxo_database.py index fe3c38e..7cc8064 100644 --- a/harmonica/tpxo_database.py +++ b/harmonica/tpxo_database.py @@ -97,7 +97,7 @@ def get_components(self, locs, cons=None, positive_ph=False): # phase ph + (360. if positive_ph and ph < 0 else 0), # speed - NOAA_SPEEDS[c] + NOAA_SPEEDS[c][0] ] return self diff --git a/tutorials/python_api/expected_tidal_test.out b/tutorials/python_api/expected_tidal_test.out index bb53e51..d7f0add 100644 --- a/tutorials/python_api/expected_tidal_test.out +++ b/tutorials/python_api/expected_tidal_test.out @@ -1,9 +1,9 @@ Nodal factor: - amplitude frequency earth_tide_reduction_factor equilibrium_argument nodal_factor -M2 0.242334 0.000141 0.693 345.151862 1.021162 -S2 0.112841 0.000145 0.693 90.000000 1.000000 -N2 0.046398 0.000138 0.693 77.558471 1.021162 -K1 0.141565 0.000073 0.736 105.776526 0.945419 + amplitude frequency speed earth_tide_reduction_factor equilibrium_argument nodal_factor +M2 0.242334 0.000141 28.984104 0.693 345.201515 1.087974 +S2 0.112841 0.000145 30.000000 0.693 90.000000 1.000000 +N2 0.046398 0.000138 28.439730 0.693 77.612890 1.087974 +K1 0.141565 0.000073 15.041069 0.736 105.776383 0.483807 LeProvost components: amplitude phase speed @@ -98,34 +98,3 @@ M2 0.910663 234.117743 28.984104 N2 0.188528 206.717292 28.439730 S2 0.253107 260.181916 30.000000 -FES2014 components: - amplitude phase speed -K1 0.090187 173.347897 15.041069 -M2 0.574708 354.674980 28.984104 -N2 0.134563 335.961855 28.439730 -S2 0.111234 15.286627 30.000000 - - amplitude phase speed -K1 0.131800 203.724442 15.041069 -M2 1.165043 107.518818 28.984104 -N2 0.262041 75.300656 28.439730 -S2 0.176183 144.626343 30.000000 - - amplitude phase speed -K1 0.148333 193.815262 15.041069 -M2 3.954875 98.342868 28.984104 -N2 0.786427 75.031938 28.439730 -S2 0.648518 146.118892 30.000000 - - amplitude phase speed -K1 0.416949 232.809029 15.041069 -M2 0.803786 220.349546 28.984104 -N2 0.169759 195.739307 28.439730 -S2 0.214817 245.327583 30.000000 - - amplitude phase speed -K1 0.428201 237.123548 15.041069 -M2 0.903397 229.333500 28.984104 -N2 0.189360 204.322635 28.439730 -S2 0.249951 255.765785 30.000000 - diff --git a/tutorials/python_api/test.py b/tutorials/python_api/test.py index 3321e99..4166474 100644 --- a/tutorials/python_api/test.py +++ b/tutorials/python_api/test.py @@ -1,3 +1,4 @@ +import datetime import os from harmonica.tidal_constituents import Constituents @@ -14,7 +15,7 @@ constituents = Constituents('leprovost') # Get astronomical nodal factor data (not dependent on the tidal model) - nodal_factors = constituents.get_nodal_factor(cons, 15, 30, 8, 2018) + nodal_factors = constituents.get_nodal_factor(cons, datetime.datetime(2018, 8, 30, 15)) f = open(os.path.join(os.getcwd(), "tidal_test.out"), "w") f.write("Nodal factor:\n") @@ -40,10 +41,10 @@ for pt in tpxo_comps.data: f.write(pt.sort_index().to_string() + "\n\n") - f.write("FES2014 components:\n") - f.flush() - fes2014_comps = constituents.get_components(all_points, cons, model='fes2014') - for pt in fes2014_comps.data: - f.write(pt.sort_index().to_string() + "\n\n") + # f.write("FES2014 components:\n") + # f.flush() + # fes2014_comps = constituents.get_components(all_points, cons, model='fes2014') + # for pt in fes2014_comps.data: + # f.write(pt.sort_index().to_string() + "\n\n") f.close() \ No newline at end of file From fe25d2f75ebff3c374243f8ec6a89110817b2cd9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 19 Oct 2018 17:01:21 -0600 Subject: [PATCH 24/24] Don't download ADCIRC resources until necessary. --- harmonica/adcirc_database.py | 1 - 1 file changed, 1 deletion(-) diff --git a/harmonica/adcirc_database.py b/harmonica/adcirc_database.py index 769c280..955defe 100644 --- a/harmonica/adcirc_database.py +++ b/harmonica/adcirc_database.py @@ -31,7 +31,6 @@ def __init__(self, model=DEFAULT_ADCIRC_RESOURCE): model, ", ".join(ResourceManager.ADCIRC_MODELS).strip() )) super().__init__(model) - self.resources.download_model(None) def get_components(self, locs, cons=None, positive_ph=False): """Get the amplitude, phase, and speed for the given constituents at the given points.