Welcome to py-eddy-tracker’s documentation!

How do I get set up ?

Source are available on github https://github.com/AntSimi/py-eddy-tracker

Use python3. To avoid problems with installation, use of the virtualenv Python virtual environment is recommended or conda.

Then use pip to install all dependencies (numpy, scipy, matplotlib, netCDF4, …), e.g.:

pip install numpy scipy netCDF4 matplotlib opencv-python pyyaml pint polygon3

Then run the following to install the eddy tracker:

python setup.py install

Several executables are available in your PATH:

GridFiltering # Allow to apply a high frequency filter on a NetCDF grid
EddyId # Provide identification of eddies for one grid
EddySubSetter # Allow to apply sub setting on eddies dataset
EddyTracking # Allow to track Identification dataset

Py eddy tracker toolbox

All figures in this gallery, used an experimental dataset, compute with this dataset : cmems_product.

Eddy detection

Display contour & circle

from matplotlib import pyplot as plt

from py_eddy_tracker import data
from py_eddy_tracker.observations.observation import EddiesObservations

Load detection files

a = EddiesObservations.load_file(data.get_path("Anticyclonic_20190223.nc"))

Plot the speed and effective (dashed) contours

fig = plt.figure(figsize=(15, 8))
ax = fig.add_axes((0.05, 0.05, 0.9, 0.9))
ax.set_aspect("equal")
ax.set_xlim(10, 70)
ax.set_ylim(-50, -25)
a.display(ax, label="Anticyclonic contour", color="r", lw=1)

# Replace contours by circles using center and radius (effective is dashed)
a.circle_contour()
a.display(ax, label="Anticyclonic circle", color="g", lw=1)
ax.legend(loc="upper right")
pet contour circle

Total running time of the script: ( 0 minutes 1.568 seconds)

Gallery generated by Sphinx-Gallery

Display identification

from matplotlib import pyplot as plt

from py_eddy_tracker import data
from py_eddy_tracker.observations.observation import EddiesObservations

Load detection files

a = EddiesObservations.load_file(data.get_path("Anticyclonic_20190223.nc"))
c = EddiesObservations.load_file(data.get_path("Cyclonic_20190223.nc"))

Fill effective contour with amplitude

fig = plt.figure(figsize=(15, 8))
ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
ax.set_aspect("equal")
ax.set_xlim(0, 140)
ax.set_ylim(-80, 0)
kwargs = dict(extern_only=True, color="k", lw=1)
a.display(ax, **kwargs), c.display(ax, **kwargs)
a.filled(ax, "amplitude", cmap="magma_r", vmin=0, vmax=0.5)
m = c.filled(ax, "amplitude", cmap="magma_r", vmin=0, vmax=0.5)
colorbar = plt.colorbar(m, cax=ax.figure.add_axes([0.95, 0.03, 0.02, 0.94]))
colorbar.set_label("Amplitude (m)")
pet display id

Draw speed contours

fig = plt.figure(figsize=(15, 8))
ax = fig.add_axes([0.03, 0.03, 0.94, 0.94])
ax.set_aspect("equal")
ax.set_xlim(0, 360)
ax.set_ylim(-80, 80)
a.display(ax, label="Anticyclonic ({nb_obs} eddies)", color="r", lw=1)
c.display(ax, label="Cyclonic ({nb_obs} eddies)", color="b", lw=1)
ax.legend(loc="upper right")
pet display id

Get general informations

print(a)

Out:

| 3137 observations from 25255 to 25255 (1 days, ~3137 obs/day)
|   Speed area      : 32.98 Mkm²/day
|   Effective area  : 45.65 Mkm²/day
----Distribution in Amplitude:
|   Amplitude bounds (cm)        0.00      1.00      2.00      3.00      4.00      5.00     10.00    500.00
|   Percent of eddies         :      19.35     22.73     15.40     10.30      6.18     15.91     10.14
----Distribution in Radius:
|   Speed radius (km)            0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
|   Percent of eddies         :       0.00      9.47     34.56     24.55     13.29     11.67      6.34      0.13
|   Effective radius (km)        0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
|   Percent of eddies         :       0.00      7.52     26.62     20.88     15.40     15.94     13.32      0.32
----Distribution in Latitude
    Latitude bounds            -90.00    -60.00    -15.00     15.00     60.00     90.00
    Percent of eddies         :       7.62     46.86     12.81     30.06      2.65
    Percent of speed area     :       4.69     41.94     26.90     25.30      1.17
    Percent of effective area :       4.74     43.40     25.53     25.11      1.21
    Mean speed radius (km)    :      43.94     52.75     81.69     51.01     37.91
    Mean effective radius (km):      52.14     62.43     94.14     59.44     44.81
    Mean amplitude (cm)       :       3.53      5.30      2.19      4.32      3.12
print(c)

Out:

| 3360 observations from 25255 to 25255 (1 days, ~3360 obs/day)
|   Speed area      : 32.89 Mkm²/day
|   Effective area  : 46.42 Mkm²/day
----Distribution in Amplitude:
|   Amplitude bounds (cm)        0.00      1.00      2.00      3.00      4.00      5.00     10.00    500.00
|   Percent of eddies         :      18.81     24.02     14.11     10.89      5.98     16.19     10.00
----Distribution in Radius:
|   Speed radius (km)            0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
|   Percent of eddies         :       0.03     10.15     35.03     25.15     14.40     10.09      5.12      0.03
|   Effective radius (km)        0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
|   Percent of eddies         :       0.03      7.98     26.88     21.61     15.92     15.09     12.14      0.36
----Distribution in Latitude
    Latitude bounds            -90.00    -60.00    -15.00     15.00     60.00     90.00
    Percent of eddies         :       7.92     46.96     13.12     29.61      2.38
    Percent of speed area     :       4.80     41.08     27.30     25.87      0.93
    Percent of effective area :       4.83     42.35     25.36     26.55      0.92
    Mean speed radius (km)    :      42.23     50.71     78.76     50.80     34.64
    Mean effective radius (km):      49.25     60.50     89.91     59.96     40.20
    Mean amplitude (cm)       :       3.19      5.71      2.19      4.24      2.42

Total running time of the script: ( 0 minutes 2.586 seconds)

Gallery generated by Sphinx-Gallery

Radius vs area

from matplotlib import pyplot as plt
from numpy import array, pi

from py_eddy_tracker import data
from py_eddy_tracker.generic import coordinates_to_local
from py_eddy_tracker.observations.observation import EddiesObservations
from py_eddy_tracker.poly import poly_area

Load detection files

a = EddiesObservations.load_file(data.get_path("Anticyclonic_20190223.nc"))
areas = list()
# For each contour area will be compute in local reference
for i in a:
    x, y = coordinates_to_local(
        i["contour_lon_s"], i["contour_lat_s"], i["lon"], i["lat"]
    )
    areas.append(poly_area(x, y))
areas = array(areas)

Radius provided by eddy detection is computed with fit_circle() method. This radius will be compared with an equivalent radius deduced from polygon area.

ax = plt.subplot(111)
ax.set_aspect("equal")
ax.grid()
ax.set_xlabel("Speed radius computed with fit_circle")
ax.set_ylabel("Radius deduced from area\nof contour_lon_s/contour_lat_s")
ax.set_title("Area vs radius")
ax.plot(a["radius_s"] / 1000.0, (areas / pi) ** 0.5 / 1000.0, ".")
ax.plot((0, 250), (0, 250), "r")
Area vs radius

Fit circle give a radius bigger than polygon area

When error is tiny, radius are very close.

ax = plt.subplot(111)
ax.grid()
ax.set_xlabel("Radius ratio")
ax.set_ylabel("Shape error")
ax.set_title("err = f(radius_ratio)")
ax.plot(a["radius_s"] / (areas / pi) ** 0.5, a["shape_error_s"], ".")
err = f(radius_ratio)

Total running time of the script: ( 0 minutes 3.475 seconds)

Gallery generated by Sphinx-Gallery

Get mean of grid in each eddies

from matplotlib import pyplot as plt

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
from py_eddy_tracker.observations.observation import EddiesObservations
def start_axes(title):
    fig = plt.figure(figsize=(13, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title)
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    ax.legend()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.95, 0.05, 0.01, 0.9]))

Load detection files and data to interp

a = EddiesObservations.load_file(data.get_path("Anticyclonic_20160515.nc"))
c = EddiesObservations.load_file(data.get_path("Cyclonic_20160515.nc"))

aviso_map = RegularGridDataset(
    data.get_path("dt_med_allsat_phy_l4_20160515_20190101.nc"), "longitude", "latitude"
)
aviso_map.add_uv("adt")

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/dt_med_allsat_phy_l4_20160515_20190101.nc

Compute and store eke in cm²/s²

aviso_map.add_grid(
    "eke", (aviso_map.grid("u") ** 2 + aviso_map.grid("v") ** 2) * 0.5 * (100 ** 2)
)

eke_kwargs = dict(vmin=1, vmax=1000, cmap="magma_r")

ax = start_axes("EKE (cm²/s²)")
m = aviso_map.display(ax, "eke", **eke_kwargs)
a.display(ax, color="r", linewidth=0.5, label="Anticyclonic", ref=-10)
c.display(ax, color="b", linewidth=0.5, label="Cyclonic", ref=-10)
update_axes(ax, m)
EKE (cm²/s²)

Get mean of eke in each effective contour

ax = start_axes("EKE mean (cm²/s²)")
a.display(ax, color="r", linewidth=0.5, label="Anticyclonic ({nb_obs} eddies)", ref=-10)
c.display(ax, color="b", linewidth=0.5, label="Cyclonic ({nb_obs} eddies)", ref=-10)
eke = a.interp_grid(aviso_map, "eke", method="mean", intern=False)
a.filled(ax, eke, ref=-10, **eke_kwargs)
eke = c.interp_grid(aviso_map, "eke", method="mean", intern=False)
m = c.filled(ax, eke, ref=-10, **eke_kwargs)
update_axes(ax, m)
EKE mean (cm²/s²)

Total running time of the script: ( 0 minutes 5.625 seconds)

Gallery generated by Sphinx-Gallery

Eddy detection : Med

Script will detect eddies on adt field, and compute u,v with method add_uv(which could use, only if equator is avoid)

Figures will show different step to detect eddies.

from datetime import datetime

from matplotlib import pyplot as plt
from numpy import arange

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
def start_axes(title):
    fig = plt.figure(figsize=(13, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title, weight="bold")
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.94, 0.05, 0.01, 0.9]))

Load Input grid, ADT is used to detect eddies

g = RegularGridDataset(
    data.get_path("dt_med_allsat_phy_l4_20160515_20190101.nc"), "longitude", "latitude"
)

ax = start_axes("ADT (m)")
m = g.display(ax, "adt", vmin=-0.15, vmax=0.15, cmap="RdBu_r")
update_axes(ax, m)
ADT (m)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/dt_med_allsat_phy_l4_20160515_20190101.nc

Get geostrophic speed u,v

U/V are deduced from ADT, this algortihm is not ok near the equator (~+- 2°)

g.add_uv("adt")
ax = start_axes("U/V deduce from ADT (m)")
ax.set_xlim(2.5, 9), ax.set_ylim(37.5, 40)
m = g.display(ax, "adt", vmin=-0.15, vmax=0.15, cmap="RdBu_r")
u, v = g.grid("u").T, g.grid("v").T
ax.quiver(g.x_c, g.y_c, u, v, scale=10)
update_axes(ax, m)
U/V deduce from ADT (m)

Pre-processings

Apply a high-pass filter to remove the large scale and highlight the mesoscale

g.bessel_high_filter("adt", 500)
ax = start_axes("ADT (m) filtered (500km)")
m = g.display(ax, "adt", vmin=-0.15, vmax=0.15, cmap="RdBu_r")
update_axes(ax, m)
ADT (m) filtered (500km)

Identification

Run the identification step with slices of 2 mm

date = datetime(2016, 5, 15)
a, c = g.eddy_identification("adt", "u", "v", date, 0.002, shape_error=55)

Display of all closed contours found in the grid (only 1 contour every 4)

ax = start_axes("ADT closed contours (only 1 / 4 levels)")
g.contours.display(ax, step=4)
update_axes(ax)
ADT closed contours (only 1 / 4 levels)

Contours included in eddies

ax = start_axes("ADT contours used as eddies")
g.contours.display(ax, only_used=True)
update_axes(ax)
ADT contours used as eddies

Post analysis

Contours can be rejected for several reasons (shape error to high, several extremum in contour, …)

ax = start_axes("ADT rejected contours")
g.contours.display(ax, only_unused=True)
update_axes(ax)
ADT rejected contours

Creteria for rejecting a contour 0. - Accepted (green) 1. - Rejection for shape error (red) 2. - Masked value within contour (blue) 3. - Under or over the pixel limit bounds (black) 4. - Amplitude criterion (yellow)

ax = start_axes("Contours' rejection criteria")
g.contours.display(ax, only_unused=True, lw=0.5, display_criterion=True)
update_axes(ax)
Contours' rejection criteria

