1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
|
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
.. Features (see parent package :mod:`sigima.algorithms.signal`)
"""
from __future__ import annotations
import numpy as np
from sigima.tools.checks import check_1d_array, check_1d_arrays
@check_1d_array(min_size=2, finite_only=True)
def find_zero_crossings(y: np.ndarray) -> np.ndarray:
"""Find the left indices of the zero-crossing intervals in the given array.
Args:
y: Input array.
Returns:
An array of indices where zero-crossings occur.
"""
return np.nonzero(np.diff(np.sign(y)))[0]
@check_1d_arrays(x_sorted=True)
def find_x_axis_crossings(x: np.ndarray, y: np.ndarray) -> np.ndarray:
"""Find the :math:`x_n` values where :math:`y = f(x)` intercepts the x-axis.
This function uses zero-crossing detection and interpolation to find the x values
where :math:`y = 0`.
Args:
x: X data.
y: Y data.
Returns:
Array of x-intercepts. The array is empty if no intercept is found.
"""
# Find zero crossings.
xi_before = find_zero_crossings(y)
if len(xi_before) == 0:
return np.array([])
# Interpolate to find x values at zero crossings.
xi_after = xi_before + 1
slope = (y[xi_after] - y[xi_before]) / (x[xi_after] - x[xi_before])
with np.errstate(divide="ignore"):
x0 = -y[xi_before] / slope + x[xi_before]
x0 = np.where(np.isfinite(x0), x0, (x[xi_before] + x[xi_after]) / 2)
# mask = ~np.isfinite(x0)
# x0[mask] = xi_before[mask]
return x0
@check_1d_arrays(x_min_size=2, x_finite_only=True, x_sorted=True)
def find_y_at_x_value(x: np.ndarray, y: np.ndarray, x_target: float) -> float:
"""Return the y value at a specified x value using linear interpolation.
Args:
x: X data.
y: Y data.
x_target: Input x value.
Returns:
Interpolated y value at x_target, or `nan` if input value is not within the
interpolation range.
"""
if np.isnan(x_target):
return np.nan
return float(np.interp(x_target, x, y, left=np.nan, right=np.nan))
@check_1d_arrays
def find_x_values_at_y(x: np.ndarray, y: np.ndarray, y_target: float) -> np.ndarray:
"""Find all x values where :math:`y = f(x)` equals the value :math:`y_target`.
Args:
x: X data.
y: Y data.
y_target: Target value.
Returns:
Array of x values where :math:`y = f(x)` equals :math:`y_target`.
"""
return find_x_axis_crossings(x, y - y_target)
@check_1d_arrays(x_evenly_spaced=True)
def find_bandwidth_coordinates(
x: np.ndarray, y: np.ndarray, threshold: float = -3.0
) -> tuple[float, float, float, float] | None:
"""Compute the bandwidth of the signal at a given threshold relative to the maximum.
Args:
x: X data.
y: Y data.
threshold: Threshold in decibel (relative to the maximum) at which the bandwidth
is computed. Defaults to -3.0 dB.
Returns:
Segment coordinates of the bandwidth of the signal at the given threshold.
Returns None if the bandwidth cannot be determined.
"""
level: float = np.max(y) + threshold
crossings = find_x_values_at_y(x, y, level)
if len(crossings) == 1:
# One crossing: 1) baseband bandwidth if max is above crossing
# 2) passband bandwidth if max is below crossing
if x[np.argmax(y)] < crossings[0]: # Baseband bandwidth
coords = (0.0, level, crossings[0], level)
else:
coords = (crossings[0], level, x[-1], level)
elif len(crossings) == 2: # Passband bandwidth
# Two crossings: 1) passband bandwidth if max is above both crossings
# 2) no bandwidth if max is below both crossings
# 3) baseband bandwidth if max is between crossings
coords = (crossings[0], level, crossings[1], level)
else:
# No crossing or more than two crossings: cannot determine bandwidth
return None
return coords
def contrast(y: np.ndarray) -> float:
"""Compute contrast
Args:
y: Input array
Returns:
Contrast
"""
max_, min_ = np.max(y), np.min(y)
return (max_ - min_) / (max_ + min_)
|