Diagnositics for a flock of particels#

When a group of particles, or trajectories, join together is space, they can be considered a ‘’flock’’. Such clusters have specific diagnostics and spatial temporal statistics, of which some are shown below.

0. Import of libraries#

General libraries are imported, but also the specific functions which are situated in this folder

[1]:
import xarray as xr
import cartopy
import matplotlib.pyplot as plt
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import numpy as np
from osgeo import osr

1. Load the data#

load the toy data:

[2]:
file = '../../Simulations/toy_data_01.nc'

ds=xr.open_dataset(file)
lat, lon = ds['lat'][:], ds['lon'][:]
tim, id = ds['time'][:], ds['trajectory'][:]

2. Calculate flock properties#

The toy data example consist of parcels that are released at the same time, within an square box. As time progresses these parcels create trajectories. One trajectory which might describe the general trajectory best is the medoid. In respect to all others, this spatial temporal path has the least separation distance, along the whole timespan. It can be calculated in the following way:

[3]:

def dist_meter(lon_1, lon_2, lat_1, lat_2, radius=None): """ calculate the distance between two locations """ Δlon, Δlat = lon_2 - lon_1, lat_2 - lat_1 lat_mean = (lat_1+lat_2)/2 distance = haversine(Δlat, Δlon, lat_mean, radius=radius) return distance def haversine(Δlat, Δlon, lat, radius=None): """ calculate the distance along a great-circle Parameters ---------- Δlat, Δlon : float, unit=degrees step size or angle difference lat : float, unit=degrees (mean) latitude where angles are situated radius : float, unit=meters estimate of the radius of the earth Returns ------- float, unit=meters distance """ Δlat, Δlon, lat = np.deg2rad(Δlat), np.deg2rad(Δlon), np.deg2rad(lat) if radius is None: wgs84 = osr.SpatialReference() wgs84.ImportFromEPSG(4326) a,b = wgs84.GetSemiMajor(), wgs84.GetSemiMinor() radius = np.sqrt(np.divide((a**2*np.cos(lat))**2 + (b**2*np.sin(lat))**2, (a*np.cos(lat))**2 + (b*np.sin(lat))**2) ) a = np.sin(Δlat / 2) ** 2 + \ np.cos(lat) * np.cos(lat+Δlat) * np.sin(Δlon / 2) ** 2 c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) # great circle distance d = radius * c return d
[4]:
def mediod(X, Y, spherical=False, robust=True):
    """ calculate which trajectory with least disagreement with all others

    Parameters
    ----------
    X,Y: np.ndarray, size=(m,n), unit={degrees,meters}
        m: individual entity, n: time
    spherical: bool
        when True, then spherical coordinates are given
        otherwise metric plane coordinates are assumed
    robust: bool
        when True, robust statistics are used in the form of the median
        otherwise the mean is used instead

    Returns
    -------
    integer, range=0...m
        index of the trajectory with the least sepration distance between
        all other trajectories
    """
    assert len(set({X.size, Y.size})) == 1
    assert len(set({X.ndim, Y.ndim})) == 1

    X, Y = np.atleast_2d(X), np.atleast_2d(Y)
    m,n = X.shape[:2]
    err = np.zeros((m,))
    for i in range(n):
        X_A, X_B = np.meshgrid(X[:, i], X[:, i])
        Y_A, Y_B = np.meshgrid(Y[:, i], Y[:, i])
        if spherical:
            D = dist_meter(X_A, X_B, Y_A, Y_B)
        else:
            D = np.sqrt((X_A - X_B)**2 + (Y_A - Y_B)**2)
        if robust:
            err += np.median(D, axis=1)
        else:
            err += np.mean(D, axis=1)
    id = np.argmin(err)
    return id