Display the shape error of each tested contour, the limit of shape error is set to 55 %

ax = start_axes("Contour shape error")
m = g.contours.display(
    ax, lw=0.5, field="shape_error", bins=arange(20, 90.1, 5), cmap="PRGn_r"
)
update_axes(ax, m)
Contour shape error

Some closed contours contains several eddies (aka, more than one extremum)

ax = start_axes("ADT rejected contours containing eddies")
g.contours.label_contour_unused_which_contain_eddies(a)
g.contours.label_contour_unused_which_contain_eddies(c)
g.contours.display(
    ax,
    only_contain_eddies=True,
    color="k",
    lw=1,
    label="Could be a contour of interaction",
)
a.display(ax, color="r", linewidth=0.75, label="Anticyclonic", ref=-10)
c.display(ax, color="b", linewidth=0.75, label="Cyclonic", ref=-10)
ax.legend()
update_axes(ax)
ADT rejected contours containing eddies

Output

When displaying the detected eddies, dashed lines are for effective contour, solide lines for the contour of the maximum mean speed. See figure 1 of https://doi.org/10.1175/JTECH-D-14-00019.1

ax = start_axes("Detected Eddies")
a.display(
    ax, color="r", linewidth=0.75, label="Anticyclonic ({nb_obs} eddies)", ref=-10
)
c.display(ax, color="b", linewidth=0.75, label="Cyclonic ({nb_obs} eddies)", ref=-10)
ax.legend()
update_axes(ax)
Detected Eddies

Display the speed radius of the detected eddies

ax = start_axes("Speed Radius (km)")
a.scatter(ax, "radius_s", vmin=10, vmax=50, s=80, ref=-10, cmap="magma_r", factor=0.001)
m = c.scatter(
    ax, "radius_s", vmin=10, vmax=50, s=80, ref=-10, cmap="magma_r", factor=0.001
)
update_axes(ax, m)
Speed Radius (km)

Filling the effective radius contours with the effective radius values

ax = start_axes("Effective Radius (km)")
kwargs = dict(vmin=10, vmax=80, cmap="magma_r", factor=0.001, lut=14, ref=-10)
a.filled(ax, "effective_radius", **kwargs)
m = c.filled(
    ax, "radius_e", vmin=10, vmax=80, cmap="magma_r", factor=0.001, lut=14, ref=-10
)
update_axes(ax, m)
Effective Radius (km)

Total running time of the script: ( 0 minutes 19.600 seconds)

Gallery generated by Sphinx-Gallery

Eddy detection : Gulf stream

Script will detect eddies on adt field, and compute u,v with method add_uv(which could use, only if equator is avoid)

Figures will show different step to detect eddies.

from datetime import datetime

from matplotlib import pyplot as plt
from numpy import arange

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
from py_eddy_tracker.eddy_feature import Contours
def start_axes(title):
    fig = plt.figure(figsize=(13, 8))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(279, 304), ax.set_ylim(29, 44)
    ax.set_aspect("equal")
    ax.set_title(title, weight="bold")
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.94, 0.05, 0.01, 0.9]))

Load Input grid, ADT is used to detect eddies

margin = 30
g = RegularGridDataset(
    data.get_path("nrt_global_allsat_phy_l4_20190223_20190226.nc"),
    "longitude",
    "latitude",
    # Manual area subset
    indexs=dict(
        longitude=slice(1116 - margin, 1216 + margin),
        latitude=slice(476 - margin, 536 + margin),
    ),
)

ax = start_axes("ADT (m)")
m = g.display(ax, "adt", vmin=-1, vmax=1, cmap="RdBu_r")
# Draw line on the gulf stream front
great_current = Contours(g.x_c, g.y_c, g.grid("adt"), levels=(0.35,), keep_unclose=True)
great_current.display(ax, color="k")
update_axes(ax, m)
ADT (m)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/nrt_global_allsat_phy_l4_20190223_20190226.nc

Get geostrophic speed u,v

U/V are deduced from ADT, this algortihm is not ok near the equator (~+- 2°)

g.add_uv("adt")

Pre-processings

Apply a high-pass filter to remove the large scale and highlight the mesoscale

g.bessel_high_filter("adt", 700)
ax = start_axes("ADT (m) filtered (700km)")
m = g.display(ax, "adt", vmin=-0.4, vmax=0.4, cmap="RdBu_r")
great_current.display(ax, color="k")
update_axes(ax, m)
ADT (m) filtered (700km)

Identification

Run the identification step with slices of 2 mm

date = datetime(2016, 5, 15)
a, c = g.eddy_identification("adt", "u", "v", date, 0.002, shape_error=55)

Display of all closed contours found in the grid (only 1 contour every 5)

ax = start_axes("ADT closed contours (only 1 / 5 levels)")
g.contours.display(ax, step=5, lw=1)
great_current.display(ax, color="k")
update_axes(ax)
ADT closed contours (only 1 / 5 levels)

Contours included in eddies

ax = start_axes("ADT contours used as eddies")
g.contours.display(ax, only_used=True, lw=0.25)
great_current.display(ax, color="k")
update_axes(ax)
ADT contours used as eddies

Post analysis

Contours can be rejected for several reasons (shape error to high, several extremum in contour, …)

ax = start_axes("ADT rejected contours")
g.contours.display(ax, only_unused=True, lw=0.25)
great_current.display(ax, color="k")
update_axes(ax)
ADT rejected contours
Criteria for rejecting a contour :
  1. Accepted (green)

  2. Rejection for shape error (red)

  3. Masked value within contour (blue)

  4. Under or over the pixel limit bounds (black)

  5. Amplitude criterion (yellow)

ax = start_axes("Contours' rejection criteria")
g.contours.display(ax, only_unused=True, lw=0.5, display_criterion=True)
update_axes(ax)
Contours' rejection criteria

Display the shape error of each tested contour, the limit of shape error is set to 55 %

ax = start_axes("Contour shape error")
m = g.contours.display(
    ax, lw=0.5, field="shape_error", bins=arange(20, 90.1, 5), cmap="PRGn_r"
)
update_axes(ax, m)
Contour shape error

Some closed contours contains several eddies (aka, more than one extremum)

ax = start_axes("ADT rejected contours containing eddies")
g.contours.label_contour_unused_which_contain_eddies(a)
g.contours.label_contour_unused_which_contain_eddies(c)
g.contours.display(
    ax,
    only_contain_eddies=True,
    color="k",
    lw=1,
    label="Could be a contour of interaction",
)
a.display(ax, color="r", linewidth=0.75, label="Anticyclonic", ref=-10)
c.display(ax, color="b", linewidth=0.75, label="Cyclonic", ref=-10)
ax.legend()
update_axes(ax)
ADT rejected contours containing eddies

Output

When displaying the detected eddies, dashed lines are for effective contour, solide lines for the contour of the maximum mean speed. See figure 1 of https://doi.org/10.1175/JTECH-D-14-00019.1

ax = start_axes("Eddies detected")
a.display(
    ax, color="r", linewidth=0.75, label="Anticyclonic ({nb_obs} eddies)", ref=-10
)
c.display(ax, color="b", linewidth=0.75, label="Cyclonic ({nb_obs} eddies)", ref=-10)
ax.legend()
great_current.display(ax, color="k")
update_axes(ax)
Eddies detected

Display the effective radius of the detected eddies

ax = start_axes("Effective radius (km)")
a.filled(ax, "radius_e", vmin=10, vmax=150, cmap="magma_r", factor=0.001, lut=14)
m = c.filled(ax, "radius_e", vmin=10, vmax=150, cmap="magma_r", factor=0.001, lut=14)
great_current.display(ax, color="k")
update_axes(ax, m)
Effective radius (km)

Total running time of the script: ( 0 minutes 10.348 seconds)

Gallery generated by Sphinx-Gallery

Eddy detection and filter

from datetime import datetime

from matplotlib import pyplot as plt
from numpy import arange

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
def start_axes(title):
    fig = plt.figure(figsize=(13, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title, weight="bold")
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.94, 0.05, 0.01, 0.9]))

Load Input grid, ADT is used to detect eddies. Add a new filed to store the high-pass filtered ADT

g = RegularGridDataset(
    data.get_path("dt_med_allsat_phy_l4_20160515_20190101.nc"), "longitude", "latitude"
)
g.add_uv("adt")
g.copy("adt", "adt_high")
wavelength = 800
g.bessel_high_filter("adt_high", wavelength)
date = datetime(2016, 5, 15)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/dt_med_allsat_phy_l4_20160515_20190101.nc

Run the detection for the total grid and the filtered grid

a_filtered, c_filtered = g.eddy_identification("adt_high", "u", "v", date, 0.002)
merge_f = a_filtered.merge(c_filtered)
a_tot, c_tot = g.eddy_identification("adt", "u", "v", date, 0.002)
merge_t = a_tot.merge(c_tot)

Display the two detections

ax = start_axes("Eddies detected over ADT")
m = g.display(ax, "adt", vmin=-0.15, vmax=0.15)
merge_f.display(
    ax,
    lw=0.75,
    label="Eddies in the filtered grid ({nb_obs} eddies)",
    ref=-10,
    color="k",
)
merge_t.display(
    ax, lw=0.75, label="Eddies without filter ({nb_obs} eddies)", ref=-10, color="r"
)
ax.legend()
update_axes(ax, m)
Eddies detected over ADT

Amplitude and Speed Radius distributions

fig = plt.figure(figsize=(12, 5))
ax_a = fig.add_subplot(121, xlabel="Amplitude (cm)")
ax_r = fig.add_subplot(122, xlabel="Speed Radius (km)")
ax_a.hist(
    merge_f.amplitude * 100,
    bins=arange(0.0005, 100, 1),
    label="Eddies in the filtered grid",
    histtype="step",
)
ax_a.hist(
    merge_t.amplitude * 100,
    bins=arange(0.0005, 100, 1),
    label="Eddies without filter",
    histtype="step",
)
ax_a.set_xlim(0, 10)
ax_r.hist(merge_f.radius_s / 1000.0, bins=arange(0, 300, 5), histtype="step")
ax_r.hist(merge_t.radius_s / 1000.0, bins=arange(0, 300, 5), histtype="step")
ax_r.set_xlim(0, 100)
ax_a.legend()
pet filter and detection

Match detection and compare

i_, j_, c = merge_f.match(merge_t, cmin=0.1)

Where are the lonely eddies?

kwargs_f = dict(lw=1.5, label="Lonely eddies in the filtered grid", ref=-10, color="k")
kwargs_t = dict(lw=1.5, label="Lonely eddies without filter", ref=-10, color="r")
ax = start_axes("Eddies with no match, over filtered ADT")
mappable = g.display(ax, "adt_high", vmin=-0.15, vmax=0.15)
merge_f.index(i_, reverse=True).display(ax, **kwargs_f)
merge_t.index(j_, reverse=True).display(ax, **kwargs_t)
ax.legend()
update_axes(ax, mappable)

ax = start_axes("Eddies with no match, over filtered ADT (zoom)")
ax.set_xlim(25, 36), ax.set_ylim(31, 35.25)
mappable = g.display(ax, "adt_high", vmin=-0.15, vmax=0.15)
u, v = g.grid("u").T, g.grid("v").T
ax.quiver(g.x_c, g.y_c, u, v, scale=10, pivot="mid", color="gray")
merge_f.index(i_, reverse=True).display(ax, **kwargs_f)
merge_t.index(j_, reverse=True).display(ax, **kwargs_t)
ax.legend()
update_axes(ax, mappable)
  • Eddies with no match, over filtered ADT
  • Eddies with no match, over filtered ADT (zoom)
fig = plt.figure(figsize=(12, 12))
fig.suptitle(f"Scatter plot ({i_.shape[0]} matches)", weight="bold")

for i, (label, field, factor, stop) in enumerate(
    (
        ("Speed radius (km)", "radius_s", 0.001, 80),
        ("Effective radius (km)", "radius_e", 0.001, 120),
        ("Amplitude (cm)", "amplitude", 100, 25),
        ("Maximum Speed (cm/s)", "speed_average", 100, 25),
    )
):
    ax = fig.add_subplot(
        2, 2, i + 1, xlabel="Filtered grid", ylabel="Without filter", title=label
    )
    ax.plot(merge_f[field][i_] * factor, merge_t[field][j_] * factor, ".")
    ax.set_aspect("equal"), ax.grid()
    ax.plot((0, 1000), (0, 1000), "r")
    ax.set_xlim(0, stop), ax.set_ylim(0, stop)
Scatter plot (106 matches), Speed radius (km), Effective radius (km), Amplitude (cm), Maximum Speed (cm/s)

Total running time of the script: ( 0 minutes 8.243 seconds)

Gallery generated by Sphinx-Gallery

Eddy detection on SLA and ADT

from datetime import datetime

from matplotlib import pyplot as plt

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
def start_axes(title):
    fig = plt.figure(figsize=(13, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title)
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.95, 0.05, 0.01, 0.9]))

