File: analysis.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 (225 lines) | stat: -rw-r--r-- 6,839 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# -*- coding: utf-8 -*-
# Licensed under the terms of the BSD 3-Clause
# (see sigima/LICENSE for details)

"""
General analysis functions
==========================

This module provides general analysis functions for signal objects:

- Histogram computation
- Other analysis operations

.. note::

    Most operations use standard NumPy/SciPy functions or custom analysis routines.
"""

from __future__ import annotations

import guidata.dataset as gds
import numpy as np

from sigima.config import _
from sigima.objects import (
    SignalObj,
    TableKind,
    TableResult,
    TableResultBuilder,
)
from sigima.proc.base import HistogramParam, new_signal_result
from sigima.proc.decorator import computation_function
from sigima.tools.signal import dynamic, features, pulse


@computation_function()
def histogram(src: SignalObj, p: HistogramParam) -> SignalObj:
    """Compute histogram with :py:func:`numpy.histogram`

    Args:
        src: source signal
        p: parameters

    Returns:
        Result signal object
    """
    data = src.get_masked_view().compressed()
    suffix = p.get_suffix(data)  # Also updates p.lower and p.upper

    # Compute histogram:
    y, bin_edges = np.histogram(data, bins=p.bins, range=(p.lower, p.upper))
    x = (bin_edges[:-1] + bin_edges[1:]) / 2

    # Note: we use the `new_signal_result` function to create the result signal object
    # because the `dst_1_to_1` would copy the source signal, which is not what we want
    # here (we want a brand new signal object).
    dst = new_signal_result(
        src,
        "histogram",
        suffix=suffix,
        units=(src.yunit, ""),
        labels=(src.ylabel, _("Counts")),
    )
    dst.set_xydata(x, y)
    dst.set_metadata_option("shade", 0.5)
    dst.set_metadata_option("curvestyle", "Steps")
    return dst


class PulseFeaturesParam(gds.DataSet, title=_("Pulse features")):
    """Pulse features parameters."""

    signal_shape = gds.ChoiceItem(
        _("Signal shape"),
        [
            (None, _("Auto")),
            ("step", _("Step")),
            ("square", _("Square")),
        ],
        default=None,
        help=_("Signal type: auto-detect, step, or square."),
    )
    xstartmin = gds.FloatItem(
        _("Start baseline min"),
        default=0.0,
        help=_("Lower X boundary for the start baseline"),
    )
    xstartmax = gds.FloatItem(
        _("Start baseline max"),
        default=0.0,
        help=_("Upper X boundary for the start baseline"),
    )
    xendmin = gds.FloatItem(
        _("End baseline min"),
        default=1.0,
        help=_("Lower X boundary for the end baseline"),
    )
    xendmax = gds.FloatItem(
        _("End baseline max"),
        default=1.0,
        help=_("Upper X boundary for the end baseline"),
    )
    reference_levels = gds.ChoiceItem(
        _("Rise/Fall time"),
        [
            ((5, 95), _("5% - 95% (High precision)")),
            ((10, 90), _("10% - 90% (IEEE standard)")),
            ((20, 80), _("20% - 80% (Noisy signals)")),
            ((25, 75), _("25% - 75% (Alternative)")),
        ],
        default=(10, 90),
        help=_(
            "Reference levels for rise/fall time measurement. "
            "10%-90% is the IEEE standard for digital signal analysis."
        ),
    )

    def update_from_obj(self, obj: SignalObj) -> None:
        """Update parameters from a signal object."""
        self.xstartmin, self.xstartmax = pulse.get_start_range(obj.x)
        self.xendmin, self.xendmax = pulse.get_end_range(obj.x)


@computation_function()
def extract_pulse_features(obj: SignalObj, p: PulseFeaturesParam) -> TableResult:
    """Extract pulse features.

    Args:
        obj: The signal object from which to extract features.
        p: The pulse features parameters.

    Returns:
        An object containing the pulse features.
    """
    start_ratio, stop_ratio = p.reference_levels

    def func_extract_pulse_features(xydata: tuple[np.ndarray, np.ndarray]):
        """Extract pulse features (internal function).

        Args:
            xydata: Tuple of (x, y) data arrays

        Returns:
            Pulse features dataclass
        """
        x, y = xydata

        # When processing ROI data, the start/end ranges from parameters might be
        # outside the ROI's x-range. In that case, use None to auto-detect ranges.
        start_range = [p.xstartmin, p.xstartmax]
        end_range = [p.xendmin, p.xendmax]

        # Check if ranges are within the data's x-range
        x_min, x_max = x.min(), x.max()
        if (
            start_range[0] < x_min
            or start_range[1] > x_max
            or end_range[0] < x_min
            or end_range[1] > x_max
        ):
            # Ranges are outside ROI bounds - use auto-detection
            start_range = None
            end_range = None

        return pulse.extract_pulse_features(
            x,
            y,
            signal_shape=p.signal_shape,
            start_range=start_range,
            end_range=end_range,
            start_ratio=start_ratio / 100.0,
            stop_ratio=stop_ratio / 100.0,
        )

    builder = TableResultBuilder(_("Pulse features"), kind=TableKind.PULSE_FEATURES)
    builder.set_global_function(func_extract_pulse_features)
    builder.hide_columns(
        ["xstartmin", "xstartmax", "xendmin", "xendmax", "xplateaumin", "xplateaumax"]
    )
    return builder.compute(obj)


@computation_function()
def sampling_rate_period(obj: SignalObj) -> TableResult:
    """Compute sampling rate and period
    using the following functions:

    - fs: :py:func:`sigima.tools.signal.dynamic.sampling_rate`
    - T: :py:func:`sigima.tools.signal.dynamic.sampling_period`

    Args:
        obj: source signal

    Returns:
        Result properties with sampling rate and period
    """
    table = TableResultBuilder(_("Sampling rate and period"))
    table.add(lambda xy: dynamic.sampling_rate(xy[0]), "fs")
    table.add(lambda xy: dynamic.sampling_period(xy[0]), "T")
    return table.compute(obj)


@computation_function()
def contrast(obj: SignalObj) -> TableResult:
    """Compute contrast with :py:func:`sigima.tools.signal.misc.contrast`"""
    table = TableResultBuilder(_("Contrast"))
    table.add(lambda xy: features.contrast(xy[1]), "contrast")
    return table.compute(obj)


@computation_function()
def x_at_minmax(obj: SignalObj) -> TableResult:
    """
    Compute the smallest argument at the minima and the smallest argument at the maxima.

    Args:
        obj: The signal object.

    Returns:
        An object containing the x-values at the minima and the maxima.
    """
    table = TableResultBuilder(_("X at min/max"))
    table.add(lambda xy: xy[0][np.argmin(xy[1])], "X@Ymin")
    table.add(lambda xy: xy[0][np.argmax(xy[1])], "X@Ymax")
    return table.compute(obj)