Tidal Constituents#
In Chapter 3 of the book and slides, we learned that the tide is generated through the interplay between gravitational attraction forces in the Earth-Moon and Earth-Sun systems respectively. In this notebook we will explore these concepts in a more interactive way, with some exercises, visualizations, and code. We will not repeat the whole theory, however, so make sure you have followed the lectures for this week and read the relevant pages in chapter 3 of the book.
Earth has continents, oceans with varying water depths, and a complex gravitational field, hence the tidal signals we observe around the planet are complex. Fortunately, we can break them down into multiple harmonic components, called tidal constituents. These can either be diurnal, semi-diurnal, short-, or long-period constituents. Each constituent is characterized by an amplitude, frequency and phase, and individually looks like a smooth sin/cosine curve. When we sum up all the individual components we observe signal beating resulting in complex tidal signals with spring-neap cycles, daily inequalities, and longer-term variations.
A table of principal tidal constituents is provided below (compare Table 3.5 in the book). These components are direclty related to the astronomical forcing and the listed amplitudes are according to equilibrium theory. The period is given with more significant figures than in Table 3.5 to make more accurate calculations of the beating periods possible.
Tidal constituents |
Name |
Equil. |
Period [h] |
---|---|---|---|
Semi-diurnal |
|||
Principal lunar |
M2 |
0.24 |
12.4206012 |
Principal solar |
S2 |
0.11 |
12 |
Lunar elliptical |
N2 |
0.046 |
12.65834751 |
Lunar-solar declinational |
K2 |
0.031 |
11.96723606 |
Diurnal |
|||
Lunar-solar declinational |
K1 |
0.14 |
23.93447213 |
Principal lunar |
O1 |
0.10 |
25.81933871 |
Principal solar |
P1 |
0.047 |
24.06588766 |
Lunar elliptical |
Q1 |
0.019 |
26.868350 |
Long period |
|||
Fortnightly |
Mf |
0.042 |
327.8599387 |
Monthly |
Mm |
0.022 |
661.3111655 |
Semi-annual |
Ssa |
0.019 |
4383.076325 |
# Before running, install 'uptide' in the coastal environment as 'pip install uptide'
# Run this to import modules
import math
import pathlib
import pickle
import sys
from datetime import datetime, timedelta
from pathlib import Path
from warnings import filterwarnings
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import ipywidgets as widgets
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pooch
import uptide
import xarray as xr
from initialize.Tide_Initialize import (
plot_timeseries_with_interactive_controls,
questions_2d,
)
from IPython.display import Image, display
from ipywidgets import interact
cwd = pathlib.Path().resolve()
proj_dir = cwd.parent # this is the root of the CoastalCodeBook
DATA_DIR = proj_dir / "data"
Visualisation of Astronomical Constituents#
In this part of the exercise, our aim is to enhance the understanding of tidal constituents by visualising the full observational sea level in comparison to the tidal signal at Scheveningen. To achieve this, we will use two datasets:
GESLA-3 (Global Extreme Sea Level Analysis) sea level records
FES2014 Global Tide data, providing amplitude and phase information for 34 tidal constituents, distributed on 1/16˚ grids.
Look at the cell below and run it to see how the observed sea level matches up with the tidal signal derived from the available 34 constituents. The tidal constituents include both astronomical constituents (like in the Table above) and non-linear shallow-water tides or overtides. These tidal signals were computed for an arbitrary timeframe spanning from 1977 to 2017, using the uptide python package.
Can you try to explain why the tidal signal doesn’t perfectly match the observed sea level?
## Download GESLA tide gauge data for Scheveningen
tide_gauge_fp = pooch.retrieve(
"https://coclico.blob.core.windows.net/coastal-dynamics/2_wind_waves_tides/Scheveningen_GESLA.pkl",
known_hash="90355584803ddcdf88b01fcf02546c4d8201a3fa6f63355ecfdb8ab6a07d1e38",
)
tide_gauge = pd.read_pickle(tide_gauge_fp)
## Load FES2014 amplitude and phase data and calculate the signal
# see the commented script at the bottom of the notebook if you want to know how to load FES2014 amplitude and phase data
tide = {}
amplitudes = {}
phases = {}
comps = [
"EPS2",
"J1",
"K1",
"K2",
"L2",
"LAMBDA2",
"M2",
"M3",
"M4",
"M6",
"M8",
"MF",
"MKS2",
"MM",
"MN4",
"MS4",
"MSF",
"MSQM",
"MTM",
"MU2",
"N2",
"N4",
"NU2",
"O1",
"P1",
"Q1",
"R2",
"S1",
"S2",
"S4",
"SA",
"SSA",
"T2",
]
for comp in comps:
fp = DATA_DIR / "fes2014_amp_ph" / ("02_" + comp.lower() + ".p")
tide[comp.lower()] = pd.read_pickle(fp)
amplitudes[comp.lower()] = tide[comp.lower()]["amplitude"]["scheveningen"]
phases[comp.lower()] = tide[comp.lower()]["phase"]["scheveningen"]
# Choose th initial time for calculating the tidal signal (has to be between 1977-2017)
initial_time = datetime(1977, 1, 1, 12, 0, 0)
tidal_signal = uptide.Tides(comps) # select which constituents to use, we will use all
tidal_signal.set_initial_time(
initial_time
) # set t=0 at 1 Jan 1977, UTC 12:00, arbitrary choice
amp = [amplitudes[comp.lower()] for comp in comps]
pha = [math.radians(phases[comp.lower()]) for comp in comps] # phase (in radians!)
t = np.arange(
0, 365 * 40 * 24 * 3600, 900
) # seconds since initial time, 40 years since 1977, 15min frequency
dates = np.array(
[initial_time + timedelta(seconds=int(s)) for s in t]
) # so that we have datetime on x-axis
eta = tidal_signal.from_amplitude_phase(amp, pha, t) # calculate the signal
eta_df = pd.DataFrame({"eta": eta}, index=dates)
## Plot
# Choose a time window to plot (has to be between 1977 - 2017)
start_date = "2015-05-01 00:00"
end_date = "2015-05-20 00:00"
filtered_gauge = tide_gauge[start_date:end_date]
filtered_tide = eta_df[start_date:end_date]
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(
filtered_tide.index,
filtered_tide["eta"] / 100,
color="blue",
label="Tidal signal",
linewidth=0.7,
)
ax.plot(
filtered_gauge.index,
filtered_gauge,
color="black",
label="Observed sea level",
linewidth=0.7,
)
ax.set_xlabel("Time")
ax.set_ylabel("Sea level [m]")
ax.set_title("Scheveningen")
ax.legend()
plt.tight_layout()
plt.show()
Downloading data from 'https://coclico.blob.core.windows.net/coastal-dynamics/2_wind_waves_tides/Scheveningen_GESLA.pkl' to file '/home/runner/.cache/pooch/178ff95a007d3ed66d9e4b40f241fe43-Scheveningen_GESLA.pkl'.
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
Cell In[2], line 53
51 for comp in comps:
52 fp = DATA_DIR / "fes2014_amp_ph" / ("02_" + comp.lower() + ".p")
---> 53 tide[comp.lower()] = pd.read_pickle(fp)
54 amplitudes[comp.lower()] = tide[comp.lower()]["amplitude"]["scheveningen"]
55 phases[comp.lower()] = tide[comp.lower()]["phase"]["scheveningen"]
File ~/micromamba/envs/coastal/lib/python3.11/site-packages/pandas/io/pickle.py:185, in read_pickle(filepath_or_buffer, compression, storage_options)
123 """
124 Load pickled pandas object (or any object) from file.
125
(...)
182 4 4 9
183 """
184 excs_to_catch = (AttributeError, ImportError, ModuleNotFoundError, TypeError)
--> 185 with get_handle(
186 filepath_or_buffer,
187 "rb",
188 compression=compression,
189 is_text=False,
190 storage_options=storage_options,
191 ) as handles:
192 # 1) try standard library Pickle
193 # 2) try pickle_compat (older pandas version) to handle subclass changes
194 # 3) try pickle_compat with latin-1 encoding upon a UnicodeDecodeError
196 try:
197 # TypeError for Cython complaints about object.__new__ vs Tick.__new__
198 try:
File ~/micromamba/envs/coastal/lib/python3.11/site-packages/pandas/io/common.py:882, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
873 handle = open(
874 handle,
875 ioargs.mode,
(...)
878 newline="",
879 )
880 else:
881 # Binary mode
--> 882 handle = open(handle, ioargs.mode)
883 handles.append(handle)
885 # Convert BytesIO or file objects passed with an encoding
FileNotFoundError: [Errno 2] No such file or directory: '/home/runner/work/CoastalCodebook/CoastalCodebook/book/data/fes2014_amp_ph/02_eps2.p'
What if we don’t use all 34 tidal constituents?
Below is an interactive plot in which you can adjust the plotted time range using the slider (from 1 day to 1 year). You can select which tidal constituents should be plotted with the tick boxes. This way you can play around with different constituents and see what kind of signal you get and how close it is to the total tidal signal.
The plot displays the individual tidal components (upper plot), their combined tidal signal (second plot), the combined tidal signal against the total tidal signal (third plot), and the observed sea level with the total tidal signal and the combined signal (fourth plot).
# We choose one year to plot, 2000-2001
## Download and load previously calculated tidal signal per constituent
scheveningen_fp = pooch.retrieve(
"https://coclico.blob.core.windows.net/coastal-dynamics/2_wind_waves_tides/tide_scheveningen.p",
known_hash="4ebac210fc0893e52655cbc3c9501a6c805e3537f327fed7edb9e7dbfe7fa06a",
)
with open(scheveningen_fp, "rb") as pickle_file:
scheveningen = pickle.load(pickle_file)
## Plot
plot_timeseries_with_interactive_controls(tide_gauge, eta_df, scheveningen)
Now we focus on the interplay between some of the main semi-diurnal and diurnal components. Use the interactive plot above and try to answer the questions below. Give your numeric answer with at least three significant figures.
## Write your code here to get the answer to the questions. Use the table of principal constituents and equation 3.24b from the textbook
## Run this cell to get questions
questions_2d()
Tidal Amplitudes#
Figure 3.27 in the textbook shows a spectrum of equilibrium tides. Let’s now look at the amplitudes of the tidal constituents at Scheveningen, obtained from FES2014, in a similar way.
What are the main constituents?
What are their frequencies (compared to the periods in Table 1)?
Can you recognize the tides near twice a day and near once a day?
How do the diurnal and semi-diurnal constituents that we used above relate to this larger set?
How do the longer-period constituents that we used above relate to this larger set?
Can you recognize the non-linear shallow water tides and longer-period tides?
## Load FES2014 amplitudes
data_dir_path = Path("../data/fes2014_amp_ph/")
tide = {}
amplitudes = {}
# This time we will include more constituents:
comps = [
"eps2",
"j1",
"k1",
"k2",
"l2",
"lambda2",
"m2",
"m3",
"m4",
"m6",
"m8",
"mf",
"mm",
"mn4",
"ms4",
"msf",
"mtm",
"mu2",
"n2",
"nu2",
"o1",
"p1",
"q1",
"r2",
"s1",
"s2",
"s4",
"sa",
"ssa",
"t2",
]
for comp in comps:
fp = data_dir_path / ("02_" + comp + ".p")
tide[comp] = pd.read_pickle(fp)
amplitudes[comp] = tide[comp]["amplitude"]["scheveningen"]
component_names = list(amplitudes.keys())
component_names_upper = [
comp.upper() for comp in component_names
] # Convert to uppercase
amplitude_values = [value / 100 for value in amplitudes.values()]
periods = [
13.13,
23.09848146,
23.93447213,
11.96723606,
12.19162085,
12.22177348,
12.4206012,
8.280400802,
6.210300601,
4.140200401,
3.105150301,
327.8599387,
661.3111655,
6.269173724,
6.103339275,
354.3670666,
219,
12.8717576,
12.65834751,
12.62600509,
25.81933871,
24.06588766,
26.868350,
11.98359564,
24,
12,
6,
8766.15265,
4383.076325,
12.01644934,
] # in [h]
frequency = [1 / (period / 24) for period in periods] # in [1/days]
semidiurnal = ["m2", "s2", "n2", "k2"]
diurnal = ["k1", "o1", "p1", "q1"]
## Bar plot
fig, ax = plt.subplots(figsize=(12, 6))
bar_width = 0.025 # Using a small width for better visualization
for i, freq in enumerate(frequency):
ax.bar(freq, amplitude_values[i], color="skyblue", width=bar_width)
# Plot semi-diurnal components in orange
if component_names[i] in semidiurnal:
ax.bar(freq, amplitude_values[i], color="orange", width=bar_width)
# Plot diurnal components in red
if component_names[i] in diurnal:
ax.bar(freq, amplitude_values[i], color="red", width=bar_width)
ax.text(
freq,
amplitude_values[i],
component_names[i].upper(),
ha="center",
va="bottom",
rotation=0,
)
ax.set_xlabel("Frequency [1/days]")
ax.set_ylabel("Amplitude [m]")
ticks = np.arange(0, max(frequency) + 0.1, 0.5)
plt.xticks(rotation=45, ha="right", ticks=ticks)
plt.xlim([0 - 0.1, max(frequency) + 0.1])
legend_elements = [
plt.Line2D(
[0],
[0],
marker="o",
color="w",
markerfacecolor="orange",
markersize=10,
label="Semi-diurnal",
),
plt.Line2D(
[0],
[0],
marker="o",
color="w",
markerfacecolor="red",
markersize=10,
label="Diurnal",
),
]
ax.legend(handles=legend_elements)
plt.yscale("log") # comment this line to see the differences better
plt.show()
Tidal Propagation#
In chapter 3.8 of the textbook, you learned that the propagation of the tide is influenced by the Coriolis acceleration and friction and resonances determined by the shapes and depths of the ocean basins and marginal seas.
This propagation can be visualised by mapping the lines of simultaneous high water (co-tidal or co-phase lines) and the lines of equal tidal range (co-range lines), as in the figure below. Examine the M2 amplitude and phase pattern along the Atlantic, computed from the FES2014 data. What insights does this pattern provide? Why are the co-tidal (co-phase) lines not equally spaced? Can you link the amplitudes and phases to the Figure 3.30 from the textbook?
### Script for loading FES2014 data (phase and amplitudes)
# import xarray as xr
# from os.path import join
# import pandas as pd
# path = '.../ocean_tide_extrapolated/' # Your path to data
# # Rename la2 to lambda2 if you plan on using uptide
# comps = ['eps2', 'j1', 'k1', 'k2', 'l2', 'la2', 'm2', 'm3', 'm4', 'm6', 'm8', 'mf', 'mks2',
# 'mm', 'mn4', 'ms4', 'msf', 'msqm', 'mtm', 'mu2', 'n2', 'n4', 'nu2', 'o1', 'p1', 'q1',
# 'r2', 's1', 's2', 's4', 'sa', 'ssa', 't2']
# # Resolution of fes2014 is 0.0625x0.0625 (lat,lon), we choose a few
# # locations and the closest model points
# locs = {
# 'scheveningen' : [52.125, 4.25], #lat, lon
# 'galveston' : [29.25, -94.6875],
# 'valparaiso' : [-33, -71.625],
# 'jakarta' : [-6.0625, 106.8125]
# }
# tide = {}
## Extract constituents from FES2014 and store pickle files
# for comp in comps:
# data = xr.open_dataset(join(path, comp + '.nc'))
# data.coords['lon'] = (data.coords['lon'] + 180) % 360 - 180 #lon=[0,360]
# temp = {}
# for loc in locs.keys():
# temp[loc] = (data.sel(lat=locs[loc][0],lon=locs[loc][1])
# .to_dataframe()
# .drop(['lat_bnds', 'lon_bnds', 'crs'], axis=1)
# .drop(1)
# .assign(place=loc)
# .set_index('place', drop=True)
# )
# tide[comp] = pd.concat(temp.values())
# tide[comp].to_pickle('path/02_%s.p' % comp) # Choose your path