Load Input grid, ADT will be used to detect eddies

g = RegularGridDataset(
    data.get_path("dt_med_allsat_phy_l4_20160515_20190101.nc"), "longitude", "latitude"
)
g.add_uv("adt", "ugos", "vgos")
g.add_uv("sla", "ugosa", "vgosa")
wavelength = 400
g.copy("adt", "adt_raw")
g.copy("sla", "sla_raw")
g.bessel_high_filter("adt", wavelength)
g.bessel_high_filter("sla", wavelength)
date = datetime(2016, 5, 15)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/dt_med_allsat_phy_l4_20160515_20190101.nc
kwargs_a_adt = dict(
    lw=0.5, label="Anticyclonic ADT ({nb_obs} eddies)", ref=-10, color="k"
)
kwargs_c_adt = dict(lw=0.5, label="Cyclonic ADT ({nb_obs} eddies)", ref=-10, color="r")
kwargs_a_sla = dict(
    lw=0.5, label="Anticyclonic SLA ({nb_obs} eddies)", ref=-10, color="g"
)
kwargs_c_sla = dict(lw=0.5, label="Cyclonic SLA ({nb_obs} eddies)", ref=-10, color="b")

Run algorithm of detection

a_adt, c_adt = g.eddy_identification("adt", "ugos", "vgos", date, 0.002)
a_sla, c_sla = g.eddy_identification("sla", "ugosa", "vgosa", date, 0.002)

over filtered

ax = start_axes(f"ADT (m) filtered ({wavelength}km)")
m = g.display(ax, "adt", vmin=-0.15, vmax=0.15)
a_adt.display(ax, **kwargs_a_adt), c_adt.display(ax, **kwargs_c_adt)
ax.legend(), update_axes(ax, m)

ax = start_axes(f"SLA (m) filtered ({wavelength}km)")
m = g.display(ax, "sla", vmin=-0.15, vmax=0.15)
a_sla.display(ax, **kwargs_a_sla), c_sla.display(ax, **kwargs_c_sla)
ax.legend(), update_axes(ax, m)
  • ADT (m) filtered (400km)
  • SLA (m) filtered (400km)

over raw

ax = start_axes("ADT (m)")
m = g.display(ax, "adt_raw", vmin=-0.15, vmax=0.15)
a_adt.display(ax, **kwargs_a_adt), c_adt.display(ax, **kwargs_c_adt)
ax.legend(), update_axes(ax, m)

ax = start_axes("SLA (m)")
m = g.display(ax, "sla_raw", vmin=-0.15, vmax=0.15)
a_sla.display(ax, **kwargs_a_sla), c_sla.display(ax, **kwargs_c_sla)
ax.legend(), update_axes(ax, m)
  • ADT (m)
  • SLA (m)

Display detection

ax = start_axes("Eddies detected")
a_adt.display(ax, **kwargs_a_adt)
a_sla.display(ax, **kwargs_a_sla)
c_adt.display(ax, **kwargs_c_adt)
c_sla.display(ax, **kwargs_c_sla)
ax.legend()
update_axes(ax)
Eddies detected

Match

Where cyclone meet anticyclone

i_c_adt, i_a_sla, c = c_adt.match(a_sla, cmin=0.01)
i_a_adt, i_c_sla, c = a_adt.match(c_sla, cmin=0.01)

ax = start_axes("Cyclone share area with anticyclone")
a_adt.index(i_a_adt).display(ax, **kwargs_a_adt)
c_adt.index(i_c_adt).display(ax, **kwargs_c_adt)
a_sla.index(i_a_sla).display(ax, **kwargs_a_sla)
c_sla.index(i_c_sla).display(ax, **kwargs_c_sla)
ax.legend()
update_axes(ax)
Cyclone share area with anticyclone

Scatter plot

i_a_adt, i_a_sla, c = a_adt.match(a_sla, cmin=0.1)
i_c_adt, i_c_sla, c = c_adt.match(c_sla, cmin=0.1)

where is lonely eddies

ax = start_axes("Eddies with no match")
a_adt.index(i_a_adt, reverse=True).display(ax, **kwargs_a_adt)
c_adt.index(i_c_adt, reverse=True).display(ax, **kwargs_c_adt)
a_sla.index(i_a_sla, reverse=True).display(ax, **kwargs_a_sla)
c_sla.index(i_c_sla, reverse=True).display(ax, **kwargs_c_sla)
ax.legend()
update_axes(ax)
Eddies with no match
fig = plt.figure(figsize=(12, 12))
fig.suptitle(f"Scatter plot (A : {i_a_adt.shape[0]}, C : {i_c_adt.shape[0]} matches)")

for i, (label, field, factor, stop) in enumerate(
    (
        ("speed radius (km)", "radius_s", 0.001, 80),
        ("outter radius (km)", "radius_e", 0.001, 120),
        ("amplitude (cm)", "amplitude", 100, 25),
        ("speed max (cm/s)", "speed_average", 100, 25),
    )
):
    ax = fig.add_subplot(2, 2, i + 1, title=label)
    ax.set_xlabel("Absolute Dynamic Topography")
    ax.set_ylabel("Sea Level Anomaly")

    ax.plot(
        a_adt[field][i_a_adt] * factor,
        a_sla[field][i_a_sla] * factor,
        "r.",
        label="Anticyclonic",
    )
    ax.plot(
        c_adt[field][i_c_adt] * factor,
        c_sla[field][i_c_sla] * factor,
        "b.",
        label="Cyclonic",
    )
    ax.set_aspect("equal"), ax.grid()
    ax.plot((0, 1000), (0, 1000), "g")
    ax.set_xlim(0, stop), ax.set_ylim(0, stop)
    ax.legend()
Scatter plot (A : 48, C : 35 matches), speed radius (km), outter radius (km), amplitude (cm), speed max (cm/s)

Total running time of the script: ( 0 minutes 6.418 seconds)

Gallery generated by Sphinx-Gallery

Grid Manipulation

Select pixel in eddies

from matplotlib import pyplot as plt
from matplotlib.path import Path
from numpy import ones

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
from py_eddy_tracker.observations.observation import EddiesObservations
from py_eddy_tracker.poly import create_vertice

Load an eddy file which contains contours

a = EddiesObservations.load_file(data.get_path("Anticyclonic_20190223.nc"))

Load a grid where we want found pixels in eddies or out

g = RegularGridDataset(
    data.get_path("nrt_global_allsat_phy_l4_20190223_20190226.nc"),
    "longitude",
    "latitude",
)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/nrt_global_allsat_phy_l4_20190223_20190226.nc

For each contours, we will get pixels indice in contour.

fig = plt.figure(figsize=(12, 6))
ax = fig.add_axes((0.05, 0.05, 0.9, 0.9))
ax.set_aspect("equal")
ax.set_xlim(10, 70)
ax.set_ylim(-50, -25)
# We will used the outter contour
x_name, y_name = a.intern(False)
adt = g.grid("adt")
mask = ones(adt.shape, dtype="bool")
for eddy in a:
    i, j = Path(create_vertice(eddy[x_name], eddy[y_name])).pixels_in(g)
    mask[i, j] = False
adt.mask[:] += ~mask
g.display(ax, "adt")
a.display(ax, label="Anticyclonic", color="g", lw=1, extern_only=True)
pet hide pixel out eddies
fig = plt.figure(figsize=(12, 6))
ax = fig.add_axes((0.05, 0.05, 0.9, 0.9))
ax.set_aspect("equal")
ax.set_xlim(10, 70)
ax.set_ylim(-50, -25)
adt.mask[:] = mask
g.display(ax, "adt")
a.display(ax, label="Anticyclonic", color="g", lw=1, extern_only=True)
pet hide pixel out eddies

Total running time of the script: ( 0 minutes 1.355 seconds)

Gallery generated by Sphinx-Gallery

Grid filtering in PET

How filter work in py eddy tracker. This implementation maybe doesn’t respect state art, but …

We code a specific filter in order to filter grid with same wavelength at each pixel.

from matplotlib import pyplot as plt
from numpy import arange

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset


def start_axes(title):
    fig = plt.figure(figsize=(13, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title)
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.95, 0.05, 0.01, 0.9]))

All information will be for regular grid

g = RegularGridDataset(
    data.get_path("dt_med_allsat_phy_l4_20160515_20190101.nc"), "longitude", "latitude"
)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/dt_med_allsat_phy_l4_20160515_20190101.nc

Kernel

Shape of kernel will increase in x, when latitude increase

fig = plt.figure(figsize=(12, 8))
for i, latitude in enumerate((15, 35, 55, 75)):
    k = g.kernel_bessel(latitude, 500, order=3).T
    ax0 = fig.add_subplot(
        2,
        2,
        i + 1,
        title=f"Kernel at {latitude}° of latitude\nfor 1/8° grid, shape : {k.shape}",
        aspect="equal",
    )
    m = ax0.pcolormesh(k, vmin=-0.5, vmax=2, cmap="viridis_r")
plt.colorbar(m, cax=fig.add_axes((0.92, 0.05, 0.01, 0.9)))
Kernel at 15° of latitude for 1/8° grid, shape : (215, 223), Kernel at 35° of latitude for 1/8° grid, shape : (215, 265), Kernel at 55° of latitude for 1/8° grid, shape : (215, 379), Kernel at 75° of latitude for 1/8° grid, shape : (215, 835)

Kernel along latitude

fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(
    111,
    ylabel="Kernel weight",
    xlabel="Latitude in °",
    title="Kernel in latitude, centered at 0° of latitude ",
)
k = g.kernel_bessel(0, 500, order=3)
k_lat = k[k.shape[0] // 2 + 1]
nb = k_lat.shape[0] // 2
ax.plot(
    arange(-nb * g.xstep, (nb + 0.5) * g.xstep, g.xstep), k_lat, label="Bessel kernel"
)

ax.legend()
ax.grid()
Kernel in latitude, centered at 0° of latitude

Kernel applying

Original grid

ax = start_axes("ADT")
m = g.display(ax, "adt", vmin=-0.15, vmax=0.15)
update_axes(ax, m)
ADT

We will select wavelength of 300 km

Low frequency

ax = start_axes("ADT low frequency")
g.copy("adt", "adt_low_300")
g.bessel_low_filter("adt_low_300", 300, order=3)
m = g.display(ax, "adt_low_300", vmin=-0.15, vmax=0.15)
update_axes(ax, m)
ADT low frequency

High frequency

ax = start_axes("ADT high frequency")
g.copy("adt", "adt_high_300")
g.bessel_high_filter("adt_high_300", 300, order=3)
m = g.display(ax, "adt_high_300", vmin=-0.15, vmax=0.15)
update_axes(ax, m)
ADT high frequency

Clues

wavelength : 80km

g.copy("adt", "adt_high_bessel")
g.bessel_high_filter("adt_high_bessel", 80, order=3)
g.copy("adt", "adt_low_bessel")
g.bessel_low_filter("adt_low_bessel", 80, order=3)

area = dict(llcrnrlon=11.75, urcrnrlon=21, llcrnrlat=33, urcrnrlat=36.75)

Spectrum

fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111)
ax.set_title("Spectrum")
ax.set_xlabel("km")

for label in ("adt_high_bessel", "adt_low_bessel", "adt"):
    lon_spec, lat_spec = g.spectrum_lonlat(label, area=area)
    mappable = ax.loglog(*lat_spec, label=f"lat {label}")[0]
    ax.loglog(
        *lon_spec, label=f"lon {label}", color=mappable.get_color(), linestyle="--"
    )

ax.set_xlim(10, 1000)
ax.set_ylim(1e-6, 1)
ax.set_xscale("log")
ax.legend()
ax.grid()
Spectrum

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 30, using nperseg = 30
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 74, using nperseg = 74
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 30, using nperseg = 30
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 74, using nperseg = 74
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 30, using nperseg = 30
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 74, using nperseg = 74
  .format(nperseg, input_length))

Spectrum ratio

fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111)
ax.set_title("Spectrum ratio")
ax.set_xlabel("km")

for label in ("adt_high_bessel", "adt_low_bessel"):
    lon_spec, lat_spec = g.spectrum_lonlat(label, area=area, ref=g, ref_grid_name="adt")
    mappable = ax.plot(*lat_spec, label=f"lat {label}")[0]
    ax.plot(*lon_spec, label=f"lon {label}", color=mappable.get_color(), linestyle="--")

ax.set_xlim(10, 1000)
ax.set_ylim(0, 1)
ax.set_xscale("log")
ax.legend()
ax.grid()
Spectrum ratio

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 30, using nperseg = 30
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 74, using nperseg = 74
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 30, using nperseg = 30
  .format(nperseg, input_length))
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/scipy/signal/spectral.py:1963: UserWarning: nperseg = 256 is greater than input length  = 74, using nperseg = 74
  .format(nperseg, input_length))

Old filter

To do …

Total running time of the script: ( 0 minutes 7.194 seconds)

