File: features.py

package info (click to toggle)
python-sigima 1.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 24,956 kB
  • sloc: python: 33,326; makefile: 3
file content (135 lines) | stat: -rw-r--r-- 4,310 bytes parent folder | download
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_)