From f33587ea2be5fa5d291b11e20089d90097b69bc0 Mon Sep 17 00:00:00 2001 From: Tyler Sutterley Date: Sat, 13 Jan 2024 12:44:04 +1300 Subject: [PATCH] feat: add functions to convert to and from doodson numbers feat: add option to output Cartwright numbers fix: test solve is in class --- doc/source/api_reference/arguments.rst | 4 + pyTMD/arguments.py | 105 +++++++++++++++++++------ test/test_arguments.py | 19 ++++- test/test_download_and_read.py | 2 +- 4 files changed, 106 insertions(+), 24 deletions(-) diff --git a/doc/source/api_reference/arguments.rst b/doc/source/api_reference/arguments.rst index ac50cec2..6e6de179 100644 --- a/doc/source/api_reference/arguments.rst +++ b/doc/source/api_reference/arguments.rst @@ -27,3 +27,7 @@ Calling Sequence .. autofunction:: pyTMD.arguments._arguments_table .. autofunction:: pyTMD.arguments._minor_table + +.. autofunction:: pyTMD.arguments._to_doodson_number + +.. autofunction:: pyTMD.arguments._from_doodson_number diff --git a/pyTMD/arguments.py b/pyTMD/arguments.py index 276ecfee..81cf81ef 100755 --- a/pyTMD/arguments.py +++ b/pyTMD/arguments.py @@ -672,23 +672,29 @@ def minor_arguments( return (u, f, arg) def doodson_number( - constituents: list | np.ndarray, + constituents: str | list | np.ndarray, **kwargs ): """ - Calculates the Doodson number for tidal constituents [1]_ + Calculates the Doodson or Cartwright number for + tidal constituents [1]_ Parameters ---------- - constituents: list - tidal constituent IDs + constituents: str, list or np.ndarray + tidal constituent ID(s) corrections: str, default 'OTIS' use arguments from OTIS/ATLAS or GOT models + formalism: str, default 'Doodson' + constituent identifier formalism + + - ``'Doodson'`` + - ``'Cartwright'`` Returns ------- - DO: dict - Doodson number for each constituent + numbers: float or dict + Doodson or Cartwright number for each constituent References ---------- @@ -700,6 +706,7 @@ def doodson_number( """ # set default keyword arguments kwargs.setdefault('corrections', 'OTIS') + kwargs.setdefault('formalism', 'Doodson') # constituents array (not all are included in tidal program) cindex = ['sa', 'ssa', 'mm', 'msf', 'mf', 'mt', 'alpha1', '2q1', 'sigma1', @@ -710,21 +717,32 @@ def doodson_number( 's6', 's7', 's8', 'm8', 'mks2', 'msqm', 'mtm', 'n4', 'eps2', 'z0'] # get the table of coefficients coefficients = _arguments_table(**kwargs) - # output dictionary with doodson numbers - DO = {} - # for each input constituent - for i,c in enumerate(constituents): + if isinstance(constituents, str): # map between given constituents and supported in tidal program - j, = [j for j,val in enumerate(cindex) if (val == c)] - # remove phase dependence "k" - coef = coefficients[:-1,j] - # add 5 to values following Doodson convention (prevent negatives) - coef[1:] += 5 - # convert to single number and round off floating point errors - value = np.sum([v*10**(2-o) for o,v in enumerate(coef)]) - DO[c] = np.round(value, decimals=3) - # return the doodson number - return DO + j, = [j for j,val in enumerate(cindex) if (val == constituents)] + # extract identifier in formalism + if (kwargs['formalism'] == 'Cartwright'): + # extract Cartwright number + numbers = np.array(coefficients[:6,j]) + elif (kwargs['formalism'] == 'Doodson'): + # convert from coefficients to Doodson number + numbers = _to_doodson_number(coefficients[:,j]) + else: + # output dictionary with Doodson numbers + numbers = {} + # for each input constituent + for i,c in enumerate(constituents): + # map between given constituents and supported in tidal program + j, = [j for j,val in enumerate(cindex) if (val == c)] + # convert from coefficients to Doodson number + if (kwargs['formalism'] == 'Cartwright'): + # extract Cartwright number + numbers[c] = np.array(coefficients[:6,j]) + elif (kwargs['formalism'] == 'Doodson'): + # convert from coefficients to Doodson number + numbers[c] = _to_doodson_number(coefficients[:,j]) + # return the Doodson or Cartwright number + return numbers def _arguments_table(**kwargs): """ @@ -738,7 +756,7 @@ def _arguments_table(**kwargs): Returns ------- coef: np.ndarray - Spectral lines for each constituent + Doodson coefficients (Cartwright numbers) for each constituent References ---------- @@ -838,7 +856,7 @@ def _minor_table(**kwargs): Returns ------- coef: np.ndarray - Spectral lines for each constituent + Doodson coefficients (Cartwright numbers) for each constituent References ---------- @@ -882,3 +900,46 @@ def _minor_table(**kwargs): coef[:,19] = [2.0, 3.0, 0.0, 0.0, 0.0, -1.0, 0.0] # eta2 # return the coefficient table return coef + +def _to_doodson_number(coef: list | np.ndarray): + """ + Converts Cartwright numbers into a Doodson number + + Parameters + ---------- + coef: list or np.ndarray + Doodson coefficients (Cartwright numbers) for constituent + + Returns + ------- + DO: float + Doodson number for constituent + """ + # assert length and verify array + coef = np.array(coef[:6]) + # add 5 to values following Doodson convention (prevent negatives) + coef[1:] += 5 + # convert to single number and round off floating point errors + DO = np.sum([v*10**(2-o) for o,v in enumerate(coef)]) + return np.round(DO, decimals=3) + +def _from_doodson_number(DO: float | np.ndarray): + """ + Converts Doodson numbers into Cartwright numbers + + Parameters + ---------- + DO: float or np.ndarray + Doodson number for constituent + + Returns + ------- + coef: np.ndarray + Doodson coefficients (Cartwright numbers) for constituent + """ + # convert from Doodson number to Cartwright numbers + # multiply by 1000 to prevent floating point errors + coef = np.array([np.mod(1e3*DO, 10**(6-o))//10**(5-o) for o in range(6)]) + # remove 5 from values following Doodson convention + coef[1:] -= 5 + return coef diff --git a/test/test_arguments.py b/test/test_arguments.py index 94267c53..ae73b1a7 100644 --- a/test/test_arguments.py +++ b/test/test_arguments.py @@ -6,7 +6,13 @@ import pytest import numpy as np import pyTMD.astro -from pyTMD.arguments import doodson_number, _arguments_table, _minor_table +from pyTMD.arguments import ( + doodson_number, + _arguments_table, + _minor_table, + _to_doodson_number, + _from_doodson_number +) @pytest.mark.parametrize("corrections", ['OTIS', 'GOT']) def test_arguments(corrections): @@ -199,6 +205,17 @@ def test_doodson(): exp['m8'] = 855.555 # get observed values for constituents obs = doodson_number(exp.keys()) + cartwright = doodson_number(exp.keys(), formalism='Cartwright') # check values for key,val in exp.items(): assert val == obs[key] + # check values when entered as string + test = doodson_number(key) + assert val == test + # check conversion to and from Doodson numbers + doodson = _to_doodson_number(cartwright[key]) + # check values when entered as Cartwright + assert val == doodson + # check values when entered as Doodson + coefficients = _from_doodson_number(val) + assert np.all(cartwright[key] == coefficients) diff --git a/test/test_download_and_read.py b/test/test_download_and_read.py index 43a08c68..041fa6f3 100644 --- a/test/test_download_and_read.py +++ b/test/test_download_and_read.py @@ -548,7 +548,7 @@ def test_tidal_ellipse(self): assert np.all(np.abs(difference) < eps) # PURPOSE: Tests solving for harmonic constants - def test_solve(): + def test_solve(self): # get model parameters model = pyTMD.io.model(filepath).elevation('CATS2008')