Gallery generated by Sphinx-Gallery

Get Okubo Weis

\[OW = S_n^2 + S_s^2 + \omega^2\]

with normal strain (\(S_n\)), shear strain (\(S_s\)) and vorticity (\(\omega\))

\[S_n = \frac{\partial u}{\partial x} - \frac{\partial v}{\partial y}, S_s = \frac{\partial v}{\partial x} + \frac{\partial u}{\partial y}, \omega = \frac{\partial v}{\partial x} - \frac{\partial u}{\partial y}\]
from matplotlib import pyplot as plt
from numpy import arange, ma, where

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset
from py_eddy_tracker.observations.observation import EddiesObservations
def start_axes(title, zoom=False):
    fig = plt.figure(figsize=(12, 6))
    axes = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    axes.set_xlim(0, 360), axes.set_ylim(-80, 80)
    if zoom:
        axes.set_xlim(270, 340), axes.set_ylim(20, 50)
    axes.set_aspect("equal")
    axes.set_title(title)
    return axes


def update_axes(axes, mappable=None):
    axes.grid()
    if mappable:
        plt.colorbar(mappable, cax=axes.figure.add_axes([0.94, 0.05, 0.01, 0.9]))

Load detection files

a = EddiesObservations.load_file(data.get_path("Anticyclonic_20190223.nc"))
c = EddiesObservations.load_file(data.get_path("Cyclonic_20190223.nc"))

Load Input grid, ADT will be used to detect eddies

g = RegularGridDataset(
    data.get_path("nrt_global_allsat_phy_l4_20190223_20190226.nc"),
    "longitude",
    "latitude",
)

ax = start_axes("ADT (cm)")
m = g.display(ax, "adt", vmin=-120, vmax=120, factor=100)
update_axes(ax, m)
ADT (cm)

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/nrt_global_allsat_phy_l4_20190223_20190226.nc

Get parameter for ow

u_x = g.compute_stencil(g.grid("ugos"))
u_y = g.compute_stencil(g.grid("ugos"), vertical=True)
v_x = g.compute_stencil(g.grid("vgos"))
v_y = g.compute_stencil(g.grid("vgos"), vertical=True)
ow = g.vars["ow"] = (u_x - v_y) ** 2 + (v_x + u_y) ** 2 - (v_x - u_y) ** 2

ax = start_axes("Okubo weis")
m = g.display(ax, "ow", vmin=-1e-10, vmax=1e-10, cmap="bwr")
update_axes(ax, m)
Okubo weis

Gulf stream zoom

ax = start_axes("Okubo weis, Gulf stream", zoom=True)
m = g.display(ax, "ow", vmin=-1e-10, vmax=1e-10, cmap="bwr")
kw_ed = dict(intern_only=True, color="k", lw=1)
a.display(ax, **kw_ed), c.display(ax, **kw_ed)
update_axes(ax, m)
Okubo weis, Gulf stream

only negative OW

ax = start_axes("Okubo weis, Gulf stream", zoom=True)
threshold = ow.std() * -0.2
ow = ma.array(ow, mask=ow > threshold)
m = g.display(ax, ow, vmin=-1e-10, vmax=1e-10, cmap="bwr")
a.display(ax, **kw_ed), c.display(ax, **kw_ed)
update_axes(ax, m)
Okubo weis, Gulf stream

Get okubo-weiss mean/min/center in eddies

plt.figure(figsize=(8, 6))
ax = plt.subplot(111)
ax.set_xlabel("Okubo-Weiss parameter")
kw_hist = dict(bins=arange(-20e-10, 20e-10, 50e-12), histtype="step")
for method in ("mean", "center", "min"):
    kw_interp = dict(grid_object=g, varname="ow", method=method, intern=True)
    _, _, m = ax.hist(
        a.interp_grid(**kw_interp), label=f"Anticyclonic - OW {method}", **kw_hist
    )
    ax.hist(
        c.interp_grid(**kw_interp),
        label=f"Cyclonic - OW {method}",
        color=m[0].get_edgecolor(),
        ls="--",
        **kw_hist,
    )
ax.axvline(threshold, color="r")
ax.set_yscale("log")
ax.grid()
ax.set_ylim(1, 1e4)
ax.set_xlim(-15e-10, 15e-10)
ax.legend()
pet okubo weiss

Catch eddies with bad OW

ax = start_axes("Eddies with a min OW in speed contour over threshold")
ow_min = a.interp_grid(**kw_interp)
a_bad_ow = a.index(where(ow_min > threshold)[0])
a_bad_ow.display(ax, color="r", label="Anticyclonic")
ow_min = c.interp_grid(**kw_interp)
c_bad_ow = c.index(where(ow_min > threshold)[0])
c_bad_ow.display(ax, color="b", label="Cyclonic")
ax.legend()
Eddies with a min OW in speed contour over threshold

Display Radius and amplitude of eddies

fig = plt.figure(figsize=(12, 5))
fig.suptitle(
    "Parameter distribution (solid line) and cumulative distribution (dashed line)"
)
ax_amp, ax_rad = fig.add_subplot(121), fig.add_subplot(122)
ax_amp_c, ax_rad_c = ax_amp.twinx(), ax_rad.twinx()
ax_amp_c.set_ylim(0, 1), ax_rad_c.set_ylim(0, 1)
kw_a = dict(xname="amplitude", bins=arange(0, 2, 0.002).astype("f4"))
kw_r = dict(xname="radius_s", bins=arange(0, 500e6, 2e3).astype("f4"))
for d, label, color in (
    (a, "Anticyclonic all", "r"),
    (a_bad_ow, "Anticyclonic bad OW", "orange"),
    (c, "Cyclonic all", "blue"),
    (c_bad_ow, "Cyclonic bad OW", "lightblue"),
):
    x, y = d.bins_stat(**kw_a)
    ax_amp.plot(x * 100, y, label=label, color=color)
    ax_amp_c.plot(
        x * 100, y.cumsum() / y.sum(), label=label, color=color, ls="-.", lw=0.5
    )
    x, y = d.bins_stat(**kw_r)
    ax_rad.plot(x * 1e-3, y, label=label, color=color)
    ax_rad_c.plot(
        x * 1e-3, y.cumsum() / y.sum(), label=label, color=color, ls="-.", lw=0.5
    )

ax_amp.set_xlim(0, 12.5), ax_amp.grid(), ax_amp.set_ylim(0), ax_amp.legend()
ax_rad.set_xlim(0, 120), ax_rad.grid(), ax_rad.set_ylim(0)
ax_amp.set_xlabel("Amplitude (cm)"), ax_amp.set_ylabel("Nb eddies")
ax_rad.set_xlabel("Speed radius (km)")
Parameter distribution (solid line) and cumulative distribution (dashed line)

Total running time of the script: ( 0 minutes 7.616 seconds)

Gallery generated by Sphinx-Gallery

Tracking Manipulation

Track animation

Run in a terminal this script, which allow to watch eddy evolution

import py_eddy_tracker_sample

from py_eddy_tracker.appli.gui import Anim
from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load experimental atlas, and we select one eddy

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
# We get only 300 first step to save time of documentation builder
eddy = a.extract_ids([9672]).index(slice(0, 300))

Run animation Key shortcut :

  • Escape => exit

  • SpaceBar => pause

  • left arrow => t - 1

  • right arrow => t + 1

  • + => speed increase of 10 %

  • - => speed decrease of 10 %

a = Anim(eddy, sleep_event=1e-10, intern=True, figsize=(8, 3.5), cmap="viridis")
a.txt.set_position((17, 34.6))
a.ax.set_xlim(16.5, 23)
a.ax.set_ylim(34.5, 37)
a.show(infinity_loop=False)
23363 -> 23662

Total running time of the script: ( 0 minutes 6.810 seconds)

Gallery generated by Sphinx-Gallery

Display fields

import py_eddy_tracker_sample
from matplotlib import pyplot as plt

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental cyclonic atlas, we keep only eddies which are follow more than 180 days

c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
c = c.extract_with_length((180, -1))

Plot amplitude field

fig = plt.figure(figsize=(12, 6))
ax = fig.add_axes((0.05, 0.1, 0.9, 0.9))
ax.set_aspect("equal")
ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
m = c.scatter(ax, "amplitude", ref=-10, vmin=0, vmax=0.1)
ax.grid()

cb = plt.colorbar(
    m, cax=fig.add_axes([0.05, 0.07, 0.9, 0.01]), orientation="horizontal"
)
cb.set_label("Amplitude (m)")
pet display field

Total running time of the script: ( 0 minutes 2.271 seconds)

Gallery generated by Sphinx-Gallery

Track animation with standard matplotlib

Run in a terminal this script, which allow to watch eddy evolution

import py_eddy_tracker_sample
from matplotlib.animation import FuncAnimation
from numpy import arange

from py_eddy_tracker.appli.gui import Anim
from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load experimental atlas, and we select one eddy

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
eddy = a.extract_ids([9672])

Run animation

a = Anim(eddy, intern=True, figsize=(8, 3.5), cmap="magma_r", nb_step=6)
a.txt.set_position((17, 34.6))
a.ax.set_xlim(16.5, 23)
a.ax.set_ylim(34.5, 37)

# arguments to get full animation
# kwargs = dict(frames=arange(*a.period), interval=50)
# arguments to reduce compute cost for doucmentation, we display only every 10 days
kwargs = dict(frames=arange(*a.period)[200:800:10], save_count=60, interval=200)

ani = FuncAnimation(a.fig, a.func_animation, **kwargs)

Total running time of the script: ( 0 minutes 16.154 seconds)

Gallery generated by Sphinx-Gallery

Display Tracks

import py_eddy_tracker_sample
from matplotlib import pyplot as plt

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load experimental atlas

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
print(a)

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/observations/observation.py:220: RuntimeWarning: invalid value encountered in true_divide
  v = v.astype("f4") / v.sum() * 100
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/observations/observation.py:222: RuntimeWarning: invalid value encountered in true_divide
  v /= hist_numba(self[x], bins=bins)[0]
    | 473554 observations from 15706 to 25334 (9629 days, ~49 obs/day)
    |   Speed area      : 0.00 Mkm²/day
    |   Effective area  : 0.00 Mkm²/day
    ----Distribution in Amplitude:
    |   Amplitude bounds (cm)        0.00      1.00      2.00      3.00      4.00      5.00     10.00    500.00
    |   Percent of eddies         :      21.69     25.72     15.32      9.95      6.20     14.80      6.32
    ----Distribution in Radius:
    |   Speed radius (km)            0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
    |   Percent of eddies         :       0.85     48.58     36.33     10.97      2.59      0.64      0.03      0.00
    |   Effective radius (km)        0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
    |   Percent of eddies         :     100.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
    ----Distribution in Latitude
        Latitude bounds            -90.00    -60.00    -15.00     15.00     60.00     90.00
        Percent of eddies         :       0.00      0.00      0.00    100.00      0.00
        Percent of speed area     :        nan       nan       nan       nan       nan
        Percent of effective area :        nan       nan       nan       nan       nan
        Mean speed radius (km)    :        nan       nan       nan     32.28       nan
        Mean effective radius (km):        nan       nan       nan      0.00       nan
        Mean amplitude (cm)       :        nan       nan       nan      3.51       nan
    | 12224 tracks (38.74 obs/tracks, shorter 10 obs, longer 1183 obs)
    |   27246 filled observations (2.23 obs/tracks, 5.75 % of total)
    |   Intepolated speed area      : 0.00 Mkm²/day
    |   Intepolated effective area  : 0.00 Mkm²/day
    |   Distance by day             : Mean 3.68 , Median 2.53 km/day
    |   Distance by track           : Mean 139.01 , Median 77.28 km/track
    ----Distribution in lifetime:
    |   Lifetime (days  )            1.00     30.00     90.00    180.00    270.00    365.00   1000.00  10000.00
    |   Percent of tracks         :      65.03     26.87      5.72      1.20      0.51      0.65      0.03
    |   Percent of eddies         :      27.88     33.88     17.90      6.74      4.02      8.65      0.93

keep only eddies longer than 20 weeks, use -1 to have no upper limit

a = a.extract_with_length((7 * 20, -1))
c = c.extract_with_length((7 * 20, -1))
print(a)

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/observations/observation.py:220: RuntimeWarning: invalid value encountered in true_divide
  v = v.astype("f4") / v.sum() * 100
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/observations/observation.py:222: RuntimeWarning: invalid value encountered in true_divide
  v /= hist_numba(self[x], bins=bins)[0]
    | 124198 observations from 15706 to 25334 (9629 days, ~13 obs/day)
    |   Speed area      : 0.00 Mkm²/day
    |   Effective area  : 0.00 Mkm²/day
    ----Distribution in Amplitude:
    |   Amplitude bounds (cm)        0.00      1.00      2.00      3.00      4.00      5.00     10.00    500.00
    |   Percent of eddies         :       5.11      9.99     12.06     11.79      9.32     31.51     20.21
    ----Distribution in Radius:
    |   Speed radius (km)            0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
    |   Percent of eddies         :       0.19     22.20     46.88     22.96      6.31      1.39      0.07      0.00
    |   Effective radius (km)        0.00     15.00     30.00     45.00     60.00     75.00    100.00    200.00   2000.00
    |   Percent of eddies         :     100.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
    ----Distribution in Latitude
        Latitude bounds            -90.00    -60.00    -15.00     15.00     60.00     90.00
        Percent of eddies         :       0.00      0.00      0.00    100.00      0.00
        Percent of speed area     :        nan       nan       nan       nan       nan
        Percent of effective area :        nan       nan       nan       nan       nan
        Mean speed radius (km)    :        nan       nan       nan     40.03       nan
        Mean effective radius (km):        nan       nan       nan      0.00       nan
        Mean amplitude (cm)       :        nan       nan       nan      6.62       nan
    | 469 tracks (264.81 obs/tracks, shorter 140 obs, longer 1183 obs)
    |   3348 filled observations (7.14 obs/tracks, 2.70 % of total)
    |   Intepolated speed area      : 0.00 Mkm²/day
    |   Intepolated effective area  : 0.00 Mkm²/day
    |   Distance by day             : Mean 3.13 , Median 2.00 km/day
    |   Distance by track           : Mean 825.11 , Median 695.99 km/track
    ----Distribution in lifetime:
    |   Lifetime (days  )            1.00     30.00     90.00    180.00    270.00    365.00   1000.00  10000.00
    |   Percent of tracks         :       0.00      0.00     37.74     31.34     13.22     16.84      0.85
    |   Percent of eddies         :       0.00      0.00     22.44     25.72     15.34     32.97      3.53

Position filtering for nice display

a.position_filter(median_half_window=1, loess_half_window=5)
c.position_filter(median_half_window=1, loess_half_window=5)

Plot

fig = plt.figure(figsize=(12, 5))
ax = fig.add_axes((0.05, 0.1, 0.9, 0.9))
ax.set_aspect("equal")
ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
a.plot(ax, ref=-10, label="Anticyclonic ({nb_tracks} tracks)", color="r", lw=0.1)
c.plot(ax, ref=-10, label="Cyclonic ({nb_tracks} tracks)", color="b", lw=0.1)
ax.legend()
ax.grid()
pet display track

Total running time of the script: ( 0 minutes 6.547 seconds)

Gallery generated by Sphinx-Gallery

One Track

import py_eddy_tracker_sample
from matplotlib import pyplot as plt

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load experimental atlas, and we select one eddy

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
eddy = a.extract_ids([9672])
eddy_f = a.extract_ids([9672])
eddy_f.position_filter(median_half_window=1, loess_half_window=5)

plot

fig = plt.figure(figsize=(12, 5))
ax = fig.add_axes((0.05, 0.05, 0.9, 0.9))
ax.set_xlim(17.5, 22)
ax.set_ylim(35, 36.5)
ax.set_aspect("equal")
ax.grid()
eddy.plot(ax, color="r", lw=0.5)
eddy_f.scatter(ax, "n", cmap="jet", s=80)
pet one track
fig = plt.figure(figsize=(12, 5))
ax = fig.add_axes((0.05, 0.05, 0.9, 0.9))
ax.set_xlim(17, 23)
ax.set_ylim(34.5, 37)
ax.set_aspect("equal")
ax.grid()
eddy.plot(ax, color="r", lw=0.5, label="track")
eddy.index(range(0, len(eddy), 40)).display(
    ax, intern_only=True, label="observations every 40 days"
)
ax.legend()
pet one track

Total running time of the script: ( 0 minutes 1.384 seconds)

Gallery generated by Sphinx-Gallery

Tracks which go through area

import py_eddy_tracker_sample
from matplotlib import pyplot as plt

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load experimental atlas, we filter position to have nice display

c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
c.position_filter(median_half_window=1, loess_half_window=5)

We extract eddies in the area set below, but we ask to keep full_path

x0, x1, y0, y1 = 3, 4, 37, 38
area = dict(llcrnrlon=x0, llcrnrlat=y0, urcrnrlon=x1, urcrnrlat=y1)
c_subset = c.extract_with_area(area, full_path=True)

Plot

fig = plt.figure(figsize=(12, 5))
ax = fig.add_axes((0.05, 0.05, 0.9, 0.9))
ax.set_xlim(-1, 9)
ax.set_ylim(36, 40)
ax.set_aspect("equal")
ax.grid()
c.plot(ax, color="gray", lw=0.1, ref=-10, label="All tracks ({nb_tracks} tracks)")
c_subset.plot(
    ax, color="red", lw=0.2, ref=-10, label="selected tracks ({nb_tracks} tracks)"
)
ax.plot(
    (x0, x0, x1, x1, x0),
    (y0, y1, y1, y0, y0),
    color="green",
    lw=1.5,
    label="Box of selection",
)
ax.legend()
pet select track across area

Total running time of the script: ( 0 minutes 2.784 seconds)

Gallery generated by Sphinx-Gallery

Track in python

This example didn’t replace EddyTracking, we remove check that application do and also postprocessing step.

from py_eddy_tracker.data import get_remote_sample
from py_eddy_tracker.featured_tracking.area_tracker import AreaTracker
from py_eddy_tracker.gui import GUI
from py_eddy_tracker.tracking import Correspondances

Get remote data, we will keep only 180 first days, get_remote_sample function is only to get demo dataset, in your own case give a list of identification filename and don’t mix cyclonic and anticyclonic files.

file_objects = get_remote_sample(
    "eddies_med_adt_allsat_dt2018/Anticyclonic_2010_2011_2012"
)[:180]

We run a traking with a tracker which use contour overlap

c = Correspondances(datasets=file_objects, class_method=AreaTracker, virtual=3)
c.track()
c.prepare_merging()
# We have now an eddy object
eddies_area_tracker = c.merge(raw_data=False)
eddies_area_tracker.virtual[:] = eddies_area_tracker.time == 0
eddies_area_tracker.filled_by_interpolation(eddies_area_tracker.virtual == 1)

We run a traking with default tracker

c = Correspondances(datasets=file_objects, virtual=3)
c.track()
c.prepare_merging()
eddies_default_tracker = c.merge(raw_data=False)
eddies_default_tracker.virtual[:] = eddies_default_tracker.time == 0
eddies_default_tracker.filled_by_interpolation(eddies_default_tracker.virtual == 1)

Out:

High number of conflict : 56 (nb_conflict)
High number of conflict : 46 (nb_conflict)
High number of conflict : 49 (nb_conflict)
High number of conflict : 50 (nb_conflict)
High number of conflict : 58 (nb_conflict)
High number of conflict : 62 (nb_conflict)
High number of conflict : 67 (nb_conflict)
High number of conflict : 67 (nb_conflict)
High number of conflict : 51 (nb_conflict)
High number of conflict : 50 (nb_conflict)
High number of conflict : 54 (nb_conflict)
High number of conflict : 60 (nb_conflict)
High number of conflict : 59 (nb_conflict)
High number of conflict : 61 (nb_conflict)
High number of conflict : 68 (nb_conflict)
High number of conflict : 74 (nb_conflict)
High number of conflict : 64 (nb_conflict)
High number of conflict : 71 (nb_conflict)
High number of conflict : 67 (nb_conflict)
High number of conflict : 67 (nb_conflict)
High number of conflict : 71 (nb_conflict)
High number of conflict : 78 (nb_conflict)
High number of conflict : 76 (nb_conflict)
High number of conflict : 78 (nb_conflict)
High number of conflict : 71 (nb_conflict)
High number of conflict : 66 (nb_conflict)
High number of conflict : 55 (nb_conflict)
High number of conflict : 60 (nb_conflict)
High number of conflict : 59 (nb_conflict)
High number of conflict : 67 (nb_conflict)
High number of conflict : 57 (nb_conflict)
High number of conflict : 57 (nb_conflict)
High number of conflict : 37 (nb_conflict)
High number of conflict : 72 (nb_conflict)
High number of conflict : 75 (nb_conflict)
High number of conflict : 73 (nb_conflict)
High number of conflict : 77 (nb_conflict)
High number of conflict : 90 (nb_conflict)
High number of conflict : 89 (nb_conflict)
High number of conflict : 88 (nb_conflict)
High number of conflict : 95 (nb_conflict)
High number of conflict : 87 (nb_conflict)
High number of conflict : 78 (nb_conflict)
High number of conflict : 73 (nb_conflict)
High number of conflict : 83 (nb_conflict)
High number of conflict : 86 (nb_conflict)
High number of conflict : 89 (nb_conflict)
High number of conflict : 72 (nb_conflict)
High number of conflict : 65 (nb_conflict)
High number of conflict : 48 (nb_conflict)
High number of conflict : 83 (nb_conflict)
High number of conflict : 81 (nb_conflict)
High number of conflict : 77 (nb_conflict)
High number of conflict : 89 (nb_conflict)
High number of conflict : 84 (nb_conflict)
High number of conflict : 89 (nb_conflict)
High number of conflict : 99 (nb_conflict)
High number of conflict : 79 (nb_conflict)
High number of conflict : 79 (nb_conflict)
High number of conflict : 75 (nb_conflict)
High number of conflict : 75 (nb_conflict)
High number of conflict : 65 (nb_conflict)
High number of conflict : 80 (nb_conflict)
High number of conflict : 50 (nb_conflict)
High number of conflict : 73 (nb_conflict)
High number of conflict : 73 (nb_conflict)
High number of conflict : 72 (nb_conflict)
High number of conflict : 75 (nb_conflict)
High number of conflict : 62 (nb_conflict)
High number of conflict : 53 (nb_conflict)
High number of conflict : 45 (nb_conflict)
High number of conflict : 49 (nb_conflict)
High number of conflict : 41 (nb_conflict)
High number of conflict : 50 (nb_conflict)
High number of conflict : 46 (nb_conflict)
High number of conflict : 37 (nb_conflict)
High number of conflict : 52 (nb_conflict)

Start GUI to compare tracking

g = GUI(
    Acyc_area_tracker=eddies_area_tracker, Acyc_default_tracker=eddies_default_tracker
)
g.now = 22000
g.bbox = 0, 9, 36, 40
g.adjust()
g.show()
pet run a tracking

Start GUI with area tracker

g = GUI(Acyc_area_tracker=eddies_area_tracker)
g.now = 22000
g.bbox = 0, 9, 36, 40
g.adjust()
g.show()
pet run a tracking

Start GUI with default one

g = GUI(Acyc_default_tracker=eddies_default_tracker)
g.now = 22000
g.bbox = 0, 9, 36, 40
g.adjust()
g.show()
pet run a tracking

Total running time of the script: ( 0 minutes 39.290 seconds)

Gallery generated by Sphinx-Gallery

Tracking diagnostics

Geographical statistics

import py_eddy_tracker_sample
from matplotlib import pyplot as plt

from py_eddy_tracker.observations.tracking import TrackEddiesObservations


