Skip to content

Commit

Permalink
feat: add parsing more constituents for #351 (#395)
Browse files Browse the repository at this point in the history
docs: add figure for tidal spectra
  • Loading branch information
tsutterley authored Feb 27, 2025
1 parent 9a294a0 commit 7b00d0c
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:
deploy:

runs-on: ubuntu-20.04
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions doc/source/api_reference/arguments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Calling Sequence

.. autofunction:: pyTMD.arguments._parse_tide_potential_table

.. autofunction:: pyTMD.arguments._to_constituent_id

.. autofunction:: pyTMD.arguments._to_doodson_number

.. autofunction:: pyTMD.arguments._to_extended_doodson
Expand Down
5 changes: 5 additions & 0 deletions doc/source/background/Tides.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ A secondary tidal effect, known as load tides, is due to the elastic response of
Tidal oscillations for both ocean and load tides can be decomposed into a series of tidal constituents (or partial tides) of particular frequencies that are associated with the relative positions of the sun, moon and Earth.
These tidal constituents are typically classified into different "species" based on their approximate period: short-period, semi-diurnal, diurnal, and long-period [see :ref:`tab-1`].

.. plot:: ./background/spectra.py
:show-source-link: False
:caption: Tidal spectra from :cite:t:`Cartwright:1973em`
:align: center

The amplitude and phase of major constituents are provided by ocean tide models, which can be used for tidal predictions.
Ocean tide models are typically one of following categories:
1) empirically adjusted models,
Expand Down
121 changes: 121 additions & 0 deletions doc/source/background/spectra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.offsetbox as offsetbox
# import tide programs
import pyTMD.astro
import pyTMD.arguments

def frequency(arguments):
"""
Calculates the angular frequencies of constituents
"""
# Modified Julian Dates at J2000
MJD = np.array([51544.5, 51544.55])
# time interval in seconds
deltat = 86400.0*(MJD[1] - MJD[0])
# calculate the mean longitudes of the sun and moon
s, h, p, n, pp = pyTMD.astro.mean_longitudes(MJD, ASTRO5=True)
# initial time conversions
hour = 24.0*np.mod(MJD, 1)
# convert from hours solar time into mean lunar time in degrees
tau = 15.0*hour - s + h
# determine equilibrium arguments
fargs = np.c_[tau, s, h, p, n, pp]
rates = (fargs[1,:] - fargs[0,:])/deltat
fd = np.dot(rates, arguments)
# convert to radians per second
omega = 2.0*np.pi*fd/360.0
return omega

# Cartwright and Edden (1973) table with updated values
table = pyTMD.arguments._ce1973_table_1
# read the table
CTE = pyTMD.arguments._parse_tide_potential_table(table)

# create figure and subplots
fig = plt.figure(num=1, figsize=(13,5))
subfig = fig.subfigures(2, 1, hspace=0.05, height_ratios=(1.0, 2.0))
ax1 = subfig[0].subplots(ncols=1)
ax2 = subfig[1].subplots(ncols=3, sharey='row')
# ax2[0].sharey(ax1)
# set x and y limits
ax1.set_xlim(-0.06, 2.14)
ax1.set_ylim(1e-3, 2e2)
ax2[0].set_ylim(1e-3, 2e2)

