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)
|