Source code for shoot.acoustic

"""
Acoustic analysis functions

Functions for computing acoustic parameters from sound speed profiles.
"""

import functools

import numba
import numpy as np
import xarray as xr
from scipy.signal import argrelmax, argrelmin

from . import meta as smeta


def _ilmax(profile):
    return argrelmax(profile)[0]


def _ilmin(profile):
    try:
        return argrelmin(profile)[0]
    except IndexError:
        return np.nan


def _ecs(profile, depth):
    ilmaxs = _ilmax(profile)
    try:
        if np.abs(depth[0]) > np.abs(depth[-1]):  # bottom to surf case
            return depth[ilmaxs[-1]] if profile[ilmaxs[-1]] > profile[-1] else 0 * depth[ilmaxs[-1]]
        else:  # surface to bottom case
            return depth[ilmaxs[0]] if profile[ilmaxs[0]] > profile[0] else 0 * depth[ilmaxs[0]]
    except IndexError:
        return np.nan * depth[-1]


def _iminc(profile, depth):
    pos_iminc = None
    ilmaxs = _ilmax(profile)
    ilmins = _ilmin(profile)
    if len(ilmaxs) > 1:
        if np.abs(depth[0]) > np.abs(depth[-1]):  # bottom to surf case
            maxd = ilmaxs[-2]
            maxs = ilmaxs[-1]
            for idx in ilmins:
                if (idx > maxd) and (idx < maxs):
                    pos_iminc = idx
                    break
        else:  # surface to bottom
            maxd = ilmaxs[1]
            maxs = ilmaxs[0]
            for idx in ilmins:
                if (idx > maxs) and (idx < maxd):
                    pos_iminc = idx
                    break
    if pos_iminc:
        return depth[pos_iminc]
    else:
        return np.nan * depth[-1]


def _mcp(profile, depth):
    ilmins = _ilmin(profile)
    try:
        if np.abs(depth[0]) > np.abs(depth[-1]):  # bottom to surface
            return depth[ilmins[0]]
        else:  # surface to bottom
            return depth[ilmins[-1]]
    except IndexError:
        return np.nan * depth[-1]


## ECS
def _get_ecs_(cs, depth, ecs, xdim, ydim, nx, ny):
    for j in numba.prange(ny):
        for i in range(nx):
            ecs[j, i] = _ecs(cs[:, j, i], depth[:, j, i])
    return ecs


def _get_ecs_wrapper_(cs, depth, xdim, ydim, nx, ny):
    return _get_ecs_(cs, depth, np.empty([ny, nx]), xdim, ydim, nx, ny)