# major constituents to label for each species
major = []
major.append(['mm', 'mf', 'mtm'])
major.append(['q1', 'o1', 'k1', 'j1'])
major.append(['2n2', 'm2', 'l2', 's2', 'n2'])
# frequency ranges for each species band
frange = []
frange.append([0, 0.5])
frange.append([0.80, 1.15])
frange.append([1.75, 2.10])
# for each spectral line
for i, line in enumerate(CTE):
# calculate the angular frequency
arguments = np.array([line[c] for c in ['tau','s','h','p','n','pp']])
omega = frequency(arguments)
# skip z0
if (omega == 0.0):
continue
# convert to frequency (solar days per cycle)
f = np.abs(omega*86400.0)/(2.0*np.pi)
# amplitude in cm
amp = 100.0*np.abs(line['Hs3'])
# get the constituent ID based on the first 6 arguments
cons = pyTMD.arguments._to_constituent_id(arguments,
arguments=6, raise_error=False)
# plot amplitudes and color if in the major constituents list
ax1.semilogy([f, f], [0.0, amp], color='0.4', zorder=1)
for j, fr in enumerate(frange):
if (f >= fr[0]) and (f <= fr[1]) and (cons in major[j]):
ax2[j].semilogy([f, f], [0.0, amp], color='red', zorder=2)
ax2[j].text(f, 1.5*amp, cons, color='red', fontsize=10, ha='center')
break
elif (f >= fr[0]) and (f <= fr[1]):
ax2[j].semilogy([f, f], [0.0, amp], color='0.4', zorder=1)
break

