Source code for oceans.ocfis

import re
import warnings

import gsw
import numpy as np
import numpy.ma as ma
import pandas as pd


[docs] def spdir2uv(spd, ang, deg=False): """ Computes u, v components from speed and direction. Parameters ---------- spd : array_like speed [m s :sup:`-1`] ang : array_like direction [deg] deg : bool option, True if data is in degrees. Default is False Returns ------- u : array_like zonal wind velocity [m s :sup:`-1`] v : array_like meridional wind velocity [m s :sup:`-1`] """ if deg: ang = np.deg2rad(ang) # Calculate U (E-W) and V (N-S) components u = spd * np.sin(ang) v = spd * np.cos(ang) return u, v
[docs] def uv2spdir(u, v, mag=0, rot=0): """ Computes speed and direction from u, v components. Converts rectangular to polar coordinate, geographic convention Allows for rotation and magnetic declination correction. Parameters ---------- u : array_like zonal wind velocity [m s :sup:`-1`] v : array_like meridional wind velocity [m s :sup:`-1`] mag : float, array_like, optional Magnetic correction [deg] rot : float, array_like Angle for rotation [deg] Returns ------- ang : array_like direction [deg] spd : array_like speed [m s :sup:`-1`] Notes ----- Zero degrees is north. Examples -------- >>> import matplotlib.pyplot as plt >>> from oceans.ocfis import uv2spdir >>> u = [+0, -0.5, -0.50, +0.90] >>> v = [+1, +0.5, -0.45, -0.85] >>> wd, ws = uv2spdir(u, v) >>> fig, ax = plt.subplots(subplot_kw=dict(polar=True)) >>> kw = dict(arrowstyle="->") >>> wd = np.deg2rad(wd) >>> lines = [ ... ax.annotate("", xy=(d, s), xytext=(0, 0), arrowprops=kw) ... for d, s in zip(wd, ws) ... ] >>> _ = ax.set_ylim(0, np.max(ws)) """ u, v, mag, rot = list(map(np.asarray, (u, v, mag, rot))) vec = u + 1j * v spd = np.abs(vec) ang = np.angle(vec, deg=True) ang = ang - mag + rot ang = np.mod(90.0 - ang, 360.0) # Zero is North. return ang, spd
[docs] def del_eta_del_x(U, f, g, balance="geostrophic", R=None): r""" Calculate :mat: `\frac{\partial \eta} {\partial x}` for different force balances Parameters ---------- U : array_like velocity magnitude [m/s] f : float Coriolis parameter d : float Acceleration of gravity balance : str, optional geostrophic [default], gradient or max_gradient R : float, optional Radius """ if balance == "geostrophic": detadx = f * U / g elif balance == "gradient": detadx = (U**2 / R + f * U) / g elif balance == "max_gradient": detadx = (R * f**2) / (4 * g) return detadx
[docs] def mld(SA, CT, p, criterion="pdvar"): r""" Compute the mixed layer depth. Parameters ---------- SA : array_like Absolute Salinity [g/kg] CT : array_like Conservative Temperature [:math:`^\circ` C (ITS-90)] p : array_like sea pressure [dbar] criterion : str, optional MLD Criteria Mixed layer depth criteria are: 'temperature' : Computed based on constant temperature difference criterion, CT(0) - T[mld] = 0.5 degree C. 'density' : computed based on the constant potential density difference criterion, pd[0] - pd[mld] = 0.125 in sigma units. 'pdvar' : computed based on variable potential density criterion pd[0] - pd[mld] = var(T[0], S[0]), where var is a variable potential density difference which corresponds to constant temperature difference of 0.5 degree C. Returns ------- MLD : array_like Mixed layer depth idx_mld : bool array Boolean array in the shape of p with MLD index. References ---------- Monterey, G., and S. Levitus, 1997: Seasonal variability of mixed layer depth for the World Ocean. NOAA Atlas, NESDIS 14, 100 pp. Washington, D.C. """ SA, CT, p = list(map(np.asanyarray, (SA, CT, p))) SA, CT, p = np.broadcast_arrays(SA, CT, p) SA, CT, p = list(map(ma.masked_invalid, (SA, CT, p))) p_min, idx = p.min(), p.argmin() sigma = gsw.rho(SA, CT, p_min) - 1000.0 # Temperature and Salinity at the surface, T0, S0, Sig0 = CT[idx], SA[idx], sigma[idx] # NOTE: The temperature difference criterion for MLD Tdiff = T0 - 0.5 # 0.8 on the matlab original if criterion == "temperature": idx_mld = CT > Tdiff elif criterion == "pdvar": pdvar_diff = gsw.rho(S0, Tdiff, p_min) - 1000.0 idx_mld = sigma <= pdvar_diff elif criterion == "density": sig_diff = Sig0 + 0.125 idx_mld = sigma <= sig_diff else: raise NameError(f"Unknown criteria {criterion}") MLD = ma.masked_all_like(p) MLD[idx_mld] = p[idx_mld] return MLD.max(axis=0), idx_mld
[docs] def pcaben(u, v): """ Principal components of 2-d (e.g. current meter) data calculates ellipse parameters for currents. Parameters ---------- u : array_like zonal wind velocity [m s :sup:`-1`] v : array_like meridional wind velocity [m s :sup:`-1`] Returns ------- major axis (majrax) minor axis (minrax) major azimuth (majaz) minor azimuth (minaz) ellipticity (elptcty) Examples -------- >>> import matplotlib.pyplot as plt >>> from oceans.ocfis import pcaben, uv2spdir >>> u = np.r_[(0.0, 1.0, -2.0, -1.0, 1.0), np.random.randn(10)] >>> v = np.r_[(3.0, 1.0, 0.0, -1.0, -1.0), np.random.randn(10)] >>> (mjrax, mjaz, mirax, miaz, el), (x1, x2, y1, y2) = pcaben(u, v) >>> fig, ax = plt.subplots() >>> _ = ax.plot(x1, y1, "r-", x2, y2, "r-") >>> ax.set_aspect("equal") >>> _ = ax.set_xlabel("U component") >>> _ = ax.set_ylabel("V component") >>> _ = ax.plot(u, v, "bo") >>> _ = ax.axis([-3.2, 3.2, -3.2, 3.2]) >>> mdir, mspd = uv2spdir(u.mean(), v.mean()) >>> _ = ax.plot([0, u.mean()], [0, v.mean()], "k-") >>> flatness = 1 - mirax / mjrax >>> if False: ... print("Mean u = {}".format(u.mean())) ... print("Mean v = {}".format(v.mean())) ... print("Mean magnitude: {}".format(mspd)) ... print("Direction: {}".format(mdir)) ... print("Axis 1 Length: {}".format(mjrax)) ... print("Axis 1 Azimuth: {}".format(mjaz)) ... print("Axis 2 Length: {}".format.format(mirax)) ... print("Axis 2 Azimuth: {}".format(miaz)) ... print("ellipticity is : {}".format(el)) ... print("Negative (positive) means clockwise (anti-clockwise)") ... print("Flatness: {}".format(flatness)) ... Notes: https://pubs.usgs.gov/of/2002/of02-217/m-files/pcaben.m """ u, v = np.broadcast_arrays(u, v) C = np.cov(u, v) D, V = np.linalg.eig(C) x1 = np.r_[0.5 * np.sqrt(D[0]) * V[0, 0], -0.5 * np.sqrt(D[0]) * V[0, 0]] y1 = np.r_[0.5 * np.sqrt(D[0]) * V[1, 0], -0.5 * np.sqrt(D[0]) * V[1, 0]] x2 = np.r_[0.5 * np.sqrt(D[1]) * V[0, 1], -0.5 * np.sqrt(D[1]) * V[0, 1]] y2 = np.r_[0.5 * np.sqrt(D[1]) * V[1, 1], -0.5 * np.sqrt(D[1]) * V[1, 1]] # Length and direction. az, leng = np.c_[uv2spdir(x1[0], y1[0]), uv2spdir(x2[1], y2[1])] if leng[0] >= leng[1]: majrax, majaz = leng[0], az[0] minrax, minaz = leng[1], az[1] else: majrax, majaz = leng[1], az[1] minrax, minaz = leng[0], az[0] elptcty = minrax / majrax # Radius to diameter. majrax *= 2 minrax *= 2 return (majrax, majaz, minrax, minaz, elptcty), (x1, x2, y1, y2)
[docs] def spec_rot(u, v): """ Compute the rotary spectra from u,v velocity components Parameters ---------- u : array_like zonal wind velocity [m s :sup:`-1`] v : array_like meridional wind velocity [m s :sup:`-1`] Returns ------- cw : array_like Clockwise spectrum [TODO] ccw : array_like Counter-clockwise spectrum [TODO] puv : array_like Cross spectra [TODO] quv : array_like Quadrature spectra [ TODO] Notes ----- The spectral energy at some frequency can be decomposed into two circularly polarized constituents, one rotating clockwise and other anti-clockwise. Examples -------- TODO: puv, quv, cw, ccw = spec_rot(u, v) References ---------- J. Gonella Deep Sea Res., 833-846, 1972. """ # Individual components Fourier series. fu, fv = list(map(np.fft.fft, (u, v))) # Auto-spectra of the scalar components. pu = fu * np.conj(fu) pv = fv * np.conj(fv) # Cross spectra. puv = fu.real * fv.real + fu.imag * fv.imag # Quadrature spectra. quv = -fu.real * fv.imag + fv.real * fu.imag # Rotatory components # TODO: Check the division, 4 (original code) or 8 (paper)? cw = (pu + pv - 2 * quv) / 4.0 ccw = (pu + pv + 2 * quv) / 4.0 N = len(u) F = np.arange(0, N) / N return puv, quv, cw, ccw, F
[docs] def lagcorr(x, y, M=None): """ Compute lagged correlation between two series. Follow emery and Thomson book "summation" notation. Parameters ---------- y : array time-series y : array time-series M : integer number of lags Returns ------- Cxy : array normalized cross-correlation function Examples -------- TODO: Implement Emery and Thomson example. """ x, y = list(map(np.asanyarray, (x, y))) if not M: M = x.size N = x.size Cxy = np.zeros(M) x_bar, y_bar = x.mean(), y.mean() for k in range(0, M, 1): a = 0.0 for i in range(N - k): a = a + (y[i] - y_bar) * (x[i + k] - x_bar) Cxy[k] = 1.0 / (N - k) * a return Cxy / (np.std(y) * np.std(x))
[docs] def complex_demodulation(series, f, fc, axis=-1): """ Perform a Complex Demodulation It acts as a bandpass filter for `f`. series => Time-Series object with data and datetime f => inertial frequency in rad/sec fc => normalized cutoff [0.2] math : series.data * np.exp(2 * np.pi * 1j * (1 / T) * time_in_seconds) """ import scipy.signal as signal # Time period ie freq = 1 / T T = 2 * np.pi / f # fc = fc * 1 / T # Filipe # De-mean data. d = series.data - series.data.mean(axis=axis) dfs = d * np.exp(2 * np.pi * 1j * (1 / T) * series.time_in_seconds) # Make a 5th order butter filter. # FIXME: Why 5th order? Try signal.buttord!? Wn = fc / series.Nyq [b, a] = signal.butter(5, Wn, btype="low") # FIXME: These are a factor of a thousand different from Matlab, why? cc = signal.filtfilt(b, a, dfs) # FIXME: * 1e3 amplitude = 2 * np.abs(cc) # TODO: Fix the outputs. # phase = np.arctan2(np.imag(cc), np.real(cc)) filtered_series = amplitude * np.exp( -2 * np.pi * 1j * (1 / T) * series.time_in_seconds, ) new_series = filtered_series.real, series.time # Return cc, amplitude, phase, dfs, filtered_series return new_series
[docs] def binave(datain, r): """ Averages vector data in bins of length r. The last bin may be the average of less than r elements. Useful for computing daily average time series (with r=24 for hourly data). Parameters ---------- data : array_like r : int bin length Returns ------- bindata : array_like bin-averaged vector Notes ----- Original from MATLAB AIRSEA TOOLBOX. Examples -------- >>> from oceans.ocfis import binave >>> data = [ ... 10.0, ... 11.0, ... 13.0, ... 2.0, ... 34.0, ... 21.5, ... 6.46, ... 6.27, ... 7.0867, ... 15.0, ... 123.0, ... 3.2, ... 0.52, ... 18.2, ... 10.0, ... 11.0, ... 13.0, ... 2.0, ... 34.0, ... 21.5, ... 6.46, ... 6.27, ... 7.0867, ... 15.0, ... 123.0, ... 3.2, ... 0.52, ... 18.2, ... 10.0, ... 11.0, ... 13.0, ... 2.0, ... 34.0, ... 21.5, ... 6.46, ... 6.27, ... 7.0867, ... 15.0, ... 123.0, ... 3.2, ... 0.52, ... 18.2, ... 10.0, ... 11.0, ... 13.0, ... 2.0, ... 34.0, ... 21.5, ... 6.46, ... 6.27, ... 7.0867, ... 15.0, ... 123.0, ... 3.2, ... 0.52, ... 18.2, ... ] >>> binave(data, 24) array([16.564725 , 21.1523625, 22.4670875]) References ---------- 03/08/1997: version 1.0 09/19/1998: version 1.1 (vectorized by RP) 08/05/1999: version 2.0 """ datain, rows = np.asarray(datain), np.asarray(r, dtype=np.int64) if datain.ndim != 1: raise ValueError("Must be a 1D array.") if rows <= 0: raise ValueError("Bin size R must be a positive integer.") # Compute bin averaged series. lines = datain.size // r z = datain[0 : (lines * rows)].reshape(rows, lines, order="F") bindata = np.r_[np.mean(z, axis=0), np.mean(datain[(lines * r) :])] return bindata
[docs] def binavg(x, y, db): """ Bins y(x) into db spacing. The spacing is given in `x` units. y = np.random.random(20) x = np.arange(len(y)) xb, yb = binavg(x, y, 2) plt.figure() plt.plot(x, y, 'k.-') plt.plot(xb, yb, 'r.-') """ # Cut the corners. x_min, x_max = np.ceil(x.min()), np.floor(x.max()) x = x.clip(x_min, x_max) # This is used to get the `inds`. xbin = np.arange(x_min, x_max, db) inds = np.digitize(x, xbin) # But this is the center of the bins. xbin = xbin - (db / 2.0) # FIXME there is an IndexError if I want to show this. # for n in range(x.size): # print xbin[inds[n]-1], "<=", x[n], "<", xbin[inds[n]] ybin = np.array([y[inds == i].mean() for i in range(0, len(xbin))]) # xbin = np.array([x[inds == i].mean() for i in range(0, len(xbin))]) return xbin, ybin
[docs] def bin_dates(self, freq, tz=None): """ Take a pandas time Series and return a new Series on the specified frequency. Examples -------- >>> import numpy as np >>> from pandas import Series, date_range >>> n = 24 * 30 >>> sig = np.random.rand(n) + 2 * np.cos(2 * np.pi * np.arange(n)) >>> dates = date_range(start="1/1/2000", periods=n, freq="h") >>> series = Series(data=sig, index=dates) >>> new_series = bin_dates(series, freq="D", tz=None) """ from pandas import date_range new_index = date_range(start=self.index[0], end=self.index[-1], freq=freq, tz=tz) new_series = self.groupby(new_index.asof).mean() # Averages at the center. secs = pd.Timedelta(new_index.freq).total_seconds() new_series.index = new_series.index.values + int(secs // 2) return new_series
[docs] def series_spline(self): """ Fill NaNs using a spline interpolation. """ from pandas import Series, isnull from scipy.interpolate import InterpolatedUnivariateSpline inds, values = np.arange(len(self)), self.values invalid = isnull(values) valid = -invalid firstIndex = valid.argmax() valid = valid[firstIndex:] invalid = invalid[firstIndex:] inds = inds[firstIndex:] result = values.copy() s = InterpolatedUnivariateSpline(inds[valid], values[firstIndex:][valid]) result[firstIndex:][invalid] = s(inds[invalid]) return Series(result, index=self.index, name=self.name)
[docs] def despike(self, n=3, recursive=False): """ Replace spikes with np.nan. Removing spikes that are >= n * std. default n = 3. """ from pandas import Series result = self.values.copy() outliers = np.abs(self.values - np.nanmean(self.values)) >= n * np.nanstd( self.values, ) removed = np.count_nonzero(outliers) result[outliers] = np.nan counter = 0 if recursive: while outliers.any(): result[outliers] = np.nan base = np.abs(result - np.nanmean(result)) outliers = base >= n * np.nanstd(result) counter += 1 removed += np.count_nonzero(outliers) return Series(result, index=self.index, name=self.name)
[docs] def pol2cart(theta, radius, units="deg"): """ Convert from polar to Cartesian coordinates Examples -------- >>> pol2cart(0, 1, units="deg") (np.float64(1.0), np.float64(0.0)) """ if units in ["deg", "degs"]: theta = theta * np.pi / 180.0 x = radius * np.cos(theta) y = radius * np.sin(theta) return x, y
[docs] def cart2pol(x, y): """ Convert from Cartesian to polar coordinates. Examples -------- >>> x = [+0, -0.5] >>> y = [+1, +0.5] >>> cart2pol(x, y) (array([1.57079633, 2.35619449]), array([1. , 0.70710678])) """ radius = np.hypot(x, y) theta = np.arctan2(y, x) return theta, radius
[docs] def wrap_lon180(lon): lon = np.atleast_1d(lon).copy() angles = np.logical_or((lon < -180), (180 < lon)) lon[angles] = wrap_lon360(lon[angles] + 180) - 180 return lon
[docs] def wrap_lon360(lon): lon = np.atleast_1d(lon).copy() positive = lon > 0 lon = lon % 360 lon[np.logical_and(lon == 0, positive)] = 360 return lon
[docs] def alphanum_key(s): key = re.split(r"(\d+)", s) key[1::2] = list(map(int, key[1::2])) return key
[docs] def get_profile(x, y, f, xi, yi, mode="nearest", order=3): """ Interpolate regular data. Parameters ---------- x : two dimensional np.ndarray an array for the :math:`x` coordinates y : two dimensional np.ndarray an array for the :math:`y` coordinates f : two dimensional np.ndarray an array with the value of the function to be interpolated at :math:`x,y` coordinates. xi : one dimension np.ndarray the :math:`x` coordinates of the point where we want the function to be interpolated. yi : one dimension np.ndarray the :math:`y` coordinates of the point where we want the function to be interpolated. order : int the order of the bivariate spline interpolation Returns ------- fi : one dimension np.ndarray the value of the interpolating spline at :math:`xi,yi` Examples -------- >>> import numpy as np >>> from oceans.ocfis import get_profile >>> x, y = np.meshgrid(range(360), range(91)) >>> f = np.array(range(91 * 360)).reshape((91, 360)) >>> Paris = [2.4, 48.9] >>> Rome = [12.5, 41.9] >>> Greenwich = [0, 51.5] >>> xi = Paris[0], Rome[0], Greenwich[0] >>> yi = Paris[1], Rome[1], Greenwich[1] >>> get_profile(x, y, f, xi, yi, order=3) array([17606, 15096, 18540]) """ from scipy.ndimage import map_coordinates x, y, f, xi, yi = list(map(np.asanyarray, (x, y, f, xi, yi))) conditions = np.array( [ xi.min() < x.min(), xi.max() > x.max(), yi.min() < y.min(), yi.max() > y.max(), ], ) if conditions.any(): warnings.warn("Warning! Extrapolation!!", stacklevel=2) dx = x[0, 1] - x[0, 0] dy = y[1, 0] - y[0, 0] jvals = (xi - x[0, 0]) / dx ivals = (yi - y[0, 0]) / dy coords = np.array([ivals, jvals]) return map_coordinates(f, coords, mode=mode, order=order)
[docs] def strip_mask(arr, fill_value=np.nan): """ Take a masked array and return its data(filled) + mask. """ if ma.isMaskedArray(arr): mask = np.ma.getmaskarray(arr) arr = np.ma.filled(arr, fill_value) return mask, arr else: return arr
[docs] def shiftdim(x, n=None): """ Matlab-like shiftdim in python. Examples -------- >>> import oceans.ocfis as ff >>> a = np.random.rand(1, 1, 3, 1, 2) >>> print("a shape and dimension: %s, %s" % (a.shape, a.ndim)) a shape and dimension: (1, 1, 3, 1, 2), 5 >>> # print(range(a.ndim)) >>> # print(np.roll(range(a.ndim), -2)) >>> # print(a.transpose(np.roll(range(a.ndim), -2))) >>> b = ff.shiftdim(a) >>> print("b shape and dimension: %s, %s" % (b.shape, b.ndim)) b shape and dimension: (3, 1, 2), 3 >>> c = ff.shiftdim(b, -2) >>> c.shape == a.shape True """ def no_leading_ones(shape): shape = np.atleast_1d(shape) if shape[0] == 1: shape = shape[1:] return no_leading_ones(shape) else: return shape if n is None: # returns the array B with the same number of # elements as X but with any leading singleton # dimensions removed. return x.reshape(no_leading_ones(x.shape)) elif n >= 0: # When n is positive, shiftdim shifts the dimensions # to the left and wraps the n leading dimensions to the end. return x.transpose(np.roll(list(range(x.ndim)), -n)) else: # When n is negative, shiftdim shifts the dimensions # to the right and pads with singletons. return x.reshape((1,) * -n + x.shape)