[docs] def get_ecs(cs): """Compute surface duct thickness Parameters ---------- cs : xarray.DataArray 3D sound speed field. Returns ------- xarray.DataArray Surface duct thickness (épaisseur du chenal de surface). """ xdim = smeta.get_xdim(cs) ydim = smeta.get_ydim(cs) zdim = smeta.get_zdim(cs) nx = len(cs[xdim]) ny = len(cs[ydim]) depth = smeta.get_depth(cs) if len(depth.shape) == 1: depth = depth.broadcast_like(cs) ecs = xr.apply_ufunc( _get_ecs_wrapper_, cs, depth, input_core_dims=[[zdim, ydim, xdim], [zdim, ydim, xdim]], output_core_dims=[[ydim, xdim]], dask="parallelized", vectorize=False, kwargs={"xdim": xdim, "ydim": ydim, "nx": nx, "ny": ny}, ) return ecs
## MCP def _get_mcp_(cs, depth, mcp, xdim, ydim, nx, ny): for j in numba.prange(ny): for i in range(nx): mcp[j, i] = _mcp(cs[:, j, i], depth[:, j, i]) return mcp def _get_mcp_wrapper_(cs, depth, xdim, ydim, nx, ny): return _get_mcp_(cs, depth, np.empty([ny, nx]), xdim, ydim, nx, ny)
[docs] def get_mcp(cs): """Compute deep sound speed minimum depth (MCP) Parameters ---------- cs : xarray.DataArray 3D sound speed field. Returns ------- xarray.DataArray Deep sound speed minimum depth (minimum de célérité profond). """ xdim = smeta.get_xdim(cs) ydim = smeta.get_ydim(cs) zdim = smeta.get_zdim(cs) nx = len(cs[xdim]) ny = len(cs[ydim]) depth = smeta.get_depth(cs) if len(depth.shape) == 1: depth = depth.broadcast_like(cs) mcp = xr.apply_ufunc( _get_mcp_wrapper_, cs, depth, input_core_dims=[[zdim, ydim, xdim], [zdim, ydim, xdim]], output_core_dims=[[ydim, xdim]], dask="parallelized", vectorize=False, kwargs={"xdim": xdim, "ydim": ydim, "nx": nx, "ny": ny}, ) return mcp
# IMINC ## MCP def _get_iminc_(cs, depth, iminc, xdim, ydim, nx, ny): for j in numba.prange(ny): for i in range(nx): iminc[j, i] = _iminc(cs[:, j, i], depth[:, j, i]) return iminc def _get_iminc_wrapper_(cs, depth, xdim, ydim, nx, ny): return _get_iminc_(cs, depth, np.empty([ny, nx]), xdim, ydim, nx, ny)
[docs] def get_iminc(cs): """Compute intermediate sound speed minimum depth (IMINC) Parameters ---------- cs : xarray.DataArray 3D sound speed field. Returns ------- xarray.DataArray Intermediate minimum depth in two-channel profiles. """ xdim = smeta.get_xdim(cs) ydim = smeta.get_ydim(cs) zdim = smeta.get_zdim(cs) nx = len(cs[xdim]) ny = len(cs[ydim]) depth = smeta.get_depth(cs) if len(depth.shape) == 1: depth = depth.broadcast_like(cs) iminc = xr.apply_ufunc( _get_iminc_wrapper_, cs, depth, input_core_dims=[[zdim, ydim, xdim], [zdim, ydim, xdim]], output_core_dims=[[ydim, xdim]], dask="parallelized", vectorize=False, kwargs={"xdim": xdim, "ydim": ydim, "nx": nx, "ny": ny}, ) return iminc
[docs] class ProfileAcous: """Acoustic parameters for a single sound speed profile Computes acoustic properties (local maxima/minima, surface duct thickness, deep minimum, etc.) from a sound speed profile. Parameters ---------- profile : xarray.DataArray Sound speed profile. depth : xarray.DataArray Depth coordinate for the profile. Attributes ---------- ilmax : ndarray Indices of local maxima in the profile. ilmin : ndarray Indices of local minima in the profile. ecs : float Surface duct thickness (épaisseur du chenal de surface). iminc : float Depth of intermediate minimum in two-channel profiles. mcp : float Depth of deep sound speed minimum (minimum de célérité profond). """
[docs] def __init__(self, profile, depth): """Initialize acoustic profile analyzer Parameters ---------- profile : xarray.DataArray Sound speed profile. depth : xarray.DataArray Depth coordinate for the profile. """ self.profile = profile # it an cs xarray profile self.depth = depth # smeta.get_depth(profile) self.depth_dim = self.depth.dims[0]
@functools.cached_property def ilmax(self): return argrelmax(self.profile.values)[0] @functools.cached_property def ilmin(self): try: return argrelmin(self.profile.values)[0] except IndexError: return np.nan @functools.cached_property def ecs(self): return _ecs(self.profile.values, self.depth.values) @functools.cached_property def iminc(self): return _iminc(self.profile.values, self.depth.values) @functools.cached_property def mcp(self): return _mcp(self.profile.values, self.depth.values)
[docs] class AcousEddy: """Acoustic analysis for an eddy anomaly Computes acoustic parameters both inside and outside an eddy, and calculates the acoustic impact (difference between inside/outside). Parameters ---------- anomaly : Anomaly Eddy anomaly object containing profile data inside and outside the eddy. Attributes ---------- ecs_inside : float Surface duct thickness inside the eddy. ecs_outside : float Surface duct thickness outside the eddy. mcp_inside : float Deep sound speed minimum depth inside the eddy. mcp_outside : float Deep sound speed minimum depth outside the eddy. iminc_inside : float Intermediate minimum depth inside the eddy. iminc_outside : float Intermediate minimum depth outside the eddy. acoustic_impact : float Combined acoustic impact metric (sum of relative differences). """
[docs] def __init__(self, anomaly): """Initialize acoustic eddy analyzer Parameters ---------- anomaly : Anomaly Eddy anomaly object containing profile data. """ self.anomaly = anomaly
@functools.cached_property def ecs_inside(self): return ProfileAcous(self.anomaly.mean_profil_inside, self.anomaly.depth_vector).ecs @functools.cached_property def iminc_inside(self): return ProfileAcous(self.anomaly.mean_profil_inside, self.anomaly.depth_vector).iminc @functools.cached_property def mcp_inside(self): return ProfileAcous(self.anomaly.mean_profil_inside, self.anomaly.depth_vector).mcp @functools.cached_property def ecs_outside(self): return ProfileAcous(self.anomaly.mean_profil_outside, self.anomaly.depth_vector).ecs @functools.cached_property def iminc_outside(self): return ProfileAcous(self.anomaly.mean_profil_outside, self.anomaly.depth_vector).iminc @functools.cached_property def mcp_outside(self): return ProfileAcous(self.anomaly.mean_profil_outside, self.anomaly.depth_vector).mcp @staticmethod def _distance(e1, e2): if np.isnan(e1) and np.isnan(e2): # point doesn exists at all return 0 elif np.isnan(e1) or np.isnan(e2): # creation/destruction point return 1 elif e1 == 0 and e2 == 0: return 0 else: return np.abs(e1 - e2) / np.abs(0.5 * (e1 + e2)) @functools.cached_property def acoustic_impact(self): ecs_in = self.ecs_inside mcp_in = self.mcp_inside iminc_in = self.iminc_inside ecs_out = self.ecs_outside mcp_out = self.mcp_outside iminc_out = self.iminc_outside d_mcp = AcousEddy._distance(mcp_in, mcp_out) d_iminc = AcousEddy._distance(iminc_in, iminc_out) d_ecs = AcousEddy._distance(ecs_in, ecs_out) return d_mcp + d_iminc + d_ecs @functools.cached_property def ecs_insides(self): ecs = np.empty(self.anomaly.profils_inside.shape[0]) for i in range(len(ecs)): ecs[i] = ProfileAcous( self.anomaly.profils_inside.isel(nb_profil=i), self.anomaly.depth_vector, ).ecs lon = smeta.get_lon(self.anomaly.profils_inside).values lat = smeta.get_lat(self.anomaly.profils_inside).values ecs = xr.DataArray( data=ecs, dims=["nb_profil"], coords=dict(lon_rho=(["nb_profil"], lon), lat_rho=(["nb_profil"], lat)), ) return ecs @functools.cached_property def ecs_outsides(self): ecs = np.empty(self.anomaly.profils_outside.shape[0]) for i in range(len(ecs)): ecs[i] = ProfileAcous( self.anomaly.profils_outside.isel(nb_profil=i), self.anomaly.depth_vector, ).ecs lon = smeta.get_lon(self.anomaly.profils_outside).values lat = smeta.get_lat(self.anomaly.profils_outside).values ecs = xr.DataArray( data=ecs, dims=["nb_profil"], coords=dict(lon_rho=(["nb_profil"], lon), lat_rho=(["nb_profil"], lat)), ) return ecs @functools.cached_property def mcp_insides(self): mcp = np.empty(self.anomaly.profils_inside.shape[0]) for i in range(len(mcp)): mcp[i] = ProfileAcous( self.anomaly.profils_inside.isel(nb_profil=i), self.anomaly.depth_vector, ).mcp lon = smeta.get_lon(self.anomaly.profils_inside).values lat = smeta.get_lat(self.anomaly.profils_inside).values mcp = xr.DataArray( data=mcp, dims=["nb_profil"], coords=dict(lon_rho=(["nb_profil"], lon), lat_rho=(["nb_profil"], lat)), ) return mcp @functools.cached_property def mcp_outsides(self): mcp = np.empty(self.anomaly.profils_outside.shape[0]) for i in range(len(mcp)): mcp[i] = ProfileAcous( self.anomaly.profils_outside.isel(nb_profil=i), self.anomaly.depth_vector, ).mcp lon = smeta.get_lon(self.anomaly.profils_outside).values lat = smeta.get_lat(self.anomaly.profils_outside).values mcp = xr.DataArray( data=mcp, dims=["nb_profil"], coords=dict(lon_rho=(["nb_profil"], lon), lat_rho=(["nb_profil"], lat)), ) return mcp @functools.cached_property def iminc_insides(self): iminc = np.empty(self.anomaly.profils_inside.shape[0]) for i in range(len(iminc)): iminc[i] = ProfileAcous( self.anomaly.profils_inside.isel(nb_profil=i), self.anomaly.depth_vector, ).iminc lon = smeta.get_lon(self.anomaly.profils_inside).values lat = smeta.get_lat(self.anomaly.profils_inside).values iminc = xr.DataArray( data=iminc, dims=["nb_profil"], coords=dict(lon_rho=(["nb_profil"], lon), lat_rho=(["nb_profil"], lat)), ) return iminc @functools.cached_property def iminc_outsides(self): iminc = np.empty(self.anomaly.profils_outside.shape[0]) for i in range(len(iminc)): iminc[i] = ProfileAcous( self.anomaly.profils_outside.isel(nb_profil=i), self.anomaly.depth_vector, ).iminc lon = smeta.get_lon(self.anomaly.profils_outside).values lat = smeta.get_lat(self.anomaly.profils_outside).values iminc = xr.DataArray( data=iminc, dims=["nb_profil"], coords=dict(lon_rho=(["nb_profil"], lon), lat_rho=(["nb_profil"], lat)), ) return iminc
[docs] def acoustic_points(eddies): """Compute acoustic impact for all eddies Adds acoustic parameters (ecs, iminc, mcp) inside and outside each eddy, plus overall acoustic impact metric. Parameters ---------- eddies : Eddies2D Collection of eddies with anomaly attributes. Notes ----- Modifies eddies in-place by adding acoustic attributes. """ for eddy in eddies.eddies: acous = AcousEddy(eddy.anomaly) eddy.ecs_insides = acous.ecs_insides eddy.ecs_outsides = acous.ecs_outsides eddy.iminc_insides = acous.iminc_insides eddy.iminc_outsides = acous.iminc_outsides eddy.mcp_insides = acous.mcp_insides eddy.mcp_outsides = acous.mcp_outsides eddy.acoustic_impact = acous.acoustic_impact