# create inset axes and set ticks
plot_colors = ['k', 'k', 'k']
labels = ['Long-Period', 'Diurnal', 'Semi-Diurnal']
connector_visibility = [False, True, False, True]
for i, ax in enumerate(ax2):
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
inset_rectangle, inset_connectors = ax1.indicate_inset(
bounds=(xmin, ymin, xmax-xmin, ymax-ymin),
inset_ax=ax, facecolor=plot_colors[i], alpha=0.15,
edgecolor=plot_colors[i], zorder=0)
# set visibility of connectors
for j, vis in enumerate(connector_visibility):
inset_connectors[j].set_visible(vis)
# add labels to inset axes
prop = dict(size=12, weight='bold', color=plot_colors[i])
at = offsetbox.AnchoredText(labels[i], loc=2, pad=0,
borderpad=0.5, frameon=False, prop=prop)
ax.axes.add_artist(at)
# set ticks for inset axes
ax.get_xaxis().set_tick_params(which='both', direction='in', color=plot_colors[i])
ax.get_yaxis().set_tick_params(which='both', direction='in', color=plot_colors[i])
# stronger linewidth on frame
for key,val in ax.spines.items():
val.set_linewidth(1.5)
val.set_color(plot_colors[i])
# set ticks
ax1.get_xaxis().set_tick_params(which='both', direction='in')
ax1.get_yaxis().set_tick_params(which='both', direction='in')
[val.set_linewidth(1.5) for key,val in ax1.spines.items()]
# # add x and y labels
ax1.set_ylabel('Amplitude [cm]', fontsize=10)
ax2[0].set_ylabel('Amplitude [cm]', fontsize=10)
ax2[1].set_xlabel('Frequency [cpd]', fontsize=10)
# set titles
ax1.set_title(f'Tidal Spectra', fontsize=12)
# adjust subplots
subfig[0].subplots_adjust(left=0.048,right=0.9975,bottom=0.0,top=0.85)
subfig[1].subplots_adjust(left=0.048,right=0.9975,bottom=0.12,top=0.975,wspace=0.05)
plt.show()
19 changes: 11 additions & 8 deletions doc/source/notebooks/Plot-Tidal-Spectra.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@
"# read the table\n",
"CTE = pyTMD.arguments._parse_tide_potential_table(table)\n",
"fig, ax = plt.subplots(ncols=2, sharey=True, figsize=(10, 5))\n",
"# major constituents to label\n",
"scons = ['2n2', 'm2', 's2', 'n2']\n",
"SDO = pyTMD.arguments.doodson_number(scons, astype=np.str_)\n",
"dcons = ['q1', 'o1', 'k1', 'j1']\n",
"DDO = pyTMD.arguments.doodson_number(dcons, astype=np.str_)\n",
"# for each spectral line\n",
"for i, line in enumerate(CTE):\n",
" # calculate the angular frequency\n",
" arguments = np.array([line[c] for c in ['tau','s','h','p','n','pp']])\n",
Expand All @@ -107,22 +107,25 @@
" f = omega*86400.0/(2.0*np.pi)\n",
" # amplitude in cm\n",
" amp = 100.0*np.abs(line['Hs3'])\n",
" # plot amplitudes and color if in the common list\n",
" if line['DO'] in SDO.values():\n",
" # get the constituent ID based on the first 6 arguments\n",
" cons = pyTMD.arguments._to_constituent_id(arguments,\n",
" arguments=6, raise_error=False)\n",
" # plot amplitudes and color if in the major constituents list\n",
" if cons in scons:\n",
" ax[0].semilogy([f, f], [0.0, amp], 'r', zorder=1)\n",
" elif line['DO'] in DDO.values():\n",
" elif cons in dcons:\n",
" ax[1].semilogy([f, f], [0.0, amp], 'r', zorder=1)\n",
" elif (f >= 1.75) and (f <= 2.10):\n",
" ax[0].semilogy([f, f], [0.0, amp], '0.4', zorder=0)\n",
" elif (f >= 0.80) and (f <= 1.15):\n",
" ax[1].semilogy([f, f], [0.0, amp], '0.4', zorder=0)\n",
"\n",
"# add labels for some common semi-diurnal constituents\n",
"# add labels for some major semi-diurnal constituents\n",
"for c in scons:\n",
" omega = pyTMD.arguments.frequency(c)\n",
" f = omega*86400.0/(2.0*np.pi)\n",
" ax[0].text(f, 100.0, c, color='r', fontsize=10, ha='center')\n",
"# add labels for some common diurnal constituents\n",
"# add labels for some major diurnal constituents\n",
"for c in dcons:\n",
" omega = pyTMD.arguments.frequency(c)\n",
" f = omega*86400.0/(2.0*np.pi)\n",
Expand Down Expand Up @@ -159,7 +162,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "py13",
"language": "python",
"name": "python3"
},
Expand Down
89 changes: 83 additions & 6 deletions pyTMD/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Updated 02/2025: add option to make doodson numbers strings
add Doodson number convention for converting 11 to E
add Doodson (1921) table for coefficients missing from Cartwright tables
add function to convert from Cartwright number to constituent ID
Updated 12/2024: added function to calculate tidal aliasing periods
Updated 11/2024: allow variable case for Doodson number formalisms
fix species in constituent parameters for complex tides
Expand Down Expand Up @@ -102,6 +103,7 @@
"_constituent_parameters",
"_love_numbers",
"_parse_tide_potential_table",
"_to_constituent_id",
"_to_doodson_number",
"_to_extended_doodson",
"_from_doodson_number",
Expand All @@ -115,7 +117,8 @@ def arguments(
):
"""
Calculates the nodal corrections for tidal constituents
:cite:p:`Doodson:1941td` :cite:p:`Schureman:1958ty` :cite:p:`Foreman:1989dt` :cite:p:`Egbert:2002ge`
:cite:p:`Doodson:1941td` :cite:p:`Schureman:1958ty`
:cite:p:`Foreman:1989dt` :cite:p:`Egbert:2002ge`
Parameters
----------
Expand Down Expand Up @@ -181,7 +184,8 @@ def minor_arguments(
):
"""
Calculates the nodal corrections for minor tidal constituents
in order to infer their values :cite:p:`Doodson:1941td` :cite:p:`Schureman:1958ty`
in order to infer their values
:cite:p:`Doodson:1941td` :cite:p:`Schureman:1958ty`
:cite:p:`Foreman:1989dt` :cite:p:`Egbert:2002ge`
Expand Down Expand Up @@ -484,7 +488,8 @@ def nodal(
):
"""
Calculates the nodal corrections for tidal constituents
:cite:p:`Doodson:1941td` :cite:p:`Schureman:1958ty` :cite:p:`Foreman:1989dt` :cite:p:`Ray:1999vm`
:cite:p:`Doodson:1941td` :cite:p:`Schureman:1958ty`
:cite:p:`Foreman:1989dt` :cite:p:`Ray:1999vm`
Calculates factors for compound tides using recursion
Expand Down Expand Up @@ -1265,7 +1270,8 @@ def aliasing_period(

def _arguments_table(**kwargs):
"""
Arguments table for tidal constituents :cite:p:`Doodson:1921kt` :cite:p:`Doodson:1941td`
Arguments table for tidal constituents
:cite:p:`Doodson:1921kt` :cite:p:`Doodson:1941td`
Parameters
----------
Expand Down Expand Up @@ -1302,7 +1308,8 @@ def _arguments_table(**kwargs):

