Timeseries module - demo#

pynsitu.tseries implements methods useful to the time series analysis

Assumptions about the data

import xarray as xr
import pandas as pd
import numpy as np

import matplotlib.pyplot as plt

import pynsitu as pin
Warning: could not import pytide

generate synthetic time series#

tdefault = dict(start="2018-01-01", end="2018-01-30", freq="1h")


def generate_time_series(label="time", uniform=True, time_units="datetime"):
    """Create a drifter time series."""
    time = pd.date_range(**tdefault)
    time_scale = pd.Timedelta("1D")
    if time_units == "timedelta":
        time = time - time[0]
    elif time_units == "numeric":
        time = (time - time[0]) / pd.Timedelta("1h")
        time_scale = 1.0
    if not uniform:
        nt = time.size
        import random

        random.seed(1001)
        time = time[random.sample(range(nt), 2 * nt // 3)].sort_values()
    #
    rng = np.random.default_rng(12345)
    v0 = (
        np.cos(2 * np.pi * ((time - time[0]) / time_scale))
        + rng.normal(size=time.size) / 2
    )
    v1 = (
        np.sin(2 * np.pi * ((time - time[0]) / time_scale))
        + rng.normal(size=time.size) / 2
    )
    df = pd.DataFrame({"v0": v0, "v1": v1, label: time})
    df = df.set_index(label)
    return df
# actually generate one time series
df = generate_time_series(uniform=False)
df.head()
v0 v1
time
2018-01-01 02:00:00 0.288087 0.554390
2018-01-01 03:00:00 1.597790 -0.565405
2018-01-01 05:00:00 0.271776 0.536564
2018-01-01 07:00:00 0.129232 0.903828
2018-01-01 09:00:00 -0.296491 1.024384
df.v0.plot()
df.v1.plot();
_images/ea6f0373e78b1c1d49cc70f307aee7b3e2e3030cbb4b713171ae1b599659062b.png

basic editing#

triming based on deployment information#

d = pin.events.Deployment(
    "some_event",
    start="2018/01/02 12:12:00",
    end="2018/01/10 12:12:00",
)

df_trimmed = df.ts.trim(d)

df.v0.plot()
df_trimmed.v0.plot()
<Axes: xlabel='time'>
_images/7d0a9c838fd78126450d1630abaff3b6ff9dda1615536c08357325bb073773a2.png

resampling on a regular timeline#

df_resampled = df.ts.resample_uniform("1h")
fig, ax = plt.subplots(1, 1)
df.v0.plot(marker="o", ls="None")
df_resampled.v0.plot(marker="*")
ax.set_xlim(df.index[10], df.index[30])
(np.float64(17532.791666666668), np.float64(17533.791666666668))
_images/64f5a0868b987d2f44e59902f7812715feb05dd6e2507df4ea823a345b889cd1.png

spectral analysis#

E = df_resampled.ts.spectrum(nperseg=24 * 5)
E
v0 v1
frequency
-0.000139 351.074214 309.222013
-0.000137 469.446684 284.267359
-0.000134 332.358004 210.038744
-0.000132 326.892586 232.814772
-0.000130 378.643108 237.213704
... ... ...
0.000127 392.326265 168.116599
0.000130 378.643108 237.213704
0.000132 326.892586 232.814772
0.000134 332.358004 210.038744
0.000137 469.446684 284.267359

120 rows × 2 columns

E.plot()
plt.yscale("log")
plt.grid()
_images/3da1adc36ae3e00bccb9d7c8a8fc038a205ee70a154538d52718c6d8b1943c71.png

rotary spectrum#

Compute the spectrum of v0 + 1j v1

E = df_resampled.ts.spectrum(nperseg=24 * 5, complex=("v0", "v1"))
E.plot()
plt.yscale("log")
plt.grid()
_images/be0b9df3bb2d3e6cc6b64a9ae4f61245dd19a18b35ecc0c6787993e860121537.png

filtering#

to be done …


tidal analysis#

to be done …


with xarray objects#

ds = df_resampled.to_xarray().expand_dims(x=range(10)).chunk(dict(x=2))
ds
<xarray.Dataset> Size: 117kB
Dimensions:  (x: 10, time: 695)
Coordinates:
  * x        (x) int64 80B 0 1 2 3 4 5 6 7 8 9
  * time     (time) datetime64[ns] 6kB 2018-01-01T02:00:00 ... 2018-01-30
Data variables:
    v0       (x, time) float64 56kB dask.array<chunksize=(2, 695), meta=np.ndarray>
    v1       (x, time) float64 56kB dask.array<chunksize=(2, 695), meta=np.ndarray>
E = ds.ts.spectrum(nperseg=24 * 5)
E.v0.isel(x=0).plot()
E.v1.isel(x=0).plot()
plt.yscale("log")
plt.grid()
_images/4e34b8f2c5f42ae1ace1dc157c4bdffd9f5e06aba70a28c5bea3db339178749f.png

vector principal axes/ellipse#

def compute_vector_principal_axes(u, v):
    """Compute vector time series principal axes
    See Emery and Thomson section 4.4.1

    Parameters
    ----------
        u, v: xr.DataArray
        Must possess `time` dimension

    Returns
    -------
        major, minor: xr.DataArray
        Complex arrays corresponding to major/minor axes scaled by respective eigenvalues
        The orientation of each axis is thus provided by the complex angle
    """

    # demean
    u_mean, v_mean = u.mean("time"), v.mean("time")
    up = u - u_mean
    vp = v - v_mean

    # build velocity covariance array and return eigenvectors
    uu = (up * up).mean("time")
    uv = (up * vp).mean("time")
    vv = (vp * vp).mean("time")

    # proceed with principal axes calculation  see Emery and Thomson 4.53
    ke = (uu + vv) * 0.5
    det = np.sqrt((uu - vv) ** 2 + 4 * uv**2)
    lambda_1 = ke + det * 0.5
    lambda_2 = ke - det * 0.5
    #
    s_1 = (lambda_1 - uu) / uv
    e_1 = (1 + 1j * s_1) / np.sqrt(1 + s_1**2)
    # compute vectors scaled by eigenvalues
    major = lambda_1 * e_1
    minor = lambda_2 * e_1 * np.exp(1j * np.pi / 2)

    return major, minor


def build_principal_ellipse(major, minor, scale=1):
    """build principal ellipse for drawing purposes"""
    # build ellipse
    t = xr.DataArray(np.linspace(0, 2 * np.pi, 100), dims="time").rename("time")
    v = (np.cos(t) * major + np.sin(t) * minor) * scale
    x = np.real(v)
    y = np.imag(v)
    return x, y
u = ds.v0
v = ds.v0 + ds.v1  # introduces correlation to rotate the ellipse
v_major, v_minor = compute_vector_principal_axes(u, v)

# check variances is preserved:
float(np.abs(v_major).compute()[0]) + float(np.abs(v_minor).compute()[0]), float(
    (u.std("time") ** 2 + v.std("time") ** 2).compute()[0]
)
(2.1267873595224254, 2.126787359522425)
# plot
x, y = build_principal_ellipse(v_major, v_minor)

fig, ax = plt.subplots(1, 1)
ax.plot(x + x.x, y)
ax.set_aspect("equal")
_images/d5e382daf947a09575cfdf0d39b2c2ff838cefc42e8c0d6b358947e4dc46c00b.png