def start_axes(title):
    fig = plt.figure(figsize=(13.5, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title)
    return ax

Load an experimental med atlas over a period of 26 years (1993-2019), we merge the 2 datasets

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
a = a.merge(c)

step = 0.1

Mean of amplitude in each box

ax = start_axes("Amplitude mean by box of %s°" % step)
g = a.grid_stat(((-7, 37, step), (30, 46, step)), "amplitude")
m = g.display(ax, name="amplitude", vmin=0, vmax=10, factor=100)
ax.grid()
cb = plt.colorbar(m, cax=ax.figure.add_axes([0.94, 0.05, 0.01, 0.9]))
cb.set_label("Amplitude (cm)")
Amplitude mean by box of 0.1°

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/dataset/grid.py:1896: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  return ax.pcolormesh(x, self.y_bounds, data.T * factor, **kwargs)

Mean of speed radius in each box

ax = start_axes("Speed radius mean by box of %s°" % step)
g = a.grid_stat(((-7, 37, step), (30, 46, step)), "radius_s")
m = g.display(ax, name="radius_s", vmin=10, vmax=50, factor=0.001)
ax.grid()
cb = plt.colorbar(m, cax=ax.figure.add_axes([0.94, 0.05, 0.01, 0.9]))
cb.set_label("Speed radius (km)")
Speed radius mean by box of 0.1°

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/dataset/grid.py:1896: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  return ax.pcolormesh(x, self.y_bounds, data.T * factor, **kwargs)

Percent of virtual on the whole obs in each box

ax = start_axes("Percent of virtual by box of %s°" % step)
g = a.grid_stat(((-7, 37, step), (30, 46, step)), "virtual")
g.vars["virtual"] *= 100
m = g.display(ax, name="virtual", vmin=0, vmax=15)
ax.grid()
cb = plt.colorbar(m, cax=ax.figure.add_axes([0.94, 0.05, 0.01, 0.9]))
cb.set_label("Percent of virtual (%)")
Percent of virtual by box of 0.1°

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/dataset/grid.py:1896: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  return ax.pcolormesh(x, self.y_bounds, data.T * factor, **kwargs)

Total running time of the script: ( 0 minutes 4.987 seconds)

Gallery generated by Sphinx-Gallery

Birth and death

Following figures are based on https://doi.org/10.1016/j.pocean.2011.01.002

import py_eddy_tracker_sample
from matplotlib import pyplot as plt

from py_eddy_tracker.observations.tracking import TrackEddiesObservations
def start_axes(title):
    fig = plt.figure(figsize=(13, 5))
    ax = fig.add_axes([0.03, 0.03, 0.90, 0.94])
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.set_aspect("equal")
    ax.set_title(title)
    return ax


def update_axes(ax, mappable=None):
    ax.grid()
    if mappable:
        plt.colorbar(mappable, cax=ax.figure.add_axes([0.95, 0.05, 0.01, 0.9]))

Load an experimental med atlas over a period of 26 years (1993-2019)

kwargs_load = dict(
    include_vars=(
        "longitude",
        "latitude",
        "observation_number",
        "track",
        "time",
        "speed_contour_longitude",
        "speed_contour_latitude",
    )
)
a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
t0, t1 = a.period
step = 0.125
bins = ((-10, 37, step), (30, 46, step))
kwargs = dict(cmap="terrain_r", factor=100 / (t1 - t0), name="count", vmin=0, vmax=1)

Cyclonic

ax = start_axes("Birth cyclonic frenquency (%)")
g_c_first = c.first_obs().grid_count(bins, intern=True)
m = g_c_first.display(ax, **kwargs)
update_axes(ax, m)
Birth cyclonic frenquency (%)
ax = start_axes("Death cyclonic frenquency (%)")
g_c_last = c.last_obs().grid_count(bins, intern=True)
m = g_c_last.display(ax, **kwargs)
update_axes(ax, m)
Death cyclonic frenquency (%)

Anticyclonic

ax = start_axes("Birth anticyclonic frequency (%)")
g_a_first = a.first_obs().grid_count(bins, intern=True)
m = g_a_first.display(ax, **kwargs)
update_axes(ax, m)
Birth anticyclonic frequency (%)
ax = start_axes("Death anticyclonic frequency (%)")
g_a_last = a.last_obs().grid_count(bins, intern=True)
m = g_a_last.display(ax, **kwargs)
update_axes(ax, m)
Death anticyclonic frequency (%)

Total running time of the script: ( 0 minutes 5.135 seconds)

Gallery generated by Sphinx-Gallery

Lifetime Histogram

import py_eddy_tracker_sample
from matplotlib import pyplot as plt
from numpy import arange, ones

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental med atlas over a period of 26 years (1993-2019)

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
nb_year = (a.period[1] - a.period[0] + 1) / 365.25

Setup axes

figure = plt.figure(figsize=(12, 8))
ax_ratio_cum = figure.add_axes([0.55, 0.06, 0.42, 0.34])
ax_ratio = figure.add_axes([0.07, 0.06, 0.46, 0.34])
ax_cum = figure.add_axes([0.55, 0.43, 0.42, 0.54])
ax = figure.add_axes([0.07, 0.43, 0.46, 0.54])
ax.set_ylabel("Eddies by year")
ax_ratio.set_ylabel("Ratio Cyclonic/Anticyclonic")
for ax_ in (ax, ax_cum, ax_ratio_cum, ax_ratio):
    ax_.set_xlim(0, 400)
    if ax_ in (ax, ax_cum):
        ax_.set_ylim(1e-1, 1e4), ax_.set_yscale("log")
    else:
        ax_.set_xlabel("Lifetime in days (by week bins)")
        ax_.set_ylim(0, 2)
        ax_.axhline(1, color="g", lw=2)
    ax_.grid()
ax_cum.xaxis.set_ticklabels([]), ax_cum.yaxis.set_ticklabels([])
ax.xaxis.set_ticklabels([]), ax_ratio_cum.yaxis.set_ticklabels([])

# plot data
bin_hist = arange(7, 2000, 7)
x = (bin_hist[1:] + bin_hist[:-1]) / 2.0
a_nb, c_nb = a.nb_obs_by_track, c.nb_obs_by_track
a_nb, c_nb = a_nb[a_nb != 0], c_nb[c_nb != 0]
w_a, w_c = ones(a_nb.shape) / nb_year, ones(c_nb.shape) / nb_year
kwargs_a = dict(histtype="step", bins=bin_hist, x=a_nb, color="r", weights=w_a)
kwargs_c = dict(histtype="step", bins=bin_hist, x=c_nb, color="b", weights=w_c)
cum_a, _, _ = ax_cum.hist(cumulative=-1, **kwargs_a)
cum_c, _, _ = ax_cum.hist(cumulative=-1, **kwargs_c)
nb_a, _, _ = ax.hist(label="Anticyclonic", **kwargs_a)
nb_c, _, _ = ax.hist(label="Cyclonic", **kwargs_c)
ax_ratio_cum.plot(x, cum_c / cum_a)
ax_ratio.plot(x, nb_c / nb_a)
ax.legend()
pet lifetime

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_lifetime.py:55: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio_cum.plot(x, cum_c / cum_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_lifetime.py:56: RuntimeWarning: divide by zero encountered in true_divide
  ax_ratio.plot(x, nb_c / nb_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_lifetime.py:56: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio.plot(x, nb_c / nb_a)

Total running time of the script: ( 0 minutes 3.156 seconds)

Gallery generated by Sphinx-Gallery

Parameter Histogram

import py_eddy_tracker_sample
from matplotlib import pyplot as plt
from numpy import arange

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental med atlas over a period of 26 years (1993-2019)

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
kwargs_a = dict(label="Anticyclonic", color="r", histtype="step", density=True)
kwargs_c = dict(label="Cyclonic", color="b", histtype="step", density=True)

Plot

fig = plt.figure(figsize=(12, 7))

for x0, name, title, xmax, factor, bins in zip(
    (0.4, 0.72, 0.08),
    ("speed_radius", "speed_average", "amplitude"),
    ("Speed radius (km)", "Speed average (cm/s)", "Amplitude (cm)"),
    (100, 50, 20),
    (0.001, 100, 100),
    (arange(0, 2000, 1), arange(0, 1000, 0.5), arange(0.0005, 1000, 0.2)),
):
    ax_hist = fig.add_axes((x0, 0.24, 0.27, 0.35))
    nb_a, _, _ = ax_hist.hist(a[name] * factor, bins=bins, **kwargs_a)
    nb_c, _, _ = ax_hist.hist(c[name] * factor, bins=bins, **kwargs_c)
    ax_hist.set_xticklabels([])
    ax_hist.set_xlim(0, xmax)
    ax_hist.grid()

    ax_cum = fig.add_axes((x0, 0.62, 0.27, 0.35))
    ax_cum.hist(a[name] * factor, bins=bins, cumulative=-1, **kwargs_a)
    ax_cum.hist(c[name] * factor, bins=bins, cumulative=-1, **kwargs_c)
    ax_cum.set_xticklabels([])
    ax_cum.set_title(title)
    ax_cum.set_xlim(0, xmax)
    ax_cum.set_ylim(0, 1)
    ax_cum.grid()

    ax_ratio = fig.add_axes((x0, 0.06, 0.27, 0.15))
    ax_ratio.set_xlim(0, xmax)
    ax_ratio.set_ylim(0, 2)
    ax_ratio.plot((bins[1:] + bins[:-1]) / 2, nb_c / nb_a)
    ax_ratio.axhline(1, color="k")
    ax_ratio.grid()
    ax_ratio.set_xlabel(title)

ax_cum.set_ylabel("Cumulative\npercent distribution")
ax_hist.set_ylabel("Percent of observations")
ax_ratio.set_ylabel("Ratio percent\nCyc/Acyc")
ax_cum.legend()
Speed radius (km), Speed average (cm/s), Amplitude (cm)

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_histo.py:54: RuntimeWarning: divide by zero encountered in true_divide
  ax_ratio.plot((bins[1:] + bins[:-1]) / 2, nb_c / nb_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_histo.py:54: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio.plot((bins[1:] + bins[:-1]) / 2, nb_c / nb_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_histo.py:54: RuntimeWarning: divide by zero encountered in true_divide
  ax_ratio.plot((bins[1:] + bins[:-1]) / 2, nb_c / nb_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_histo.py:54: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio.plot((bins[1:] + bins[:-1]) / 2, nb_c / nb_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_histo.py:54: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio.plot((bins[1:] + bins[:-1]) / 2, nb_c / nb_a)

Total running time of the script: ( 0 minutes 3.634 seconds)

Gallery generated by Sphinx-Gallery

Groups distribution

import py_eddy_tracker_sample
from matplotlib import pyplot as plt
from numpy import arange, ones, percentile

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental med atlas over a period of 26 years (1993-2019)

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)

Group distribution

groups = dict()
bins_time = [10, 20, 30, 60, 90, 180, 360, 100000]
for t0, t1 in zip(bins_time[:-1], bins_time[1:]):
    groups[f"lifetime_{t0}_{t1}"] = lambda dataset, t0=t0, t1=t1: (
        dataset.lifetime >= t0
    ) * (dataset.lifetime < t1)
bins_percentile = arange(0, 100.0001, 5)

Function to build stats

def stats_compilation(dataset, groups, field, bins, filter=None):
    datas = dict(ref=dataset.bins_stat(field, bins=bins, mask=filter)[1], y=dict())
    for k, index in groups.items():
        i = dataset.merge_filters(filter, index)
        x, datas["y"][k] = dataset.bins_stat(field, bins=bins, mask=i)
    datas["x"], datas["bins"] = x, bins
    return datas


def plot_stats(ax, bins, x, y, ref, box=False, cmap=None, percentiles=None, **kw):
    base, ref = ones(x.shape) * 100.0, ref / 100.0
    x = arange(bins.shape[0]).repeat(2)[1:-1] if box else x
    y0 = base
    if cmap is not None:
        cmap, nb_groups = plt.get_cmap(cmap), len(y)
    keys = tuple(y.keys())
    for i, k in enumerate(keys[::-1]):
        y1 = y0 - y[k] / ref
        args = (y0.repeat(2), y1.repeat(2)) if box else (y0, y1)
        if cmap is not None:
            kw["color"] = cmap(1 - i / (nb_groups - 1))
        ax.fill_between(x, *args, label=k, **kw)
        y0 = y1
    if percentiles:
        for b in bins:
            ax.axvline(b, **percentiles)

Speed radius by track period

stats = stats_compilation(
    a, groups, "radius_s", percentile(a.radius_s, bins_percentile)
)
fig = plt.figure()
ax = fig.add_subplot(111)
plot_stats(ax, **stats, cmap="magma", percentiles=dict(color="gray", ls="-.", lw=0.4))
ax.set_xlabel("Speed radius (m)"), ax.set_ylabel("% of class"), ax.set_ylim(0, 100)
ax.grid(), ax.legend()
pet groups

Amplitude by track period

stats = stats_compilation(
    a, groups, "amplitude", percentile(a.amplitude, bins_percentile)
)
fig = plt.figure()
ax = fig.add_subplot(111)
plot_stats(ax, **stats, cmap="magma")
ax.set_xlabel("Amplitude (m)"), ax.set_ylabel("% of class"), ax.set_ylim(0, 100)
ax.grid(), ax.legend()
pet groups

Total running time of the script: ( 0 minutes 2.031 seconds)

Gallery generated by Sphinx-Gallery

Propagation Histogram

import py_eddy_tracker_sample
from matplotlib import pyplot as plt
from numpy import arange, ones

from py_eddy_tracker.generic import cumsum_by_track
from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental med atlas over a period of 26 years (1993-2019)

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)
nb_year = (a.period[1] - a.period[0] + 1) / 365.25

Filtering position to remove noisy position

a.position_filter(median_half_window=1, loess_half_window=5)
c.position_filter(median_half_window=1, loess_half_window=5)

Compute curvilign distance

i0, nb = a.index_from_track, a.nb_obs_by_track
d_a = cumsum_by_track(a.distance_to_next(), a.tracks)[(i0 - 1 + nb)[nb != 0]] / 1000.0
i0, nb = c.index_from_track, c.nb_obs_by_track
d_c = cumsum_by_track(c.distance_to_next(), c.tracks)[(i0 - 1 + nb)[nb != 0]] / 1000.0

Setup axes

figure = plt.figure(figsize=(12, 8))
ax_ratio_cum = figure.add_axes([0.55, 0.06, 0.42, 0.34])
ax_ratio = figure.add_axes([0.07, 0.06, 0.46, 0.34])
ax_cum = figure.add_axes([0.55, 0.43, 0.42, 0.54])
ax = figure.add_axes([0.07, 0.43, 0.46, 0.54])
ax.set_ylabel("Eddies by year")
ax_ratio.set_ylabel("Ratio Cyclonic/Anticyclonic")
for ax_ in (ax, ax_cum, ax_ratio_cum, ax_ratio):
    ax_.set_xlim(0, 1000)
    if ax_ in (ax, ax_cum):
        ax_.set_ylim(1e-1, 1e4), ax_.set_yscale("log")
    else:
        ax_.set_xlabel("Propagation in km (with bins of 20 km)")
        ax_.set_ylim(0, 2)
        ax_.axhline(1, color="g", lw=2)
    ax_.grid()
ax_cum.xaxis.set_ticklabels([]), ax_cum.yaxis.set_ticklabels([])
ax.xaxis.set_ticklabels([]), ax_ratio_cum.yaxis.set_ticklabels([])

# plot data
bin_hist = arange(0, 2000, 20)
x = (bin_hist[1:] + bin_hist[:-1]) / 2.0
w_a, w_c = ones(d_a.shape) / nb_year, ones(d_c.shape) / nb_year
kwargs_a = dict(histtype="step", bins=bin_hist, x=d_a, color="r", weights=w_a)
kwargs_c = dict(histtype="step", bins=bin_hist, x=d_c, color="b", weights=w_c)
cum_a, _, _ = ax_cum.hist(cumulative=-1, **kwargs_a)
cum_c, _, _ = ax_cum.hist(cumulative=-1, **kwargs_c)
nb_a, _, _ = ax.hist(label="Anticyclonic", **kwargs_a)
nb_c, _, _ = ax.hist(label="Cyclonic", **kwargs_c)
ax_ratio_cum.plot(x, cum_c / cum_a)
ax_ratio.plot(x, nb_c / nb_a)
ax.legend()
pet propagation

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_propagation.py:66: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio_cum.plot(x, cum_c / cum_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_propagation.py:67: RuntimeWarning: divide by zero encountered in true_divide
  ax_ratio.plot(x, nb_c / nb_a)
/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/checkouts/v3.3.0/examples/10_tracking_diagnostics/pet_propagation.py:67: RuntimeWarning: invalid value encountered in true_divide
  ax_ratio.plot(x, nb_c / nb_a)

Total running time of the script: ( 0 minutes 4.234 seconds)

Gallery generated by Sphinx-Gallery

Count pixel used

Do Geo stat with frequency and compare with center count method: Count center

import py_eddy_tracker_sample
from matplotlib import pyplot as plt
from matplotlib.colors import LogNorm

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental med atlas over a period of 26 years (1993-2019)

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)

Parameters

step = 0.125
bins = ((-10, 37, step), (30, 46, step))
kwargs_pcolormesh = dict(
    cmap="terrain_r", vmin=0, vmax=0.75, factor=1 / a.nb_days, name="count"
)

Plot

fig = plt.figure(figsize=(12, 18.5))
ax_a = fig.add_axes([0.03, 0.75, 0.90, 0.25])
ax_a.set_title("Anticyclonic frequency")
ax_c = fig.add_axes([0.03, 0.5, 0.90, 0.25])
ax_c.set_title("Cyclonic frequency")
ax_all = fig.add_axes([0.03, 0.25, 0.90, 0.25])
ax_all.set_title("All eddies frequency")
ax_ratio = fig.add_axes([0.03, 0.0, 0.90, 0.25])
ax_ratio.set_title("Ratio cyclonic / Anticyclonic")

# Count pixel used for each contour
g_a = a.grid_count(bins, intern=True)
g_a.display(ax_a, **kwargs_pcolormesh)
g_c = c.grid_count(bins, intern=True)
g_c.display(ax_c, **kwargs_pcolormesh)
# Compute a ratio Cyclonic / Anticyclonic
ratio = g_c.vars["count"] / g_a.vars["count"]

# Mask manipulation to be able to sum the 2 grids
m_c = g_c.vars["count"].mask
m = m_c & g_a.vars["count"].mask
g_c.vars["count"][m_c] = 0
g_c.vars["count"] += g_a.vars["count"]
g_c.vars["count"].mask = m

m = g_c.display(ax_all, **kwargs_pcolormesh)
plt.colorbar(m, cax=fig.add_axes([0.95, 0.27, 0.01, 0.7]))

g_c.vars["count"] = ratio
m = g_c.display(
    ax_ratio, name="count", vmin=0.1, vmax=10, norm=LogNorm(), cmap="coolwarm_r"
)
plt.colorbar(m, cax=fig.add_axes([0.95, 0.02, 0.01, 0.2]))

for ax in (ax_a, ax_c, ax_all, ax_ratio):
    ax.set_aspect("equal")
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.grid()
Anticyclonic frequency, Cyclonic frequency, All eddies frequency, Ratio cyclonic / Anticyclonic

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/dataset/grid.py:1896: MatplotlibDeprecationWarning: Passing parameters norm and vmin/vmax simultaneously is deprecated since 3.3 and will become an error two minor releases later. Please pass vmin/vmax directly to the norm when creating it.
  return ax.pcolormesh(x, self.y_bounds, data.T * factor, **kwargs)

Count Anticyclones as a function of lifetime

fig = plt.figure(figsize=(12, 10))
mask = a.lifetime >= 60
ax_long = fig.add_axes([0.03, 0.53, 0.90, 0.45])
g_a = a.grid_count(bins, intern=True, filter=mask)
g_a.display(ax_long, **kwargs_pcolormesh)
ax_long.set_title(f"Anticyclones with lifetime >= 60 days ({mask.sum()} Obs)")
ax_short = fig.add_axes([0.03, 0.03, 0.90, 0.45])
g_a = a.grid_count(bins, intern=True, filter=~mask)
m = g_a.display(ax_short, **kwargs_pcolormesh)
ax_short.set_title(f"Anticyclones with lifetime < 60 days ({(~mask).sum()} Obs)")
for ax in (ax_short, ax_long):
    ax.set_aspect("equal"), ax.grid()
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
cb = plt.colorbar(m, cax=fig.add_axes([0.94, 0.05, 0.015, 0.9]))
Anticyclones with lifetime >= 60 days (238825 Obs), Anticyclones with lifetime < 60 days (234729 Obs)

Total running time of the script: ( 0 minutes 27.104 seconds)

Gallery generated by Sphinx-Gallery

Count center

Do Geo stat with center and compare with frequency method show: Count pixel used

import py_eddy_tracker_sample
from matplotlib import pyplot as plt
from matplotlib.colors import LogNorm

from py_eddy_tracker.observations.tracking import TrackEddiesObservations

Load an experimental med atlas over a period of 26 years (1993-2019)

a = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Anticyclonic.zarr")
)
c = TrackEddiesObservations.load_file(
    py_eddy_tracker_sample.get_path("eddies_med_adt_allsat_dt2018/Cyclonic.zarr")
)

Parameters

step = 0.125
bins = ((-10, 37, step), (30, 46, step))
kwargs_pcolormesh = dict(
    cmap="terrain_r", vmin=0, vmax=2, factor=1 / (a.nb_days * step ** 2), name="count"
)

Plot

fig = plt.figure(figsize=(12, 18.5))
ax_a = fig.add_axes([0.03, 0.75, 0.90, 0.25])
ax_a.set_title("Anticyclonic center frequency")
ax_c = fig.add_axes([0.03, 0.5, 0.90, 0.25])
ax_c.set_title("Cyclonic center frequency")
ax_all = fig.add_axes([0.03, 0.25, 0.90, 0.25])
ax_all.set_title("All eddies center frequency")
ax_ratio = fig.add_axes([0.03, 0.0, 0.90, 0.25])
ax_ratio.set_title("Ratio cyclonic / Anticyclonic")

# Count pixel used for each center
g_a = a.grid_count(bins, intern=True, center=True)
g_a.display(ax_a, **kwargs_pcolormesh)
g_c = c.grid_count(bins, intern=True, center=True)
g_c.display(ax_c, **kwargs_pcolormesh)
# Compute a ratio Cyclonic / Anticyclonic
ratio = g_c.vars["count"] / g_a.vars["count"]

# Mask manipulation to be able to sum the 2 grids
m_c = g_c.vars["count"].mask
m = m_c & g_a.vars["count"].mask
g_c.vars["count"][m_c] = 0
g_c.vars["count"] += g_a.vars["count"]
g_c.vars["count"].mask = m

m = g_c.display(ax_all, **kwargs_pcolormesh)
cb = plt.colorbar(m, cax=fig.add_axes([0.94, 0.27, 0.01, 0.7]))
cb.set_label("Eddies by 1°^2 by day")

g_c.vars["count"] = ratio
m = g_c.display(
    ax_ratio, name="count", vmin=0.1, vmax=10, norm=LogNorm(), cmap="coolwarm_r"
)
plt.colorbar(m, cax=fig.add_axes([0.94, 0.02, 0.01, 0.2]))

for ax in (ax_a, ax_c, ax_all, ax_ratio):
    ax.set_aspect("equal")
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
    ax.grid()
Anticyclonic center frequency, Cyclonic center frequency, All eddies center frequency, Ratio cyclonic / Anticyclonic

Out:

/home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/dataset/grid.py:1896: MatplotlibDeprecationWarning: Passing parameters norm and vmin/vmax simultaneously is deprecated since 3.3 and will become an error two minor releases later. Please pass vmin/vmax directly to the norm when creating it.
  return ax.pcolormesh(x, self.y_bounds, data.T * factor, **kwargs)

Count Anticyclones as a function of lifetime

Count at the center’s position

fig = plt.figure(figsize=(12, 10))
mask = a.lifetime >= 60
ax_long = fig.add_axes([0.03, 0.53, 0.90, 0.45])
g_a = a.grid_count(bins, center=True, filter=mask)
g_a.display(ax_long, **kwargs_pcolormesh)
ax_long.set_title(f"Anticyclones with lifetime >= 60 days ({mask.sum()} Obs)")
ax_short = fig.add_axes([0.03, 0.03, 0.90, 0.45])
g_a = a.grid_count(bins, center=True, filter=~mask)
m = g_a.display(ax_short, **kwargs_pcolormesh)
ax_short.set_title(f"Anticyclones with lifetime < 60 days ({(~mask).sum()} Obs)")
for ax in (ax_short, ax_long):
    ax.set_aspect("equal"), ax.grid()
    ax.set_xlim(-6, 36.5), ax.set_ylim(30, 46)
cb = plt.colorbar(m, cax=fig.add_axes([0.94, 0.05, 0.015, 0.9]))
Anticyclones with lifetime >= 60 days (238825 Obs), Anticyclones with lifetime < 60 days (234729 Obs)

Total running time of the script: ( 0 minutes 3.789 seconds)

Gallery generated by Sphinx-Gallery

External data

Collocating external data

Script will use py-eddy-tracker methods to upload external data (sea surface temperature, SST) in a common structure with altimetry.

Figures higlights the different steps.

from datetime import datetime

from matplotlib import pyplot as plt

from py_eddy_tracker import data
from py_eddy_tracker.dataset.grid import RegularGridDataset

date = datetime(2016, 7, 7)

filename_alt = data.get_path(f"dt_blacksea_allsat_phy_l4_{date:%Y%m%d}_20200801.nc")
filename_sst = data.get_path(
    f"{date:%Y%m%d}000000-GOS-L4_GHRSST-SSTfnd-OISST_HR_REP-BLK-v02.0-fv01.0.nc"
)
var_name_sst = "analysed_sst"

extent = [27, 42, 40.5, 47]

Loading data

sst = RegularGridDataset(filename=filename_sst, x_name="lon", y_name="lat")
alti = RegularGridDataset(
    data.get_path(filename_alt), x_name="longitude", y_name="latitude"
)
# We can use `Grid` tools to interpolate ADT on the sst grid
sst.regrid(alti, "sla")
sst.add_uv("sla")

Out:

We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/20160707000000-GOS-L4_GHRSST-SSTfnd-OISST_HR_REP-BLK-v02.0-fv01.0.nc
We assume pixel position of grid is center for /home/docs/checkouts/readthedocs.org/user_builds/py-eddy-tracker/envs/v3.3.0/lib/python3.7/site-packages/pyEddyTracker-3.3.0-py3.7.egg/py_eddy_tracker/data/dt_blacksea_allsat_phy_l4_20160707_20200801.nc

Functions to initiate figure axes

def start_axes(title, extent=extent):
    fig = plt.figure(figsize=(13, 6), dpi=120)
    ax = fig.add_axes([0.03, 0.05, 0.89, 0.91])
    ax.set_xlim(extent[0], extent[1])
    ax.set_ylim(extent[2], extent[3])
    ax.set_title(title)
    ax.set_aspect("equal")
    return ax


def update_axes(ax, mappable=None, unit=""):
    ax.grid()
    if mappable:
        cax = ax.figure.add_axes([0.93, 0.05, 0.01, 0.9], title=unit)
        plt.colorbar(mappable, cax=cax)

ADT first display

ax = start_axes("SLA", extent=extent)
m = sst.display(ax, "sla", vmin=0.05, vmax=0.35)
update_axes(ax, m, unit="[m]")
SLA, [m]

SST first display

We can now plot SST from sst

ax = start_axes("SST")
m = sst.display(ax, "analysed_sst", vmin=295, vmax=300)
update_axes(ax, m, unit="[°K]")
SST, [°K]
ax = start_axes("SST")
m = sst.display(ax, "analysed_sst", vmin=295, vmax=300)
u, v = sst.grid("u").T, sst.grid("v").T
ax.quiver(sst.x_c[::3], sst.y_c[::3], u[::3, ::3], v[::3, ::3], scale=10)
update_axes(ax, m, unit="[°K]")
SST, [°K]

Now, with eddy contours, and displaying SST anomaly

sst.bessel_high_filter("analysed_sst", 400)

Eddy detection

sst.bessel_high_filter("sla", 400)
# ADT filtered
ax = start_axes("SLA", extent=extent)
m = sst.display(ax, "sla", vmin=-0.1, vmax=0.1)
update_axes(ax, m, unit="[m]")
a, c = sst.eddy_identification("sla", "u", "v", date, 0.002)
SLA, [m]
kwargs_a = dict(lw=2, label="Anticyclonic", ref=-10, color="b")
kwargs_c = dict(lw=2, label="Cyclonic", ref=-10, color="r")
ax = start_axes("SST anomaly")
m = sst.display(ax, "analysed_sst", vmin=-1, vmax=1)
a.display(ax, **kwargs_a), c.display(ax, **kwargs_c)
ax.legend()
update_axes(ax, m, unit="[°K]")
SST anomaly, [°K]

Example of post-processing

Get mean of sst anomaly_high in each internal contour

anom_a = a.interp_grid(sst, "analysed_sst", method="mean", intern=True)
anom_c = c.interp_grid(sst, "analysed_sst", method="mean", intern=True)

Are cyclonic (resp. anticyclonic) eddies generally associated with positive (resp. negative) SST anomaly ?

fig = plt.figure(figsize=(7, 5))
ax = fig.add_axes([0.05, 0.05, 0.90, 0.90])
ax.set_xlabel("SST anomaly")
ax.set_xlim([-1, 1])
ax.set_title("Histograms of SST anomalies")
ax.hist(
    anom_a, 5, alpha=0.5, color="b", label="Anticyclonic (mean:%s)" % (anom_a.mean())
)
ax.hist(anom_c, 5, alpha=0.5, color="r", label="Cyclonic (mean:%s)" % (anom_c.mean()))
ax.legend()
Histograms of SST anomalies

Not clearly so in that case ..

Total running time of the script: ( 0 minutes 10.201 seconds)

Gallery generated by Sphinx-Gallery

Gallery generated by Sphinx-Gallery

Eddy identification

Run the identification process for a single day

Shell/bash command

Bash command will allow to process one grid, it will apply a filter and an identification.

EddyId share/nrt_global_allsat_phy_l4_20190223_20190226.nc 20190223 \
    adt ugos vgos longitude latitude \
    out_directory -v DEBUG

Filter could be modify with options –cut_wavelength and –filter_order. You could also defined height between two isolines with –isoline_step, which could improve speed profile quality and detect accurately tiny eddies. You could also use –fit_errmax to manage acceptable shape of eddies.

An eddy identification will produce two files in the output directory, one for anticyclonic eddies and the other one for cyclonic.

In regional area which are away from the equator, current could be deduce from height, juste write None None inplace of ugos vgos

In case of datacube, you need to specify index for each layer (time, depth, …) wiht –indexs option like:

EddyId share/nrt_global_allsat_phy_l4_20190223_20190226.nc 20190223 \
    adt ugos vgos longitude latitude \
    out_directory -v DEBUG --indexs time=0

Warning

If no index are specified, you will apply identification only on dataset first layer, which could be a problem for datacube. Date set in command is used only for output storage.

Python code

If we want customize eddies identification, python module is here.

Activate verbose

from py_eddy_tracker import start_logger
start_logger().setLevel('DEBUG') # Available options: ERROR, WARNING, INFO, DEBUG

Run identification

from datetime import datetime
h = RegularGridDataset(grid_name, lon_name, lat_name)
h.bessel_high_filter('adt', 500, order=3)
date = datetime(2019, 2, 23)
a, c = h.eddy_identification(
    'adt', 'ugos', 'vgos', # Variables used for identification
    date, # Date of identification
    0.002, # step between two isolines of detection (m)
    pixel_limit=(5, 2000), # Min and max pixel count for valid contour
    shape_error=55, # Error max (%) between ratio of circle fit and contour
    )

Plot the resulting identification

fig = plt.figure(figsize=(15,7))
ax = fig.add_axes([.03,.03,.94,.94])
ax.set_title('Eddies detected -- Cyclonic(red) and Anticyclonic(blue)')
ax.set_ylim(-75,75)
ax.set_xlim(0,360)
ax.set_aspect('equal')
a.display(ax, color='b', linewidth=.5)
c.display(ax, color='r', linewidth=.5)
ax.grid()
fig.savefig('share/png/eddies.png')
_images/eddies.png

Save identification data

from netCDF import Dataset
with Dataset(date.strftime('share/Anticyclonic_%Y%m%d.nc'), 'w') as h:
    a.to_netcdf(h)
with Dataset(date.strftime('share/Cyclonic_%Y%m%d.nc'), 'w') as h:
    c.to_netcdf(h)

Load, Display and Filtering

Loading grid

from py_eddy_tracker.dataset.grid import RegularGridDataset
grid_name, lon_name, lat_name = 'share/nrt_global_allsat_phy_l4_20190223_20190226.nc', 'longitude', 'latitude'
h = RegularGridDataset(grid_name, lon_name, lat_name)

Plotting grid

from matplotlib import pyplot as plt
fig = plt.figure(figsize=(14, 12))
ax = fig.add_axes([.02, .51, .9, .45])
ax.set_title('ADT (m)')
ax.set_ylim(-75, 75)
ax.set_aspect('equal')
m = h.display(ax, name='adt', vmin=-1, vmax=1)
ax.grid(True)
plt.colorbar(m, cax=fig.add_axes([.94, .51, .01, .45]))

Filtering

h = RegularGridDataset(grid_name, lon_name, lat_name)
h.bessel_high_filter('adt', 500, order=3)

Save grid

h.write('/tmp/grid.nc')

Add second plot

ax = fig.add_axes([.02, .02, .9, .45])
ax.set_title('ADT Filtered (m)')
ax.set_aspect('equal')
ax.set_ylim(-75, 75)
m = h.display(ax, name='adt', vmin=-.1, vmax=.1)
ax.grid(True)
plt.colorbar(m, cax=fig.add_axes([.94, .02, .01, .45]))
fig.savefig('share/png/filter.png')
_images/filter.png

Spectrum

Compute spectrum and spectrum ratio on some area

Load data

raw = RegularGridDataset(grid_name, lon_name, lat_name)
filtered = RegularGridDataset(grid_name, lon_name, lat_name)
filtered.bessel_low_filter('adt', 150, order=3)

areas = dict(
    sud_pacific=dict(llcrnrlon=188, urcrnrlon=280, llcrnrlat=-64, urcrnrlat=-7),
    atlantic_nord=dict(llcrnrlon=290, urcrnrlon=340, llcrnrlat=19.5, urcrnrlat=43),
    indien_sud=dict(llcrnrlon=35, urcrnrlon=110, llcrnrlat=-49, urcrnrlat=-26),
)

Compute and display spectrum

fig = plt.figure(figsize=(10,6))
ax = fig.add_subplot(111)
ax.set_title('Spectrum')
ax.set_xlabel('km')
for name_area, area in areas.items():

    lon_spec, lat_spec = raw.spectrum_lonlat('adt', area=area)
    mappable = ax.loglog(*lat_spec, label='lat %s raw' % name_area)[0]
    ax.loglog(*lon_spec, label='lon %s raw' % name_area, color=mappable.get_color(), linestyle='--')

    lon_spec, lat_spec = filtered.spectrum_lonlat('adt', area=area)
    mappable = ax.loglog(*lat_spec, label='lat %s high' % name_area)[0]
    ax.loglog(*lon_spec, label='lon %s high' % name_area, color=mappable.get_color(), linestyle='--')

ax.set_xscale('log')
ax.legend()
ax.grid()
fig.savefig('share/png/spectrum.png')
_images/spectrum.png

Compute and display spectrum ratio

fig = plt.figure(figsize=(10,6))
ax = fig.add_subplot(111)
ax.set_title('Spectrum ratio')
ax.set_xlabel('km')
for name_area, area in areas.items():
    lon_spec, lat_spec = filtered.spectrum_lonlat('adt', area=area, ref=raw)
    mappable = ax.plot(*lat_spec, label='lat %s high' % name_area)[0]
    ax.plot(*lon_spec, label='lon %s high' % name_area, color=mappable.get_color(), linestyle='--')

ax.set_xscale('log')
ax.legend()
ax.grid()
fig.savefig('share/png/spectrum_ratio.png')
_images/spectrum_ratio.png

Tracking

Requirements

Before to run tracking, you will need to run identification on every time step of the period(Period of your study).

Advice : Before to run tracking, display some identification file allow to learn a lot

Default method

To run a tracking just create an yaml file with minimal specification (FILES_PATTERN and SAVE_DIR). You will run tracking separately between Cyclonic eddies and Anticyclonic eddies.

Example of conf.yaml

PATHS:
  # Files produces with EddyIdentification
  FILES_PATTERN: MY_IDENTIFICATION_PATH/Anticyclonic*.nc
  SAVE_DIR: MY_OUTPUT_PATH

# Number of timestep for missing detection
VIRTUAL_LENGTH_MAX: 3
# Minimal time to consider as a full track
TRACK_DURATION_MIN: 10

To run:

EddyTracking conf.yaml -v DEBUG

It will use default tracker:

  • No travel longer than 125 km between two observation

  • Amplitude and speed radius must be close to previous observation

  • In case of several candidate only closest is kept

It will produce 4 files by run:

  • A file of correspondances which will contains all the information to merge all identifications file

  • A file which will contains all the observations which are alone

  • A file which will contains all the short track which are shorter than TRACK_DURATION_MIN

  • A file which will contains all the long track which are longer than TRACK_DURATION_MIN

Use python module

An example of tracking with python module is available in the gallery: Track in python

Choose a tracker

With yaml you could also select another tracker:

PATHS:
  # Files produces with EddyIdentification
  FILES_PATTERN: MY/IDENTIFICATION_PATH/Anticyclonic*.nc
  SAVE_DIR: MY_OUTPUT_PATH

# Number of timestep for missing detection
VIRTUAL_LENGTH_MAX: 3
# Minimal time to consider as a full track
TRACK_DURATION_MIN: 10

CLASS:
    # Give the module to import,
    # must be available when you do "import module" in python
    MODULE: py_eddy_tracker.featured_tracking.old_tracker_reference
    # Give class name which must be inherit from
    # py_eddy_tracker.observations.observation.EddiesObservations
    CLASS: CheltonTracker

This tracker is like described in CHELTON11[https://doi.org/10.1016/j.pocean.2011.01.002]. Code is here py_eddy_tracker.featured_tracking.old_tracker_reference()

Customize tracking

Code my own tracking

To use your own tracking method, you just need to create a class which inherit from py_eddy_tracker.observations.observation.EddiesObservations() and set this class in yaml file like we see in the previous topic.

API reference

py_eddy_tracker.appli

Entry point

py_eddy_tracker.dataset.grid

Class to load and manipulate RegularGrid and UnRegularGrid

py_eddy_tracker.featured_tracking

py_eddy_tracker.observations.network

Class to create network of observations

py_eddy_tracker.observations.observation

Base class to manage eddy observation

py_eddy_tracker.observations.tracking

Class to manage observations gathered in track

py_eddy_tracker.eddy_feature

Class to compute Amplitude and average speed profile

py_eddy_tracker.generic

Tool method which use mostly numba

py_eddy_tracker.gui

GUI class

py_eddy_tracker.poly

Method for polygon

py_eddy_tracker.tracking

Class to store link between observations

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased]

[3.3.0] - 2020-12-03

Added

  • Add an implementation of visvalingam algorithm to simplify polygons with low modification

  • Add method to found close tracks in an other atlas

  • Allow to give a x reference when we display grid to be able to change xlim

  • Add option to EddyId to select data index like –indexs time=5 depth=2

  • Add a method to merge several indexs type for eddy obs

  • Get dataset variable like attribute, and lifetime/age are available for all observations

  • Add EddyInfos application to get general information about eddies dataset

  • Add method to inspect contour rejection (which are not in eddies)

  • Grid interp could be “nearest” or “bilinear”

Changed

  • Now to have object informations in plot label used python `format` style, several key are available :

    • “t0”

    • “t1”

    • “nb_obs”

    • “nb_tracks” (only for tracked eddies)

[3.2.0] - 2020-09-16

[3.1.0] - 2020-06-25

Indices and tables