[5]:
id = mediod(lon.data, lat.data, spherical=True)
/Users/Hodgs004/miniforge3/envs/lagrangian_diags/lib/python3.11/site-packages/osgeo/osr.py:424: FutureWarning: Neither osr.UseExceptions() nor osr.DontUseExceptions() has been explicitly called. In GDAL 4.0, exceptions will be enabled by default.
  warnings.warn(
[6]:
fig = plt.figure(figsize=(16,17)) #(17,5)
ax1 = plt.subplot(111, projection=cartopy.crs.PlateCarree())

ax1.coastlines(resolution='50m')
ax1.plot(lon.data.T, lat.data.T, transform=cartopy.crs.PlateCarree(), color='xkcd:greyish blue')
ax1.plot(lon.data[id,...], lat.data[id,...], transform=cartopy.crs.PlateCarree(), color='xkcd:deep pink')
ax1.set_xlim([15, 35])
ax1.set_ylim([-42, -32])
gl = ax1.gridlines(crs=cartopy.crs.PlateCarree(), draw_labels=True)
gl.top_labels = False
gl.right_labels = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER

plt.show()
../_images/tutorials_flock_diagnostics_8_0.png

Center of mass displacement#

[7]:
def center_of_mass_displacement(X, Y, X0=None, Y0=None):
    """ see equation (1) in [La08]_.

    Parameters
    ----------
    X,Y: numpy.ndarray, size=(m,n), unit={degrees,meters}
        m: id, n: time
    X0,Y0: {float, numpy.ndarray}
        initial coordinate(s) at the start

    Returns
    -------
    M_x,M_y: np.ndarray, size=(n,), unit={degrees,meters}
        displacement of the center of mass over time for the different axis

    See Also
    --------
    center_of_mass_spread

    References
    ----------
    .. [La08] LaCasce, "Statistics from Lagrangian observations", Progress in
              Oceanography, vol.77(1) pp.1-29, 2008.
    """
    assert len(set({X.size, Y.size})) == 1
    assert len(set({X.ndim, Y.ndim})) == 1

    X,Y = np.atleast_2d(X), np.atleast_2d(Y)
    if X0 is None:
        X0,Y0 = X[:,0], Y[:,0]

    M_x,M_y = np.mean(np.transpose(X.T-X0), axis=0), \
              np.mean(np.transpose(Y.T-Y0), axis=0)
    return M_x,M_y
[8]:
dispx, dispy = center_of_mass_displacement(lon.data, lat.data)
a = np.mean(lon.data[:,0]) +dispx
b = np.mean(lat.data[:,0]) + dispy
[9]:
fig = plt.figure(figsize=(16,17)) #(17,5)
ax1 = plt.subplot(111, projection=cartopy.crs.PlateCarree())

ax1.coastlines(resolution='50m')
ax1.plot(lon.data.T, lat.data.T, transform=cartopy.crs.PlateCarree(),
         color='xkcd:greyish blue', alpha=0.4, linewidth=2)
ax1.plot(a, b,
         transform=cartopy.crs.PlateCarree(), color='xkcd:bright pink')
ax1.set_xlim([15, 35])
ax1.set_ylim([-42, -32])
gl = ax1.gridlines(crs=cartopy.crs.PlateCarree(), draw_labels=True)
gl.top_labels = False
gl.right_labels = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER

plt.show()
../_images/tutorials_flock_diagnostics_12_0.png

Center of mass dispersion#

[10]:
def center_of_mass_spread(X, Y, X0=None, Y0=None, M_x=None, M_y=None):
    """ see equation (2) in [La08]_.

    Parameters
    ----------
    X,Y: numpy.ndarray, size=(m,n), unit={degrees,meters}
        m: id, n: time
    X0,Y0: {float, numpy.ndarray}
        initial coordinate(s) at the start
    Mx,My: numpy.ndarray, size=(n,)
        first moment of the flock

    Returns
    -------
    D_x,D_y: numpy.ndarray, size=(n,), unit={degrees,meters}
        spread of the center of mass over time for the different spatial axis

    See Also
    --------
    center_of_mass_displacement

    References
    ----------
    .. [La08] LaCasce, "Statistics from Lagrangian observations", Progress in
              Oceanography, vol.77(1) pp.1-29, 2008.
    """
    assert len(set({X.size, Y.size})) == 1
    assert len(set({X.ndim, Y.ndim})) == 1

    X,Y = np.atleast_2d(X), np.atleast_2d(Y)
    if X0 is None:
        X0,Y0 = X[:,0], Y[:,0]
    if M_x is None:
        M_x,M_y = center_of_mass_displacement(X, Y, X0=X0, Y0=Y0)

    def _estimate_dispersion(Z, Z0, M_z):
        m, n = Z.shape[:2]
        D_z = np.sum(np.power( Z \
                               - np.tile(np.atleast_2d(Z0).T,(1, n)) \
                               - np.tile(np.atleast_2d(M_z), (m, 1)),2),
                     axis=0) / (n-1)
        return D_z

    D_x = _estimate_dispersion(X, X0, M_x)
    D_y = _estimate_dispersion(Y, Y0, M_y)
    return D_x, D_y
[11]:
D_lat, D_lon = center_of_mass_spread(lon.data, lat.data, M_x=dispx, M_y=dispy)
[12]:
fig = plt.figure(figsize=(16,17)) #(17,5)
ax1 = plt.subplot(111, projection=cartopy.crs.PlateCarree())

ax1.coastlines(resolution='50m')
ax1.plot(lon.data.T, lat.data.T, transform=cartopy.crs.PlateCarree(),
         color='xkcd:greyish blue', alpha=0.4, linewidth=2)
cc = ax1.scatter(a, b,
                 transform=cartopy.crs.PlateCarree(), c=np.sqrt(D_lat**2 + D_lon**2))
ax1.set_xlim([15, 35])
ax1.set_ylim([-42, -32])
gl = ax1.gridlines(crs=cartopy.crs.PlateCarree(), draw_labels=True)
gl.top_labels = False
gl.right_labels = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
plt.colorbar(cc, shrink=0.3)
plt.show()
../_images/tutorials_flock_diagnostics_16_0.png

Cumulative Separation#

[13]:
def normalized_cumulative_lagrangian_separation(X, Y, X_ref=None, Y_ref=None,
                                                leader_id=None, spherical=True):
    """ eq. (3) in [LW11]_

    Parameters
    ----------
    X,Y: numpy.ndarray, size=(m,n), unit={degrees,meters}
        trajectery stack with m: id, n: time
    X_ref,Y_ref: numpy.ndarray, size=(n,)
        reference or leader trajectory
    leader_id: integer, range=0...m
        index of the leader within the trajectery stack {X,Y}
    spherical: bool
        when True, then spherical coordinates are given
        otherwise metric plane coordinates are assumed

    Returns
    -------
    numpy.ndarray, size=(m,), unit=meters
        skill score for the different tracks in respect to the leader/reference

    References
    ----------
    .. [LW11] Liu & Weisberg, "Evaluation of trajectory modeling in different
              dynamic regions using normalized cumulative Lagrangian separation"
              Journal of geophysical research, vol.116 pp.C09013, 2011.
    """

    if leader_id is not None:
        X_ref, Y_ref = X[leader_id,:], Y[leader_id,:]
        X, Y = np.delete(X, leader_id, axis=0), np.delete(Y, leader_id, axis=0)

    if spherical:
        D = dist_meter(X_ref, X, Y_ref, Y) # distance from target
        L = dist_meter(X[:,:-1], X[:,1:], Y[:,:-1], Y[:,1:]) # travel distance
    else:
        D = np.sqrt(np.power(X - X_ref, 2) + np.power(Y - Y_ref, 2))
        L = np.sqrt(np.power(X[:,:-1] - X[:,1:], 2) +
                    np.power(Y[:,:-1] - Y[:,1:], 2))
    L = np.c_[np.zeros((L.shape[0],1)), L]
    L_cum = np.cumsum(L, axis=1)

    c = np.divide(np.sum(D, axis=1), np.sum(L_cum, axis=1))
    return c
[14]:
c = normalized_cumulative_lagrangian_separation(lon.data, lat.data,
                                                leader_id=id, spherical=True)
[15]:
fig = plt.figure(figsize=(16,17)) #(17,5)
ax1 = plt.subplot(111, projection=cartopy.crs.PlateCarree())

ax1.coastlines(resolution='50m')
for i in range(lat.shape[0]-1):
    ax1.plot(lon.data[i,:], lat.data[i,:],
             )
ax1.set_xlim([15, 35])
ax1.set_ylim([-42, -32])
gl = ax1.gridlines(crs=cartopy.crs.PlateCarree(), draw_labels=True)
gl.top_labels = False
gl.right_labels = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER

plt.show()
../_images/tutorials_flock_diagnostics_20_0.png
[ ]: