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,489 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for signal operations
5
+ --------------------------------
6
+
7
+ Features from the "Operations" menu are covered by this test.
8
+ The "Operations" menu contains basic operations on signals, such as
9
+ addition, multiplication, division, and more.
10
+ """
11
+
12
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
13
+
14
+ from __future__ import annotations
15
+
16
+ import warnings
17
+
18
+ import numpy as np
19
+ import pytest
20
+
21
+ import sigima.objects
22
+ import sigima.params
23
+ import sigima.proc.signal
24
+ import sigima.tests.data
25
+ from sigima.enums import (
26
+ AngleUnit,
27
+ MathOperator,
28
+ NormalizationMethod,
29
+ SignalsToImageOrientation,
30
+ )
31
+ from sigima.objects.signal import SignalObj
32
+ from sigima.proc.base import AngleUnitParam
33
+ from sigima.proc.signal import complex_from_magnitude_phase, complex_from_real_imag
34
+ from sigima.tests.helpers import check_array_result
35
+ from sigima.tools.coordinates import polar_to_complex
36
+
37
+
38
+ def __create_two_signals() -> tuple[sigima.objects.SignalObj, sigima.objects.SignalObj]:
39
+ """Create two signals for testing."""
40
+ s1 = sigima.tests.data.create_periodic_signal(
41
+ sigima.objects.SignalTypes.COSINE, freq=50.0, size=100
42
+ )
43
+ s1.dy = 0.05 * np.ones_like(s1.y)
44
+ s2 = sigima.tests.data.create_periodic_signal(
45
+ sigima.objects.SignalTypes.SINE, freq=25.0, size=100
46
+ )
47
+ s2.dy = 0.8 * np.ones_like(s2.y)
48
+ return s1, s2
49
+
50
+
51
+ def __create_n_signals(n: int = 100) -> list[sigima.objects.SignalObj]:
52
+ """Create a list of `n` different signals for testing."""
53
+ signals = []
54
+ for i in range(n):
55
+ s = sigima.tests.data.create_periodic_signal(
56
+ sigima.objects.SignalTypes.COSINE,
57
+ freq=50.0 + i,
58
+ size=100,
59
+ a=(i + 1) * 0.1,
60
+ )
61
+ s.dy = 0.5 * np.ones_like(s.y)
62
+ signals.append(s)
63
+ return signals
64
+
65
+
66
+ def __create_one_signal_and_constant() -> tuple[
67
+ sigima.objects.SignalObj, sigima.params.ConstantParam
68
+ ]:
69
+ """Create one signal and a constant for testing."""
70
+ s1 = sigima.tests.data.create_periodic_signal(
71
+ sigima.objects.SignalTypes.COSINE, freq=50.0, size=100
72
+ )
73
+ s1.dy = 0.5 * np.ones_like(s1.y)
74
+ param = sigima.params.ConstantParam.create(value=-np.pi)
75
+ return s1, param
76
+
77
+
78
+ @pytest.mark.validation
79
+ def test_signal_addition() -> None:
80
+ """Signal addition test."""
81
+ slist = __create_n_signals()
82
+ n = len(slist)
83
+ s1 = sigima.proc.signal.addition(slist)
84
+ exp_y = np.zeros_like(s1.y)
85
+ for s in slist:
86
+ exp_y += s.y
87
+ check_array_result(f"Addition of {n} signals", s1.y, exp_y)
88
+ expected_dy = np.sqrt(sum(sig.dy**2 for sig in slist))
89
+ check_array_result("Addition error propagation", s1.dy, expected_dy)
90
+
91
+
92
+ @pytest.mark.validation
93
+ def test_signal_average() -> None:
94
+ """Signal average test."""
95
+ slist = __create_n_signals()
96
+ n = len(slist)
97
+ s1 = sigima.proc.signal.average(slist)
98
+ exp_y = np.zeros_like(s1.y)
99
+ for s in slist:
100
+ exp_y += s.y
101
+ exp_y /= n
102
+ check_array_result(f"Average of {n} signals", s1.y, exp_y)
103
+ expected_dy = np.sqrt(sum(s.dy**2 for s in slist)) / n
104
+ check_array_result("Average error propagation", s1.dy, expected_dy)
105
+
106
+
107
+ @pytest.mark.validation
108
+ def test_signal_standard_deviation() -> None:
109
+ """Signal standard deviation test."""
110
+ slist = __create_n_signals()
111
+ n = len(slist)
112
+ s1 = sigima.proc.signal.standard_deviation(slist)
113
+ exp = np.zeros_like(s1.y)
114
+ average = np.mean([s.y for s in slist], axis=0)
115
+ for s in slist:
116
+ exp += (s.y - average) ** 2
117
+ exp = np.sqrt(exp / n)
118
+ check_array_result(f"Standard Deviation of {n} signals", s1.y, exp)
119
+ # Add uncertainty to source signals:
120
+ for sig in slist:
121
+ sig.dy = np.abs(0.1 * sig.y) + 0.1
122
+ s2 = sigima.proc.signal.standard_deviation(slist)
123
+ expected_dy = exp / np.sqrt(2 * (n - 1))
124
+ check_array_result("Standard Deviation error propagation", s2.dy, expected_dy)
125
+
126
+
127
+ @pytest.mark.validation
128
+ def test_signal_product() -> None:
129
+ """Signal multiplication test."""
130
+ slist = __create_n_signals()
131
+ n = len(slist)
132
+ s1 = sigima.proc.signal.product(slist)
133
+ exp_y = np.ones_like(s1.y)
134
+ for s in slist:
135
+ exp_y *= s.y
136
+ check_array_result(f"Product of {n} signals", s1.y, exp_y)
137
+ expected_dy = np.abs(exp_y) * np.sqrt(sum((s.dy / s.y) ** 2 for s in slist))
138
+ check_array_result("Product error propagation", s1.dy, expected_dy)
139
+
140
+
141
+ @pytest.mark.validation
142
+ def test_signal_difference() -> None:
143
+ """Signal difference test."""
144
+ s1, s2 = __create_two_signals()
145
+ s3 = sigima.proc.signal.difference(s1, s2)
146
+ check_array_result("Signal difference", s3.y, s1.y - s2.y)
147
+ expected_dy = np.sqrt(s1.dy**2 + s2.dy**2)
148
+ check_array_result("Difference error propagation", s3.dy, expected_dy)
149
+
150
+
151
+ @pytest.mark.validation
152
+ def test_signal_quadratic_difference() -> None:
153
+ """Signal quadratic difference validation test."""
154
+ s1, s2 = __create_two_signals()
155
+ s3 = sigima.proc.signal.quadratic_difference(s1, s2)
156
+ check_array_result("Signal quadratic difference", s3.y, (s1.y - s2.y) / np.sqrt(2))
157
+
158
+
159
+ @pytest.mark.validation
160
+ def test_signal_division() -> None:
161
+ """Signal division test."""
162
+ s1, s2 = __create_two_signals()
163
+ s3 = sigima.proc.signal.division(s1, s2)
164
+ check_array_result("Signal division", s3.y, s1.y / s2.y)
165
+ expected_dy = np.abs(s1.y / s2.y) * np.sqrt(
166
+ (s1.dy / s1.y) ** 2 + (s2.dy / s2.y) ** 2
167
+ )
168
+ check_array_result("Division error propagation", s3.dy, expected_dy)
169
+
170
+
171
+ @pytest.mark.validation
172
+ def test_signal_addition_constant() -> None:
173
+ """Signal addition with constant test."""
174
+ s1, param = __create_one_signal_and_constant()
175
+ s2 = sigima.proc.signal.addition_constant(s1, param)
176
+ check_array_result("Signal addition with constant", s2.y, s1.y + param.value)
177
+ # Error should be unchanged after addition of a constant
178
+ check_array_result("Addition constant error propagation", s2.dy, s1.dy)
179
+
180
+
181
+ @pytest.mark.validation
182
+ def test_signal_product_constant() -> None:
183
+ """Signal multiplication by constant test."""
184
+ s1, param = __create_one_signal_and_constant()
185
+ s2 = sigima.proc.signal.product_constant(s1, param)
186
+ check_array_result("Signal multiplication by constant", s2.y, s1.y * param.value)
187
+ # Error is scaled by the absolute value of the constant
188
+ assert param.value is not None
189
+ expected_dy = np.abs(param.value) * s1.dy
190
+ check_array_result("Product constant error propagation", s2.dy, expected_dy)
191
+
192
+
193
+ @pytest.mark.validation
194
+ def test_signal_difference_constant() -> None:
195
+ """Signal difference with constant test."""
196
+ s1, param = __create_one_signal_and_constant()
197
+ s2 = sigima.proc.signal.difference_constant(s1, param)
198
+ check_array_result("Signal difference with constant", s2.y, s1.y - param.value)
199
+ # Error is unchanged after subtraction of a constant
200
+ check_array_result("Difference constant error propagation", s2.dy, s1.dy)
201
+
202
+
203
+ @pytest.mark.validation
204
+ def test_signal_division_constant() -> None:
205
+ """Signal division by constant test."""
206
+ s1, param = __create_one_signal_and_constant()
207
+ s2 = sigima.proc.signal.division_constant(s1, param)
208
+ check_array_result("Signal division by constant", s2.y, s1.y / param.value)
209
+ assert param.value is not None
210
+ expected_dy = s1.dy / np.abs(param.value)
211
+ check_array_result("Division constant error propagation", s2.dy, expected_dy)
212
+
213
+
214
+ @pytest.mark.validation
215
+ def test_signal_inverse() -> None:
216
+ """Signal inversion validation test."""
217
+ s1 = __create_two_signals()[0]
218
+ inv_signal = sigima.proc.signal.inverse(s1)
219
+ with warnings.catch_warnings():
220
+ warnings.simplefilter("ignore", category=RuntimeWarning)
221
+ exp_y = 1.0 / s1.y
222
+ exp_y[np.isinf(exp_y)] = np.nan
223
+ expected_dy = np.abs(exp_y) * s1.dy / np.abs(s1.y)
224
+ expected_dy[np.isinf(expected_dy)] = np.nan
225
+ check_array_result("Signal inverse", inv_signal.y, exp_y)
226
+ check_array_result("Inverse error propagation", inv_signal.dy, expected_dy)
227
+
228
+
229
+ @pytest.mark.validation
230
+ def test_signal_absolute() -> None:
231
+ """Absolute value validation test."""
232
+ s1 = __create_two_signals()[0]
233
+ abs_signal = sigima.proc.signal.absolute(s1)
234
+ check_array_result("Absolute value", abs_signal.y, np.abs(s1.y))
235
+
236
+
237
+ @pytest.mark.validation
238
+ def test_signal_real() -> None:
239
+ """Real part validation test."""
240
+ s1 = __create_two_signals()[0]
241
+ re_signal = sigima.proc.signal.real(s1)
242
+ check_array_result("Real part", re_signal.y, np.real(s1.y))
243
+
244
+
245
+ @pytest.mark.validation
246
+ def test_signal_imag() -> None:
247
+ """Imaginary part validation test."""
248
+ s1 = __create_two_signals()[0]
249
+ im_signal = sigima.proc.signal.imag(s1)
250
+ check_array_result("Imaginary part", im_signal.y, np.imag(s1.y))
251
+
252
+
253
+ @pytest.mark.validation
254
+ def test_signal_complex_from_real_imag() -> None:
255
+ """Test :py:func:`sigima.proc.signal.complex_from_real_imag`."""
256
+ x = np.linspace(0.0, 1.0, 5)
257
+ real = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
258
+ imag = np.array([10.0, 20.0, 30.0, 40.0, 50.0])
259
+ # Create SignalObj instances for real and imaginary parts
260
+ s_real = SignalObj("real")
261
+ s_real.set_xydata(x, real)
262
+ s_imag = SignalObj("imag")
263
+ s_imag.set_xydata(x, imag)
264
+ # Create complex signal from real and imaginary parts
265
+ result = complex_from_real_imag(s_real, s_imag)
266
+ check_array_result(
267
+ "complex_from_real_imag",
268
+ result.y,
269
+ real + 1j * imag,
270
+ )
271
+
272
+
273
+ @pytest.mark.validation
274
+ def test_signal_phase() -> None:
275
+ """Phase angle validation test."""
276
+ # Create a base signal and make it complex for testing
277
+ base_signal = __create_two_signals()[0]
278
+ y_complex = base_signal.y + 1j * base_signal.y[::-1]
279
+ complex_signal = sigima.objects.create_signal("complex", base_signal.x, y_complex)
280
+
281
+ # Test phase extraction in radians without unwrapping
282
+ param_rad = sigima.params.PhaseParam.create(unit=AngleUnit.RADIAN, unwrap=False)
283
+ result_rad = sigima.proc.signal.phase(complex_signal, param_rad)
284
+ check_array_result("Phase in radians", result_rad.y, np.angle(y_complex))
285
+
286
+ # Test phase extraction in degrees without unwrapping
287
+ param_deg = sigima.params.PhaseParam.create(unit=AngleUnit.DEGREE, unwrap=False)
288
+ result_deg = sigima.proc.signal.phase(complex_signal, param_deg)
289
+ check_array_result("Phase in degrees", result_deg.y, np.angle(y_complex, deg=True))
290
+
291
+ # Test phase extraction in radians with unwrapping
292
+ param_rad_unwrap = sigima.params.PhaseParam.create(
293
+ unit=AngleUnit.RADIAN, unwrap=True
294
+ )
295
+ result_rad_unwrap = sigima.proc.signal.phase(complex_signal, param_rad_unwrap)
296
+ check_array_result(
297
+ "Phase in radians with unwrapping",
298
+ result_rad_unwrap.y,
299
+ np.unwrap(np.angle(y_complex)),
300
+ )
301
+
302
+ # Test phase extraction in degrees with unwrapping
303
+ param_deg_unwrap = sigima.params.PhaseParam.create(
304
+ unit=AngleUnit.DEGREE, unwrap=True
305
+ )
306
+ result_deg_unwrap = sigima.proc.signal.phase(complex_signal, param_deg_unwrap)
307
+ check_array_result(
308
+ "Phase in degrees with unwrapping",
309
+ result_deg_unwrap.y,
310
+ np.unwrap(np.angle(y_complex, deg=True), period=360.0),
311
+ )
312
+
313
+
314
+ MAGNITUDE_PHASE_TEST_CASES = [
315
+ (np.array([0.0, np.pi / 2, np.pi, 3.0 * np.pi / 2.0, 0.0]), AngleUnit.RADIAN),
316
+ (np.array([0.0, 90.0, 180.0, 270.0, 0.0]), AngleUnit.DEGREE),
317
+ ]
318
+
319
+
320
+ @pytest.mark.parametrize("phase, unit", MAGNITUDE_PHASE_TEST_CASES)
321
+ @pytest.mark.validation
322
+ def test_signal_complex_from_magnitude_phase(
323
+ phase: np.ndarray, unit: AngleUnit
324
+ ) -> None:
325
+ """Test :py:func:`sigima.proc.signal.complex_from_magnitude_phase`.
326
+
327
+ Args:
328
+ phase (np.ndarray): Angles in radians or degrees.
329
+ unit (AngleUnit): Unit of the angles, either radian or degree.
330
+ """
331
+ x = np.linspace(0.0, 1.0, 5)
332
+ magnitude = np.array([2.0, 3.0, 4.0, 5.0, 6.0])
333
+ # Create signal instances for magnitude and phase
334
+ s_mag = SignalObj("magnitude")
335
+ s_mag.set_xydata(x, magnitude)
336
+ s_phase = SignalObj("phase")
337
+ s_phase.set_xydata(x, phase)
338
+ # Create complex signal from magnitude and phase
339
+ p = AngleUnitParam.create(unit=unit)
340
+ result = complex_from_magnitude_phase(s_mag, s_phase, p)
341
+ unit_str = "rad" if unit == AngleUnit.RADIAN else "°"
342
+ check_array_result(
343
+ f"complex_from_magnitude_phase_{unit_str}",
344
+ result.y,
345
+ polar_to_complex(magnitude, phase, unit=unit_str),
346
+ )
347
+
348
+
349
+ def __test_all_complex_from_magnitude_phase() -> None:
350
+ """Test all combinations of magnitude and phase."""
351
+ for phase, unit in MAGNITUDE_PHASE_TEST_CASES:
352
+ test_signal_complex_from_magnitude_phase(phase, unit)
353
+
354
+
355
+ @pytest.mark.validation
356
+ def test_signal_astype() -> None:
357
+ """Data type conversion validation test."""
358
+ s1 = __create_two_signals()[0]
359
+ for dtype_str in sigima.objects.SignalObj.get_valid_dtypenames():
360
+ p = sigima.params.DataTypeSParam.create(dtype_str=dtype_str)
361
+ astype_signal = sigima.proc.signal.astype(s1, p)
362
+ assert astype_signal.y.dtype == np.dtype(dtype_str)
363
+
364
+
365
+ @pytest.mark.validation
366
+ def test_signal_exp() -> None:
367
+ """Exponential validation test."""
368
+ s1 = __create_two_signals()[0]
369
+ exp_signal = sigima.proc.signal.exp(s1)
370
+ check_array_result("Exponential", exp_signal.y, np.exp(s1.y))
371
+
372
+
373
+ @pytest.mark.validation
374
+ def test_signal_log10() -> None:
375
+ """Logarithm base 10 validation test."""
376
+ s1 = __create_two_signals()[0]
377
+ log10_signal = sigima.proc.signal.log10(sigima.proc.signal.exp(s1))
378
+ check_array_result("Logarithm base 10", log10_signal.y, np.log10(np.exp(s1.y)))
379
+
380
+
381
+ @pytest.mark.validation
382
+ def test_signal_sqrt() -> None:
383
+ """Square root validation test."""
384
+ s1 = sigima.tests.data.get_test_signal("paracetamol.txt")
385
+ sqrt_signal = sigima.proc.signal.sqrt(s1)
386
+ check_array_result("Square root", sqrt_signal.y, np.sqrt(s1.y))
387
+
388
+
389
+ @pytest.mark.validation
390
+ def test_signal_power() -> None:
391
+ """Power validation test."""
392
+ s1 = sigima.tests.data.get_test_signal("paracetamol.txt")
393
+ p = sigima.params.PowerParam.create(power=2.0)
394
+ power_signal = sigima.proc.signal.power(s1, p)
395
+ check_array_result("Power", power_signal.y, s1.y**p.power)
396
+
397
+
398
+ @pytest.mark.validation
399
+ def test_signal_arithmetic() -> None:
400
+ """Arithmetic operations validation test."""
401
+ s1, s2 = __create_two_signals()
402
+ p = sigima.params.ArithmeticParam.create()
403
+ for operator in MathOperator:
404
+ p.operator = operator
405
+ for factor in (0.0, 1.0, 2.0):
406
+ p.factor = factor
407
+ for constant in (0.0, 1.0, 2.0):
408
+ p.constant = constant
409
+ s3 = sigima.proc.signal.arithmetic(s1, s2, p)
410
+ if operator == MathOperator.ADD:
411
+ exp = s1.y + s2.y
412
+ elif operator == MathOperator.MULTIPLY:
413
+ exp = s1.y * s2.y
414
+ elif operator == MathOperator.SUBTRACT:
415
+ exp = s1.y - s2.y
416
+ elif operator == MathOperator.DIVIDE:
417
+ exp = s1.y / s2.y
418
+ else:
419
+ raise ValueError(f"Unknown operator {operator}")
420
+ exp = exp * factor + constant
421
+ check_array_result(f"Arithmetic [{p.get_operation()}]", s3.y, exp)
422
+
423
+
424
+ @pytest.mark.validation
425
+ def test_signal_signals_to_image() -> None:
426
+ """Signals to image conversion test."""
427
+ # Create test signals
428
+ slist = __create_n_signals(n=5)
429
+ n = len(slist)
430
+ size = len(slist[0].y)
431
+
432
+ # Test without normalization, as rows
433
+ p = sigima.params.SignalsToImageParam()
434
+ p.orientation = SignalsToImageOrientation.ROWS
435
+ p.normalize = False
436
+ img = sigima.proc.signal.signals_to_image(slist, p)
437
+ assert img.data.shape == (n, size), (
438
+ f"Expected shape ({n}, {size}), got {img.data.shape}"
439
+ )
440
+ for i, sig in enumerate(slist):
441
+ title = f"Signals to image (rows) - signal {i}"
442
+ check_array_result(title, img.data[i], sig.y)
443
+
444
+ # Test without normalization, as columns
445
+ p.orientation = SignalsToImageOrientation.COLUMNS
446
+ img = sigima.proc.signal.signals_to_image(slist, p)
447
+ assert img.data.shape == (size, n), (
448
+ f"Expected shape ({size}, {n}), got {img.data.shape}"
449
+ )
450
+ for i, sig in enumerate(slist):
451
+ title = f"Signals to image (columns) - signal {i}"
452
+ check_array_result(title, img.data[:, i], sig.y)
453
+
454
+ # Test with normalization
455
+ p.normalize = True
456
+ p.normalize_method = NormalizationMethod.MAXIMUM
457
+ p.orientation = SignalsToImageOrientation.ROWS
458
+ img = sigima.proc.signal.signals_to_image(slist, p)
459
+ for i, sig in enumerate(slist):
460
+ expected = sig.y / np.max(np.abs(sig.y))
461
+ title = f"Signals to image (normalized rows) - signal {i}"
462
+ check_array_result(title, img.data[i], expected)
463
+
464
+
465
+ if __name__ == "__main__":
466
+ test_signal_addition()
467
+ test_signal_average()
468
+ test_signal_product()
469
+ test_signal_difference()
470
+ test_signal_quadratic_difference()
471
+ test_signal_division()
472
+ test_signal_addition_constant()
473
+ test_signal_product_constant()
474
+ test_signal_difference_constant()
475
+ test_signal_division_constant()
476
+ test_signal_inverse()
477
+ test_signal_absolute()
478
+ test_signal_real()
479
+ test_signal_imag()
480
+ test_signal_complex_from_real_imag()
481
+ test_signal_phase()
482
+ __test_all_complex_from_magnitude_phase()
483
+ test_signal_astype()
484
+ test_signal_exp()
485
+ test_signal_log10()
486
+ test_signal_sqrt()
487
+ test_signal_power()
488
+ test_signal_arithmetic()
489
+ test_signal_signals_to_image()
@@ -0,0 +1,145 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Peak detection unit tests
5
+ """
6
+
7
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
8
+ # pylint: disable=duplicate-code
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+ import pytest
14
+
15
+ import sigima.objects
16
+ import sigima.params
17
+ import sigima.proc.signal
18
+ from sigima.objects import GaussParam, SineParam, create_signal_from_param
19
+ from sigima.tests.data import create_paracetamol_signal
20
+ from sigima.tests.helpers import check_scalar_result
21
+
22
+
23
+ @pytest.mark.validation
24
+ def test_signal_peak_detection() -> None:
25
+ """Peak detection validation test."""
26
+ # Test 1: Use a known signal with multiple peaks (paracetamol spectrum)
27
+ src = create_paracetamol_signal()
28
+
29
+ # Create peak detection parameters
30
+ param = sigima.params.PeakDetectionParam.create(threshold=20, min_dist=5)
31
+
32
+ # Apply peak detection
33
+ dst = sigima.proc.signal.peak_detection(src, param)
34
+
35
+ # Check that we got some peaks
36
+ assert dst.y.size > 0, "Peak detection should find at least some peaks"
37
+ assert dst.x.size == dst.y.size, "X and Y arrays should have same size"
38
+
39
+ # Check that all detected peaks are from the original signal
40
+ assert np.all(dst.x >= src.x.min()) and np.all(dst.x <= src.x.max()), (
41
+ "All detected peak positions should be within the original signal range"
42
+ )
43
+
44
+ # Check that peak y-values are from the original signal
45
+ for i in range(dst.x.size):
46
+ # Find closest point in original signal
47
+ idx = np.argmin(np.abs(src.x - dst.x[i]))
48
+ expected_y = src.y[idx]
49
+ check_scalar_result(f"Peak {i} y-value", dst.y[i], expected_y, rtol=1e-10)
50
+
51
+ # Test 2: Synthetic signal with known peaks (multiple Gaussians)
52
+ # Create a signal with 3 well-separated Gaussian peaks
53
+ x = np.linspace(-10, 10, 1000)
54
+ y = (
55
+ np.exp(-((x + 4) ** 2) / 2) # Peak at x=-4
56
+ + 0.5 * np.exp(-((x - 0) ** 2) / 2) # Peak at x=0 (smaller)
57
+ + np.exp(-((x - 4) ** 2) / 2) # Peak at x=4
58
+ )
59
+ synthetic_signal = sigima.objects.create_signal("Multi-peak test", x, y)
60
+
61
+ # Detect peaks with appropriate parameters
62
+ param = sigima.params.PeakDetectionParam.create(threshold=40, min_dist=100)
63
+ dst_synthetic = sigima.proc.signal.peak_detection(synthetic_signal, param)
64
+
65
+ # Should detect exactly 3 peaks
66
+ assert dst_synthetic.x.size == 3, f"Expected 3 peaks, got {dst_synthetic.x.size}"
67
+
68
+ # Check peak positions (approximately)
69
+ expected_positions = np.array([-4.0, 0.0, 4.0])
70
+ detected_positions = np.sort(dst_synthetic.x)
71
+
72
+ for i, (detected, expected) in enumerate(
73
+ zip(detected_positions, expected_positions)
74
+ ):
75
+ check_scalar_result(f"Peak {i} position", detected, expected, atol=0.2)
76
+
77
+ # Test 3: Edge case - signal with minimal peaks
78
+ # Create a simple sinusoidal signal and use very restrictive parameters
79
+ param_simple = SineParam.create(size=100, xmin=0, xmax=10, freq=1, a=0.1)
80
+ simple_signal = create_signal_from_param(param_simple)
81
+
82
+ # Use a very high threshold to minimize peak detection
83
+ param = sigima.params.PeakDetectionParam.create(threshold=99, min_dist=1)
84
+ dst_minimal = sigima.proc.signal.peak_detection(simple_signal, param)
85
+
86
+ # With such a high threshold, few or no peaks should be detected
87
+ assert dst_minimal.x.size >= 0, "Peak count should be non-negative"
88
+ # If peaks are found, they should be within signal range
89
+ if dst_minimal.x.size > 0:
90
+ assert np.all(dst_minimal.x >= simple_signal.x.min())
91
+ assert np.all(dst_minimal.x <= simple_signal.x.max())
92
+
93
+ # Test 4: Single peak signal
94
+ param_single = GaussParam.create(size=200, xmin=-5, xmax=5, a=1, sigma=1, mu=0)
95
+ single_peak_signal = create_signal_from_param(param_single)
96
+
97
+ param = sigima.params.PeakDetectionParam.create(threshold=30, min_dist=10)
98
+ dst_single = sigima.proc.signal.peak_detection(single_peak_signal, param)
99
+
100
+ # Should detect exactly 1 peak
101
+ assert dst_single.x.size == 1, f"Expected 1 peak, got {dst_single.x.size}"
102
+
103
+ # Peak should be near x=0 (the center of the Gaussian)
104
+ check_scalar_result("Single peak position", dst_single.x[0], 0.0, atol=0.1)
105
+
106
+
107
+ def test_signal_peak_detection_parameters() -> None:
108
+ """Test peak detection with different parameter values."""
109
+ # Create a test signal with multiple peaks
110
+ param_signal = SineParam.create(size=500, xmin=0, xmax=10, freq=2, a=1)
111
+ test_signal = create_signal_from_param(param_signal)
112
+
113
+ # Test different threshold values
114
+ thresholds = [10, 30, 50, 70]
115
+ peak_counts = []
116
+
117
+ for threshold in thresholds:
118
+ param = sigima.params.PeakDetectionParam.create(threshold=threshold, min_dist=5)
119
+ result = sigima.proc.signal.peak_detection(test_signal, param)
120
+ peak_counts.append(result.x.size)
121
+
122
+ # Higher thresholds should generally detect fewer peaks
123
+ # (though this isn't guaranteed for all signals)
124
+ assert all(count >= 0 for count in peak_counts), (
125
+ "All peak counts should be non-negative"
126
+ )
127
+
128
+ # Test different minimum distances
129
+ min_distances = [1, 5, 10, 20]
130
+ param = sigima.params.PeakDetectionParam.create(threshold=30, min_dist=1)
131
+ result_ref = sigima.proc.signal.peak_detection(test_signal, param)
132
+
133
+ for min_dist in min_distances[1:]: # Skip the first one (reference)
134
+ param = sigima.params.PeakDetectionParam.create(threshold=30, min_dist=min_dist)
135
+ result = sigima.proc.signal.peak_detection(test_signal, param)
136
+
137
+ # Larger minimum distances should generally detect fewer or equal peaks
138
+ assert result.x.size <= result_ref.x.size, (
139
+ f"min_dist={min_dist} should not detect more peaks than min_dist=1"
140
+ )
141
+
142
+
143
+ if __name__ == "__main__":
144
+ test_signal_peak_detection()
145
+ test_signal_peak_detection_parameters()