Use seaduck.Particle with AVISO#

Particles are simulated in the Southern Ocean AVISO altimetry surface-ocean velocity field.

Author: Wenrui Jiang, Tom Haine Feb ‘23

Hide code cell source
import cartopy.crs as ccrs
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

import seaduck as sd

mpl.rcParams["figure.dpi"] = 300

Loading data#

The velocity field dataset is derived from AVISO sea surface height products, which were processed by SSALTO/DUACS and distributed by AVISO+ (https://www.aviso.altimetry.fr) with support from CNESThose products were processed by SSALTO/DUACS and distributed by AVISO+ (https://www.aviso.altimetry.fr) with support from CNES. The velocities are defined on a lat-lon grid with staggered velocity components. It’s a snapshot from a single time (see below). seaduck.utils provides a few datasets for testing and demonstration purposes. The corresponding dataset is called when the corresponding functions are called.

Download: If you are running this notebook for the first time, The dataset needs to be downloaded and cached, which can be a little slow.
ds = sd.utils.get_dataset("aviso")
ds
<xarray.Dataset> Size: 4MB
Dimensions:  (X: 3600, Xp1: 3600, Y: 300, Yp1: 300, time: 1)
Coordinates:
  * X        (X) float32 14kB -179.9 -179.8 -179.7 -179.6 ... 179.8 179.9 180.0
  * Xp1      (Xp1) float32 14kB -179.9 -179.9 -179.8 ... 179.8 179.9 179.9
  * Y        (Y) float32 1kB -74.9 -74.8 -74.7 -74.6 ... -45.3 -45.2 -45.1 -45.0
  * Yp1      (Yp1) float32 1kB -74.95 -74.85 -74.75 ... -45.25 -45.15 -45.05
  * time     (time) datetime64[ns] 8B 2017-01-01
Data variables:
    lat      (Y) float32 1kB dask.array<chunksize=(300,), meta=np.ndarray>
    lon      (X) float32 14kB dask.array<chunksize=(3600,), meta=np.ndarray>
    u        (Yp1, X) float16 2MB dask.array<chunksize=(75, 1800), meta=np.ndarray>
    v        (Y, Xp1) float16 2MB dask.array<chunksize=(75, 1800), meta=np.ndarray>
Attributes: (12/42)
    Conventions:                     CF-1.6
    Metadata_Conventions:            Unidata Dataset Discovery v1.0
    cdm_data_type:                   Grid
    comment:                         Sea Surface Height measured by Altimetry...
    contact:                         aviso@altimetry.fr
    creator_email:                   aviso@altimetry.fr
    ...                              ...
    summary:                         CLS Delayed-Time Level-4 sea surface hei...
    time_coverage_duration:          P1D
    time_coverage_end:               2017-01-01T12:00:00Z
    time_coverage_resolution:        P1D
    time_coverage_start:             2016-12-31T12:00:00Z
    title:                           DT merged all satellites Global Gridded ...

Now, create the OceData object which provides methods to translate between lat-lon and grid-oriented coords. The model coords frequently needed are also cached.

bathtub = sd.OceData(ds)

The object also contains a tp (topology) object. It looks at the shape of the coordinate data. In this Southern Ocean case, based on the longitude range, it thinks the type is x_periodic, which is correct for the Southern Ocean.

bathtub.tp.typ
'x_periodic'

We’ve prepared the bathtub that the ducks are going to swim (passively drift) in. Now decide where and when to drop them.

We are going to use a short-hand defined in seaduck.utils to define the initial position for simplicity. The returns are normally four 1D numpy array of lon, lat, dep, and time. It’s as simple as that.

# Define the extend of the box
west = -180.0
east = 180.0
south = -74.99
north = -40.01
shallow = -10.0
deep = -10.0

time = "1970-01-01"

Nlon = 300  # How many along longitudinal direction?
Nlat = 30  # How many along latitudinal direction?
Ndep = 1  # How many along vertical direction?

x, y, z, t = sd.utils.easy_3d_cube(
    (west, east, Nlon),
    (south, north, Nlat),
    (shallow, deep, Ndep),
    time,
    print_total_number=True,
)
A total of 9000 positions are defined.

Here is where the particles start on the map:

Hide code cell source
plt.figure(figsize=(16, 9))
ax = plt.axes(projection=ccrs.SouthPolarStereo(central_longitude=170.0))
ax.plot(x, y, "r+", markersize=1, transform=ccrs.PlateCarree())
ax.coastlines()
ax.set_title("Particle initial position")
plt.show()
../_images/98ead2181fabf74a2e386674734e9e37b53e618b2f70c030f5da5d3ffb5b82e4.png

Fig.1 Initial position of the particles.

The AVISO velocity data only has the horizontal velocity component. Or in other words, all the particles are assumed to be at the same (implicit) level, which is the surface for this example. seaduck takes care of that by just setting:

z = None

The AVISO velocity field is a snapshot and does not have a time dimension. Therefore, the time is only valid in a relative sense, i.e. how long has the simulation gone in “real” time.

The standard format for time in seaduck is seconds since 1970-01-01 00:00, and it could be negative. Remember what time is set as the initial time?

all(t == 0)
True

Let’s now define the duration of the simulation (end before start means integrate backwards in time). We can again use a short hand from seaduck.utils.

tf = sd.utils.convert_time("1970-02-01")

This is equivalent to:

We’re not interested when particles leave the domain, so we disregard the ones that leave from the northern boundary by defining:

def interested_in(p):
    return np.logical_and(-74.5 < p.lat, p.lat < -45.5)

Create the OceInterp.lagrangian.particle object.#

We have all the information we need. The final step of preparation is to create the seaduck.lagrangian.particle object. We need to tell the seaduck this information about the particles: where and when they start, which bathtub they’re in, and the names of the velocity components; because it’s not great at guessing.

p = sd.Particle(
    x=x,
    y=y,
    z=z,
    t=t,
    data=bathtub,
    uname="u",
    vname="v",
    wname=None,
    callback=interested_in,
)
p
<seaduck.lagrangian.Particle at 0x7fb8d3073010>

Perform the particle trajectory simulation.#

The to_list_of_time method does the Lagrangian trajectory calculation.

Notice how we define when to dump output (normal_stops) and when to update the velocity field (update_stops). By default, the stops returned by the integration is the combination (union) of normal_stops and update_stops. raw is a list of OceInterp.eulerian.position objects with the same length as stops.

normal_stops = np.linspace(t[0], tf, 10)
stops, raw = p.to_list_of_time(normal_stops=normal_stops)
1970-01-01T00:00:00
1970-01-04T10:40:00
1970-01-07T21:20:00
1970-01-11T08:00:00
1970-01-14T18:40:00
1970-01-18T05:20:00
1970-01-21T16:00:00
1970-01-25T02:40:00
1970-01-28T13:20:00
1970-02-01T00:00:00

Plotting#

First we extract the longitude and latitude from the raw object.

lons = np.array([pt.lon for pt in raw]).T
lats = np.array([pt.lat for pt in raw]).T

Some particles passed through the dateline (periodic boundary). Here is some post-processing to handle the plot.

Hide code cell source
for i in range(len(lons)):
    diff = np.diff(lons[i])

    if max(abs(diff)) > 330:
        for j in np.where(abs(diff) > 300)[0]:
            j = int(j)
            lons[i, j + 1 :] -= np.sign(diff[j]) * 360

Voila!

Hide code cell source
plt.figure(figsize=(9, 16))
ax = plt.axes(projection=ccrs.SouthPolarStereo(central_longitude=170.0))
ax.plot(lons.T, lats.T, "darkblue", lw=0.3, transform=ccrs.PlateCarree())
ax.coastlines()
ax.set_title("Particle trajectories")
plt.show()
../_images/33eb627dc1a3a35556a1a5cea32e850316c9d620f285291cd0d21a777b2efd78.png

Fig.2 The trajectories of particles advected by AVISO-derived surface velocity field.

Advanced use of Particle#

In this subsection, we are going to demonstrate how to access the analytical trajectories of particles. We are also going to demonstrate the flexibility of Particle release.

Most of the notebooks release particles at the same time. We are going to do it differently this time. They are going to be released at 64W and 55S and 64S. Crucially, all of the particles are released at a different time.

Hide code cell source
N = 777
t_earliest = sd.utils.convert_time("1970-01-01")
t_final = sd.utils.convert_time("1970-02-01")
t_latest = sd.utils.convert_time("1970-03-03")  # damn you, February.
t = np.linspace(t_earliest, t_latest, N)

number_of_loop = 6.18
# y = -65.0+np.abs(10-np.linspace(0,number_of_loop*20,N)%20)
y = -59.5 + 4.5 * np.sin(number_of_loop * np.linspace(-3.14, 3.14, N))
x = np.ones(N) * (-64.0)
z = -np.ones(N)

To better demonstrate, here is a plot showing the release pattern of the particle. The time is referenced against the final time of the simulation. Note that every single particle will be released at a different time and there are going to be both forward and backward particles in the same simulation.

This is as if there is a ship is commuting in drake passage while releasing particles.

Hide code cell source
plt.plot(t, y, "o", markersize=1)
plt.xticks(
    [t_earliest, t_final - 15 * 86400, t_final, t_final + 15 * 86400, t_latest],
    ["-31 days", "-15 days", "0", "+15 days", "+31 days"],
)
plt.ylabel("Latitude")
plt.title("Latitude of particles released at 64W")
plt.show()
../_images/8fa97b4c5638d2e8beeca7d4473877f8267acaff32d847b026b58efcb27bf85c.png

Fig.3 Pattern of particle release. Time is relative to the final time.

The only difference in preparation between this one and the previous simulation is that we have save_raw = True. This means the particles will record all the necessary informations to reconstruct the analytical trajectory.

p = sd.Particle(
    x=x,
    y=y,
    z=z,
    t=t,
    data=bathtub,
    uname="u",
    vname="v",
    wname=None,
    callback=interested_in,
    save_raw=True,
)

No tricks need to be played while execution.

%%time
p.to_next_stop(t_final)
CPU times: user 1.21 s, sys: 16 ms, total: 1.23 s
Wall time: 1.23 s

When save_raw = True is selected, the Particle object records location and velocity information everytime velocity is updated. The following plot is plotted from longitude and latitudes of particles crossing cell walls. Each trajectory is colored based on the time of release, Purple is the earliest, red is the latest (furthest into the future).

Hide code cell source
plt.figure(figsize=(9, 16))
ax = plt.axes(projection=ccrs.PlateCarree())
rainbow = plt.get_cmap("rainbow")
for i in range(0, N):
    color = rainbow(t[i] / 2 / t_final)
    ax.plot([x[i]] + p.xxlist[i], [y[i]] + p.yylist[i], color=color, lw=0.2)
ax.coastlines()
plt.show()
../_images/d2875e5fb88752135173e1ec40d30ee31c4a39642fea9e0c532930887e99e846.png

Fig.4 Trajectory of particles released at different time. Warm color are particles released after the final time, and cold colors are those released before the final time.