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.
Files changed (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {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