def _minor_table(**kwargs):
"""
Arguments table for minor tidal constituents :cite:p:`Doodson:1921kt` :cite:p:`Doodson:1941td`
Arguments table for minor tidal constituents
:cite:p:`Doodson:1921kt` :cite:p:`Doodson:1941td`
Returns
-------
Expand Down Expand Up @@ -1417,7 +1424,8 @@ def _love_numbers(
):
"""
Compute the body tide Love/Shida numbers for a given
frequency :cite:p:`Wahr:1981ea` :cite:p:`Wahr:1981if` :cite:p:`Mathews:1995go`
frequency :cite:p:`Wahr:1981ea` :cite:p:`Wahr:1981if`
:cite:p:`Mathews:1995go`
Parameters
----------
Expand Down Expand Up @@ -1539,6 +1547,75 @@ def _parse_tide_potential_table(table: str | pathlib.Path):
# return the table values
return CTE

def _to_constituent_id(coef: list | np.ndarray, **kwargs):
"""
Converts Cartwright numbers into a tidal constituent ID
Parameters
----------
coef: list or np.ndarray
Doodson coefficients (Cartwright numbers) for constituent
corrections: str, default 'GOT'
use coefficients from OTIS, FES or GOT models
arguments: int, default 7
Number of astronomical arguments to use
file: str or pathlib.Path, default `coefficients.json`
JSON file of Doodson coefficients
raise_error: bool, default True
Raise exception if constituent is unsupported
Returns
-------
c: str
tidal constituent ID
"""
# set default keyword arguments
kwargs.setdefault('corrections', 'GOT')
kwargs.setdefault('arguments', 7)
kwargs.setdefault('file', _coefficients_table)
kwargs.setdefault('raise_error', True)

# verify list of coefficients
N = int(kwargs['arguments'])
assert (N == 6) or (N == 7)
# assert length and verify list
coef = np.copy(coef[:N]).tolist()

# verify coefficients table path
table = pathlib.Path(kwargs['file']).expanduser().absolute()
# modified Doodson coefficients for constituents
# using 7 index variables: tau, s, h, p, n, pp, k
# tau: mean lunar time
# s: mean longitude of moon
# h: mean longitude of sun
# p: mean longitude of lunar perigee
# n: mean longitude of ascending lunar node
# pp: mean longitude of solar perigee
# k: 90-degree phase
with table.open(mode='r', encoding='utf8') as fid:
coefficients = json.load(fid)

# # Without p'
# coefficients['sa'] = [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]
# coefficients['sta'] = [0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0]
# set s1 coefficients
if kwargs['corrections'] in ('OTIS','ATLAS','TMD3','netcdf'):
coefficients['s1'] = [1.0, 1.0, -1.0, 0.0, 0.0, 0.0, 1.0]
# separate dictionary into keys and values
coefficients_keys = list(coefficients.keys())
# truncate coefficient values to number of arguments
coefficients_values = np.array(list(coefficients.values()))
coefficients_values = coefficients_values[:,:N].tolist()
# get constituent ID from Doodson coefficients
try:
i = coefficients_values.index(coef)
except ValueError:
if kwargs['raise_error']:
raise ValueError('Unsupported constituent')
else:
# return constituent id
return coefficients_keys[i]

def _to_doodson_number(coef: list | np.ndarray, **kwargs):
"""
Converts Cartwright numbers into a Doodson number
Expand Down
Loading

0 comments on commit 7b00d0c

Please sign in to comment.