sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
.. Dynamic Parameters (see parent package :mod:`sigima.tools.signal`)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import scipy.optimize
|
|
16
|
+
|
|
17
|
+
from sigima.enums import PowerUnit
|
|
18
|
+
from sigima.tools.checks import check_1d_arrays
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sinusoidal_model(
|
|
22
|
+
x: np.ndarray, a: float, f: float, phi: float, offset: float
|
|
23
|
+
) -> np.ndarray:
|
|
24
|
+
"""Sinusoidal model function."""
|
|
25
|
+
return a * np.sin(2 * np.pi * f * x + phi) + offset
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
29
|
+
def sinusoidal_fit(
|
|
30
|
+
x: np.ndarray, y: np.ndarray
|
|
31
|
+
) -> tuple[tuple[float, float, float, float], float]:
|
|
32
|
+
"""Fit a sinusoidal model to the input data.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
x: X data
|
|
36
|
+
y: Y data
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A tuple containing the fit parameters (amplitude, frequency, phase, offset)
|
|
40
|
+
and the residuals
|
|
41
|
+
"""
|
|
42
|
+
# Initial guess for the parameters
|
|
43
|
+
# ==================================================================================
|
|
44
|
+
offset = np.mean(y)
|
|
45
|
+
amp = (np.max(y) - np.min(y)) / 2
|
|
46
|
+
phase_origin = 0
|
|
47
|
+
# Search for the maximum of the FFT
|
|
48
|
+
i_maxfft = np.argmax(np.abs(np.fft.fft(y - offset)))
|
|
49
|
+
if i_maxfft > len(x) / 2:
|
|
50
|
+
# If the index is greater than N/2, we are in the mirrored half spectrum
|
|
51
|
+
# (negative frequencies)
|
|
52
|
+
i_maxfft = len(x) - i_maxfft
|
|
53
|
+
freq = i_maxfft / (x[-1] - x[0])
|
|
54
|
+
# ==================================================================================
|
|
55
|
+
|
|
56
|
+
def optfunc(fitparams: np.ndarray, x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
57
|
+
"""Optimization function."""
|
|
58
|
+
return y - sinusoidal_model(x, *fitparams)
|
|
59
|
+
|
|
60
|
+
# Fit the model to the data
|
|
61
|
+
fitparams = scipy.optimize.leastsq(
|
|
62
|
+
optfunc, [amp, freq, phase_origin, offset], args=(x, y)
|
|
63
|
+
)[0]
|
|
64
|
+
y_th = sinusoidal_model(x, *fitparams)
|
|
65
|
+
residuals = np.std(y - y_th)
|
|
66
|
+
return fitparams, residuals
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
70
|
+
def sinus_frequency(x: np.ndarray, y: np.ndarray) -> float:
|
|
71
|
+
"""Compute the frequency of a sinusoidal signal.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
x: x signal data
|
|
75
|
+
y: y signal data
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Frequency of the sinusoidal signal
|
|
79
|
+
"""
|
|
80
|
+
fitparams, _residuals = sinusoidal_fit(x, y)
|
|
81
|
+
return fitparams[1]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
85
|
+
def enob(x: np.ndarray, y: np.ndarray, full_scale: float = 1.0) -> float:
|
|
86
|
+
"""Compute Effective Number of Bits (ENOB).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
x: x signal data
|
|
90
|
+
y: y signal data
|
|
91
|
+
full_scale: Full scale(V). Defaults to 1.0.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Effective Number of Bits (ENOB)
|
|
95
|
+
"""
|
|
96
|
+
_fitparams, residuals = sinusoidal_fit(x, y)
|
|
97
|
+
return -np.log2(residuals * np.sqrt(12) / full_scale)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
101
|
+
def sinad(
|
|
102
|
+
x: np.ndarray,
|
|
103
|
+
y: np.ndarray,
|
|
104
|
+
full_scale: float = 1.0,
|
|
105
|
+
unit: PowerUnit = PowerUnit.DBC,
|
|
106
|
+
) -> float:
|
|
107
|
+
"""Compute Signal-to-Noise and Distortion Ratio (SINAD).
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
x: x signal data
|
|
111
|
+
y: y signal data
|
|
112
|
+
full_scale: Full scale(V). Defaults to 1.0.
|
|
113
|
+
unit: Unit of the input data. Defaults to PowerUnit.DBC.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Signal-to-Noise and Distortion Ratio (SINAD)
|
|
117
|
+
"""
|
|
118
|
+
fitparams, residuals = sinusoidal_fit(x, y)
|
|
119
|
+
amp = fitparams[0]
|
|
120
|
+
|
|
121
|
+
# Compute the power of the fundamental
|
|
122
|
+
if unit == PowerUnit.DBC:
|
|
123
|
+
powf = np.abs(amp / np.sqrt(2))
|
|
124
|
+
else:
|
|
125
|
+
powf = full_scale / (2 * np.sqrt(2))
|
|
126
|
+
|
|
127
|
+
return 20 * np.log10(powf / residuals)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
131
|
+
def thd(
|
|
132
|
+
x: np.ndarray,
|
|
133
|
+
y: np.ndarray,
|
|
134
|
+
full_scale: float = 1.0,
|
|
135
|
+
unit: PowerUnit = PowerUnit.DBC,
|
|
136
|
+
nb_harm: int = 5,
|
|
137
|
+
) -> float:
|
|
138
|
+
"""Compute Total Harmonic Distortion (THD).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
x: x signal data
|
|
142
|
+
y: y signal data
|
|
143
|
+
full_scale: Full scale(V). Defaults to 1.0.
|
|
144
|
+
unit: Unit of the input data. Defaults to PowerUnit.DBC.
|
|
145
|
+
nb_harm: Number of harmonics to consider. Defaults to 5.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Total Harmonic Distortion (THD)
|
|
149
|
+
"""
|
|
150
|
+
fitparams, _residuals = sinusoidal_fit(x, y)
|
|
151
|
+
offset = np.mean(y)
|
|
152
|
+
amp, freq = fitparams[:2]
|
|
153
|
+
ampfft = np.abs(np.fft.fft(y - offset))
|
|
154
|
+
|
|
155
|
+
# Compute the power of the fundamental
|
|
156
|
+
if unit == PowerUnit.DBC:
|
|
157
|
+
powfund = np.max(ampfft[: len(ampfft) // 2])
|
|
158
|
+
else:
|
|
159
|
+
powfund = (full_scale / (2 * np.sqrt(2))) * (len(x) / np.sqrt(2))
|
|
160
|
+
|
|
161
|
+
sumharm = 0
|
|
162
|
+
for i in np.arange(nb_harm + 2)[2:]:
|
|
163
|
+
a = i * np.ceil(freq * (x[-1] - x[0]))
|
|
164
|
+
amp = ampfft[int(a - 5) : int(a + 5)]
|
|
165
|
+
if len(amp) > 0:
|
|
166
|
+
sumharm += np.max(amp)
|
|
167
|
+
return 20 * np.log10(sumharm / powfund)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
171
|
+
def sfdr(
|
|
172
|
+
x: np.ndarray,
|
|
173
|
+
y: np.ndarray,
|
|
174
|
+
full_scale: float = 1.0,
|
|
175
|
+
unit: PowerUnit = PowerUnit.DBC,
|
|
176
|
+
) -> float:
|
|
177
|
+
"""Compute Spurious-Free Dynamic Range (SFDR).
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
x: x signal data
|
|
181
|
+
y: y signal data
|
|
182
|
+
full_scale: Full scale(V). Defaults to 1.0.
|
|
183
|
+
unit: Unit of the input data. Defaults to PowerUnit.DBC.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Spurious-Free Dynamic Range (SFDR)
|
|
187
|
+
"""
|
|
188
|
+
fitparams, _residuals = sinusoidal_fit(x, y)
|
|
189
|
+
|
|
190
|
+
# Compute the power of the fundamental
|
|
191
|
+
if unit == PowerUnit.DBC:
|
|
192
|
+
powfund = np.max(np.abs(np.fft.fft(y)))
|
|
193
|
+
else:
|
|
194
|
+
powfund = (full_scale / (2 * np.sqrt(2))) * (len(x) / np.sqrt(2))
|
|
195
|
+
|
|
196
|
+
maxspike = np.max(np.abs(np.fft.fft(y - sinusoidal_model(x, *fitparams))))
|
|
197
|
+
return 20 * np.log10(powfund / maxspike)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@check_1d_arrays(x_evenly_spaced=True, x_sorted=True)
|
|
201
|
+
def snr(
|
|
202
|
+
x: np.ndarray,
|
|
203
|
+
y: np.ndarray,
|
|
204
|
+
full_scale: float = 1.0,
|
|
205
|
+
unit: PowerUnit = PowerUnit.DBC,
|
|
206
|
+
) -> float:
|
|
207
|
+
"""Compute Signal-to-Noise Ratio (SNR).
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
x: x signal data
|
|
211
|
+
y: y signal data
|
|
212
|
+
full_scale: Full scale(V). Defaults to 1.0.
|
|
213
|
+
unit: Unit of the input data. Defaults to PowerUnit.DBC.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Signal-to-Noise Ratio (SNR)
|
|
217
|
+
"""
|
|
218
|
+
fitparams, _residuals = sinusoidal_fit(x, y)
|
|
219
|
+
|
|
220
|
+
# Compute the power of the fundamental
|
|
221
|
+
if unit == PowerUnit.DBC:
|
|
222
|
+
powfund = np.max(np.abs(np.fft.fft(y)))
|
|
223
|
+
else:
|
|
224
|
+
powfund = (full_scale / (2 * np.sqrt(2))) * (len(x) / np.sqrt(2))
|
|
225
|
+
|
|
226
|
+
noise = np.sqrt(np.mean((y - sinusoidal_model(x, *fitparams)) ** 2))
|
|
227
|
+
return 20 * np.log10(powfund / noise)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def sampling_period(x: np.ndarray) -> float:
|
|
231
|
+
"""Compute sampling period
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
x: X data
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Sampling period
|
|
238
|
+
"""
|
|
239
|
+
steps = np.diff(x)
|
|
240
|
+
if not np.isclose(np.diff(steps).max(), 0, atol=1e-10):
|
|
241
|
+
warnings.warn("Non-constant sampling signal")
|
|
242
|
+
return steps[0]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def sampling_rate(x: np.ndarray) -> float:
|
|
246
|
+
"""Compute mean sampling rate
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
x: X data
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Sampling rate
|
|
253
|
+
"""
|
|
254
|
+
return 1.0 / sampling_period(x)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
.. Features (see parent package :mod:`sigima.algorithms.signal`)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from sigima.tools.checks import check_1d_array, check_1d_arrays
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@check_1d_array(min_size=2, finite_only=True)
|
|
15
|
+
def find_zero_crossings(y: np.ndarray) -> np.ndarray:
|
|
16
|
+
"""Find the left indices of the zero-crossing intervals in the given array.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
y: Input array.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
An array of indices where zero-crossings occur.
|
|
23
|
+
"""
|
|
24
|
+
return np.nonzero(np.diff(np.sign(y)))[0]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@check_1d_arrays(x_sorted=True)
|
|
28
|
+
def find_x_axis_crossings(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
29
|
+
"""Find the :math:`x_n` values where :math:`y = f(x)` intercepts the x-axis.
|
|
30
|
+
|
|
31
|
+
This function uses zero-crossing detection and interpolation to find the x values
|
|
32
|
+
where :math:`y = 0`.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
x: X data.
|
|
36
|
+
y: Y data.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Array of x-intercepts. The array is empty if no intercept is found.
|
|
40
|
+
"""
|
|
41
|
+
# Find zero crossings.
|
|
42
|
+
xi_before = find_zero_crossings(y)
|
|
43
|
+
if len(xi_before) == 0:
|
|
44
|
+
return np.array([])
|
|
45
|
+
# Interpolate to find x values at zero crossings.
|
|
46
|
+
xi_after = xi_before + 1
|
|
47
|
+
slope = (y[xi_after] - y[xi_before]) / (x[xi_after] - x[xi_before])
|
|
48
|
+
with np.errstate(divide="ignore"):
|
|
49
|
+
x0 = -y[xi_before] / slope + x[xi_before]
|
|
50
|
+
x0 = np.where(np.isfinite(x0), x0, (x[xi_before] + x[xi_after]) / 2)
|
|
51
|
+
# mask = ~np.isfinite(x0)
|
|
52
|
+
# x0[mask] = xi_before[mask]
|
|
53
|
+
return x0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@check_1d_arrays(x_min_size=2, x_finite_only=True, x_sorted=True)
|
|
57
|
+
def find_y_at_x_value(x: np.ndarray, y: np.ndarray, x_target: float) -> float:
|
|
58
|
+
"""Return the y value at a specified x value using linear interpolation.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
x: X data.
|
|
62
|
+
y: Y data.
|
|
63
|
+
x_target: Input x value.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Interpolated y value at x_target, or `nan` if input value is not within the
|
|
67
|
+
interpolation range.
|
|
68
|
+
"""
|
|
69
|
+
if np.isnan(x_target):
|
|
70
|
+
return np.nan
|
|
71
|
+
return float(np.interp(x_target, x, y, left=np.nan, right=np.nan))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@check_1d_arrays
|
|
75
|
+
def find_x_values_at_y(x: np.ndarray, y: np.ndarray, y_target: float) -> np.ndarray:
|
|
76
|
+
"""Find all x values where :math:`y = f(x)` equals the value :math:`y_target`.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
x: X data.
|
|
80
|
+
y: Y data.
|
|
81
|
+
y_target: Target value.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Array of x values where :math:`y = f(x)` equals :math:`y_target`.
|
|
85
|
+
"""
|
|
86
|
+
return find_x_axis_crossings(x, y - y_target)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@check_1d_arrays(x_evenly_spaced=True)
|
|
90
|
+
def find_bandwidth_coordinates(
|
|
91
|
+
x: np.ndarray, y: np.ndarray, threshold: float = -3.0
|
|
92
|
+
) -> tuple[float, float, float, float] | None:
|
|
93
|
+
"""Compute the bandwidth of the signal at a given threshold relative to the maximum.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
x: X data.
|
|
97
|
+
y: Y data.
|
|
98
|
+
threshold: Threshold in decibel (relative to the maximum) at which the bandwidth
|
|
99
|
+
is computed. Defaults to -3.0 dB.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Segment coordinates of the bandwidth of the signal at the given threshold.
|
|
103
|
+
Returns None if the bandwidth cannot be determined.
|
|
104
|
+
"""
|
|
105
|
+
level: float = np.max(y) + threshold
|
|
106
|
+
crossings = find_x_values_at_y(x, y, level)
|
|
107
|
+
if len(crossings) == 1:
|
|
108
|
+
# One crossing: 1) baseband bandwidth if max is above crossing
|
|
109
|
+
# 2) passband bandwidth if max is below crossing
|
|
110
|
+
if x[np.argmax(y)] < crossings[0]: # Baseband bandwidth
|
|
111
|
+
coords = (0.0, level, crossings[0], level)
|
|
112
|
+
else:
|
|
113
|
+
coords = (crossings[0], level, x[-1], level)
|
|
114
|
+
elif len(crossings) == 2: # Passband bandwidth
|
|
115
|
+
# Two crossings: 1) passband bandwidth if max is above both crossings
|
|
116
|
+
# 2) no bandwidth if max is below both crossings
|
|
117
|
+
# 3) baseband bandwidth if max is between crossings
|
|
118
|
+
coords = (crossings[0], level, crossings[1], level)
|
|
119
|
+
else:
|
|
120
|
+
# No crossing or more than two crossings: cannot determine bandwidth
|
|
121
|
+
return None
|
|
122
|
+
return coords
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def contrast(y: np.ndarray) -> float:
|
|
126
|
+
"""Compute contrast
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
y: Input array
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Contrast
|
|
133
|
+
"""
|
|
134
|
+
max_, min_ = np.max(y), np.min(y)
|
|
135
|
+
return (max_ - min_) / (max_ + min_)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
.. Filtering functions (see parent package :mod:`sigima.tools.signal`).
|
|
5
|
+
|
|
6
|
+
This module provides denoising and filtering tools, such as Savitzky-Golay.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import dataclasses
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import scipy.signal # type: ignore[import]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass
|
|
19
|
+
class SimilarityResult:
|
|
20
|
+
"""Result of signal similarity validation."""
|
|
21
|
+
|
|
22
|
+
ok: bool
|
|
23
|
+
rel_dc_diff: float
|
|
24
|
+
corr: float
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def signal_similarity(
|
|
28
|
+
y: np.ndarray,
|
|
29
|
+
y_filtered: np.ndarray,
|
|
30
|
+
max_dc_diff: float = 1e-2,
|
|
31
|
+
min_corr: float = 0.99,
|
|
32
|
+
) -> SimilarityResult:
|
|
33
|
+
"""Check global similarity between two signals.
|
|
34
|
+
|
|
35
|
+
Criteria:
|
|
36
|
+
- DC level (mean value) must not drift more than ``max_dc_diff`` (relative).
|
|
37
|
+
- Correlation (cosine similarity) must stay above ``min_corr``.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
y: Original 1D signal.
|
|
41
|
+
y_filtered: Filtered 1D signal (same length as ``y``).
|
|
42
|
+
max_dc_diff: Maximum allowed relative change in mean value.
|
|
43
|
+
min_corr: Minimum allowed correlation between signals.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A result object containing the similarity metrics.
|
|
47
|
+
"""
|
|
48
|
+
if y.size != y_filtered.size:
|
|
49
|
+
raise ValueError("Signals must have the same length.")
|
|
50
|
+
|
|
51
|
+
# DC level
|
|
52
|
+
dc_orig = float(np.mean(y))
|
|
53
|
+
dc_filt = float(np.mean(y_filtered))
|
|
54
|
+
rel_diff = abs(dc_filt - dc_orig) / (abs(dc_orig) + 1e-12)
|
|
55
|
+
|
|
56
|
+
# Correlation (cosine similarity)
|
|
57
|
+
num = float(np.dot(y, y_filtered))
|
|
58
|
+
denom = float(np.linalg.norm(y) * np.linalg.norm(y_filtered) + 1e-12)
|
|
59
|
+
corr = num / denom
|
|
60
|
+
|
|
61
|
+
ok = (rel_diff <= max_dc_diff) and (corr >= min_corr)
|
|
62
|
+
|
|
63
|
+
return SimilarityResult(ok=ok, rel_dc_diff=rel_diff, corr=corr)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def savgol_filter(
|
|
67
|
+
y: np.ndarray, window_length: int = 11, polyorder: int = 3, mode: str = "interp"
|
|
68
|
+
) -> np.ndarray:
|
|
69
|
+
"""Smooth a 1D signal using the Savitzky-Golay filter.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
y: Input signal values.
|
|
73
|
+
window_length: Length of the filter window (must be odd and > polyorder).
|
|
74
|
+
polyorder: Order of the polynomial used to fit the samples.
|
|
75
|
+
mode: Padding mode passed to ``scipy.signal.savgol_filter``.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Smoothed signal values.
|
|
79
|
+
"""
|
|
80
|
+
if window_length % 2 == 0:
|
|
81
|
+
raise ValueError("window_length must be odd.")
|
|
82
|
+
if window_length <= polyorder:
|
|
83
|
+
raise ValueError("window_length must be greater than polyorder.")
|
|
84
|
+
|
|
85
|
+
y_smooth = scipy.signal.savgol_filter(y, window_length, polyorder, mode=mode)
|
|
86
|
+
return y_smooth
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def choose_savgol_window_auto(
|
|
90
|
+
y: np.ndarray,
|
|
91
|
+
target_reduction: float = 0.3,
|
|
92
|
+
polyorder: int = 3,
|
|
93
|
+
min_len: int = 5,
|
|
94
|
+
max_len: int = 101,
|
|
95
|
+
) -> int:
|
|
96
|
+
"""Choose the smallest Savitzky-Golay window that sufficiently reduces noise.
|
|
97
|
+
|
|
98
|
+
Strategy: measure noise on first differences of y, then
|
|
99
|
+
increase the window until noise is reduced by ``target_reduction``.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
y: 1D signal values.
|
|
103
|
+
target_reduction: Desired reduction factor in diff-std (e.g. 0.3 → ÷3).
|
|
104
|
+
polyorder: Polynomial order.
|
|
105
|
+
min_len: Minimum allowed window length.
|
|
106
|
+
max_len: Maximum allowed window length.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Odd integer window length.
|
|
110
|
+
"""
|
|
111
|
+
diffs = np.diff(y)
|
|
112
|
+
sigma0 = np.median(np.abs(diffs - np.median(diffs))) / 0.6745
|
|
113
|
+
|
|
114
|
+
for win in range(min_len | 1, max_len + 1, 2): # odd lengths
|
|
115
|
+
if win <= polyorder:
|
|
116
|
+
continue
|
|
117
|
+
y_smooth = scipy.signal.savgol_filter(y, win, polyorder)
|
|
118
|
+
sigma = (
|
|
119
|
+
np.median(np.abs(np.diff(y_smooth) - np.median(np.diff(y_smooth)))) / 0.6745
|
|
120
|
+
)
|
|
121
|
+
if sigma <= target_reduction * sigma0:
|
|
122
|
+
return win
|
|
123
|
+
|
|
124
|
+
return max_len | 1 # fallback
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def denoise_preserve_shape(
|
|
128
|
+
y: np.ndarray,
|
|
129
|
+
polyorder: int = 3,
|
|
130
|
+
target_reduction: float = 0.3,
|
|
131
|
+
max_dc_diff: float = 1e-2,
|
|
132
|
+
min_corr: float = 0.99,
|
|
133
|
+
min_len: int = 5,
|
|
134
|
+
max_len: int = 101,
|
|
135
|
+
) -> tuple[np.ndarray, SimilarityResult]:
|
|
136
|
+
"""Denoise a signal while preserving slow variations.
|
|
137
|
+
|
|
138
|
+
Strategy:
|
|
139
|
+
1. Estimate noise on first differences.
|
|
140
|
+
2. Choose the smallest Savitzky-Golay window that reduces noise
|
|
141
|
+
by at least ``target_reduction``.
|
|
142
|
+
3. Apply the filter.
|
|
143
|
+
4. Check similarity with the original signal (DC and correlation).
|
|
144
|
+
5. Return filtered signal if ok, otherwise return original.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
y: Input signal values.
|
|
148
|
+
polyorder: Polynomial order of Savitzky-Golay filter.
|
|
149
|
+
target_reduction: Desired noise reduction factor (0.3 → ÷3).
|
|
150
|
+
max_dc_diff: Maximum allowed relative change in mean value.
|
|
151
|
+
min_corr: Minimum allowed correlation between signals.
|
|
152
|
+
min_len: Minimum window length.
|
|
153
|
+
max_len: Maximum window length.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A tuple ``(y_denoised, result)`` where ``y_denoised`` is either the
|
|
157
|
+
filtered signal or the original if similarity criteria are not met, and
|
|
158
|
+
``result`` contains the details of the similarity check.
|
|
159
|
+
"""
|
|
160
|
+
win = choose_savgol_window_auto(
|
|
161
|
+
y,
|
|
162
|
+
target_reduction=target_reduction,
|
|
163
|
+
polyorder=polyorder,
|
|
164
|
+
min_len=min_len,
|
|
165
|
+
max_len=max_len,
|
|
166
|
+
)
|
|
167
|
+
y_smooth = savgol_filter(y, window_length=win, polyorder=polyorder, mode="interp")
|
|
168
|
+
result = signal_similarity(y, y_smooth, max_dc_diff=max_dc_diff, min_corr=min_corr)
|
|
169
|
+
if not result.ok:
|
|
170
|
+
y_smooth = y
|
|
171
|
+
return y_smooth, result
|