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,176 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ DateTime support unit tests
5
+ ===========================
6
+
7
+ Unit tests for datetime functionality in SignalObj.
8
+ """
9
+
10
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import datetime, timedelta
15
+
16
+ import numpy as np
17
+ import pytest
18
+
19
+ from sigima.objects import create_signal
20
+ from sigima.objects.signal.constants import VALID_TIME_UNITS
21
+ from sigima.tests.env import execenv
22
+
23
+
24
+ def test_signal_datetime_methods() -> None:
25
+ """Test SignalObj datetime methods."""
26
+ execenv.print("Testing SignalObj datetime methods...")
27
+
28
+ # Create datetime data
29
+ base_time = datetime(2025, 10, 6, 10, 0, 0)
30
+ timestamps = [base_time + timedelta(seconds=i) for i in range(10)]
31
+ values = np.sin(np.arange(10) * 0.5)
32
+
33
+ format_str = "%Y-%m-%d %H:%M:%S"
34
+
35
+ # Test different units
36
+ for unit in VALID_TIME_UNITS:
37
+ # Create signal with initial data
38
+ signal = create_signal(
39
+ "Test Signal", x=np.arange(10, dtype=float), y=values.copy()
40
+ )
41
+
42
+ # Initially should not be datetime
43
+ assert not signal.is_x_datetime()
44
+
45
+ # Set x from datetime
46
+ signal.set_x_from_datetime(timestamps, unit=unit, format_str=format_str)
47
+
48
+ # Check datetime flag
49
+ assert signal.is_x_datetime()
50
+ assert signal.metadata["x_datetime"] is True
51
+ assert signal.xunit == unit
52
+ assert signal.metadata["x_datetime_format"] == format_str
53
+
54
+ # Check x data is float
55
+ assert isinstance(signal.x, np.ndarray)
56
+ assert signal.x.dtype in (np.float32, np.float64)
57
+
58
+ # Get x as datetime
59
+ dt_values = signal.get_x_as_datetime()
60
+ assert isinstance(dt_values, np.ndarray)
61
+ assert dt_values.dtype == np.dtype("datetime64[ns]")
62
+
63
+ # Verify y values are unchanged
64
+ assert np.allclose(signal.y, values)
65
+
66
+ execenv.print(" ✓ SignalObj datetime methods test passed")
67
+
68
+
69
+ def test_datetime_with_string_input() -> None:
70
+ """Test datetime conversion from string input."""
71
+ execenv.print("Testing datetime conversion from strings...")
72
+
73
+ signal = create_signal("String DateTime Test")
74
+
75
+ # Create datetime strings
76
+ date_strings = [
77
+ "2025-10-06 10:00:00",
78
+ "2025-10-06 10:00:01",
79
+ "2025-10-06 10:00:02",
80
+ ]
81
+ values = [1.0, 2.0, 3.0]
82
+
83
+ # Set from strings
84
+ signal.set_x_from_datetime(date_strings, unit="s")
85
+ signal.y = values
86
+
87
+ # Verify it worked
88
+ assert signal.is_x_datetime()
89
+ assert len(signal.x) == len(date_strings)
90
+
91
+ # Get back as datetime
92
+ dt_values = signal.get_x_as_datetime()
93
+ assert len(dt_values) == len(date_strings)
94
+
95
+ execenv.print(" ✓ String datetime conversion test passed")
96
+
97
+
98
+ def test_datetime_copy() -> None:
99
+ """Test that datetime metadata is preserved when copying signal."""
100
+ execenv.print("Testing datetime metadata preservation in copy...")
101
+
102
+ signal = create_signal("Original")
103
+ timestamps = [datetime(2025, 10, 6, 10, 0, i) for i in range(5)]
104
+ signal.set_x_from_datetime(timestamps, unit="ms")
105
+ signal.y = np.arange(5, dtype=float)
106
+
107
+ # Copy signal
108
+ signal_copy = signal.copy()
109
+
110
+ # Verify datetime metadata is preserved
111
+ assert signal_copy.is_x_datetime()
112
+ assert signal_copy.xunit == "ms"
113
+ assert np.array_equal(signal.x, signal_copy.x)
114
+
115
+ execenv.print(" ✓ Datetime metadata preservation test passed")
116
+
117
+
118
+ def test_datetime_non_datetime_signal() -> None:
119
+ """Test that non-datetime signals work correctly."""
120
+ execenv.print("Testing non-datetime signal behavior...")
121
+
122
+ signal = create_signal("Regular Signal", x=np.arange(10), y=np.sin(np.arange(10)))
123
+
124
+ # Should not be datetime
125
+ assert not signal.is_x_datetime()
126
+
127
+ # get_x_as_datetime should return regular x
128
+ x_data = signal.get_x_as_datetime()
129
+ assert np.array_equal(x_data, signal.x)
130
+
131
+ execenv.print(" ✓ Non-datetime signal test passed")
132
+
133
+
134
+ def test_datetime_invalid_unit() -> None:
135
+ """Test that invalid units raise appropriate errors."""
136
+ execenv.print("Testing invalid unit handling...")
137
+
138
+ timestamps = [datetime(2025, 10, 6, 10, 0, 0)]
139
+
140
+ # Test SignalObj.set_x_from_datetime with invalid unit
141
+ signal = create_signal("Test")
142
+ with pytest.raises(ValueError, match="Invalid unit"):
143
+ signal.set_x_from_datetime(timestamps, unit="invalid")
144
+
145
+ execenv.print(" ✓ Invalid unit handling test passed")
146
+
147
+
148
+ def test_datetime_arithmetic_operations() -> None:
149
+ """Test that datetime signals work with arithmetic operations."""
150
+ execenv.print("Testing datetime signal arithmetic...")
151
+
152
+ # Create two signals with datetime x
153
+ base_time = datetime(2025, 10, 6, 10, 0, 0)
154
+ timestamps = [base_time + timedelta(seconds=i) for i in range(10)]
155
+
156
+ signal1 = create_signal("Signal 1")
157
+ signal1.set_x_from_datetime(timestamps, unit="s")
158
+ signal1.y = np.arange(10, dtype=float)
159
+
160
+ signal2 = create_signal("Signal 2")
161
+ signal2.set_x_from_datetime(timestamps, unit="s")
162
+ signal2.y = np.arange(10, dtype=float) * 2
163
+
164
+ # The x data should be identical floats
165
+ assert np.array_equal(signal1.x, signal2.x)
166
+
167
+ # Verify we can do arithmetic on y
168
+ result_y = signal1.y + signal2.y
169
+ expected_y = np.arange(10, dtype=float) * 3
170
+ assert np.allclose(result_y, expected_y)
171
+
172
+ execenv.print(" ✓ Datetime signal arithmetic test passed")
173
+
174
+
175
+ if __name__ == "__main__":
176
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,303 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Signal FFT unit test."""
4
+
5
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
6
+ # pylint: disable=duplicate-code
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+ import pytest
12
+ import scipy.signal as sps
13
+
14
+ import sigima.objects
15
+ import sigima.params
16
+ import sigima.proc.signal
17
+ import sigima.tests.data
18
+ from sigima.enums import PadLocation1D
19
+ from sigima.tests import guiutils
20
+ from sigima.tests.data import get_test_signal
21
+ from sigima.tests.env import execenv
22
+ from sigima.tests.helpers import check_array_result, check_scalar_result
23
+ from sigima.tools.signal import fourier
24
+
25
+
26
+ @pytest.mark.validation
27
+ def test_signal_zero_padding() -> None:
28
+ """1D FFT zero padding validation test."""
29
+ s1 = sigima.tests.data.create_periodic_signal(
30
+ sigima.objects.SignalTypes.COSINE, freq=50.0, size=1000
31
+ )
32
+
33
+ # Validate padding length computation
34
+ for strategy, expected_length in (
35
+ ("next_pow2", 24),
36
+ ("double", 1000),
37
+ ("triple", 2000),
38
+ ):
39
+ param = sigima.params.ZeroPadding1DParam.create(strategy=strategy)
40
+ param.update_from_obj(s1)
41
+ assert param.n == expected_length, (
42
+ f"Wrong length for '{param.strategy}' strategy: {param.n}"
43
+ f" (expected {expected_length})"
44
+ )
45
+
46
+ # Validate zero padding
47
+ param = sigima.params.ZeroPadding1DParam.create(strategy="custom", n=250)
48
+ assert param.n is not None
49
+ for location in PadLocation1D:
50
+ execenv.print(f"Validating zero padding with location = {location.value}...")
51
+ param.location = location
52
+ param.update_from_obj(s1)
53
+ s2 = sigima.proc.signal.zero_padding(s1, param)
54
+ len1 = s1.y.size
55
+ n = param.n
56
+ exp_len2 = len1 + n
57
+ assert s2.y.size == exp_len2, f"Wrong length: {len(s2.y)} (expected {exp_len2})"
58
+ if location == PadLocation1D.APPEND:
59
+ dx = s1.x[1] - s1.x[0]
60
+ expected_x = np.pad(
61
+ s1.x,
62
+ (0, n),
63
+ mode="linear_ramp",
64
+ end_values=(s1.x[-1] + dx * n,),
65
+ )
66
+ check_array_result(f"{location.value}: Check x-data", s2.x, expected_x)
67
+ check_array_result(
68
+ f"{location.value}: Check original y-data", s2.y[:len1], s1.y
69
+ )
70
+ check_array_result(
71
+ f"{location.value}: Check padded y-data", s2.y[len1:], np.zeros(n)
72
+ )
73
+ elif location == PadLocation1D.PREPEND:
74
+ dx = s1.x[1] - s1.x[0]
75
+ expected_x = np.pad(
76
+ s1.x,
77
+ (n, 0),
78
+ mode="linear_ramp",
79
+ end_values=(s1.x[0] - dx * n,),
80
+ )
81
+ check_array_result(f"{location.value}: Check x-data", s2.x, expected_x)
82
+ check_array_result(
83
+ f"{location.value}: Check original y-data", s2.y[-len1:], s1.y
84
+ )
85
+ check_array_result(
86
+ f"{location.value}: Check padded y-data", s2.y[:n], np.zeros(n)
87
+ )
88
+ elif location == PadLocation1D.BOTH:
89
+ dx = s1.x[1] - s1.x[0]
90
+ expected_x = np.pad(
91
+ s1.x,
92
+ (n // 2, n - n // 2),
93
+ mode="linear_ramp",
94
+ end_values=(
95
+ s1.x[0] - dx * (n // 2),
96
+ s1.x[-1] + dx * (n - n // 2),
97
+ ),
98
+ )
99
+ check_array_result(f"{location.value}: Check x-data", s2.x, expected_x)
100
+ check_array_result(
101
+ f"{location.value}: Check original y-data",
102
+ s2.y[n // 2 : n // 2 + len1],
103
+ s1.y,
104
+ )
105
+ check_array_result(
106
+ f"{location.value}: Check padded y-data (before)",
107
+ s2.y[: n // 2],
108
+ np.zeros(n // 2),
109
+ )
110
+ check_array_result(
111
+ f"{location.value}: Check padded y-data (after)",
112
+ s2.y[-(n - n // 2) :],
113
+ np.zeros(n - n // 2),
114
+ )
115
+ execenv.print("OK")
116
+
117
+
118
+ @pytest.mark.validation
119
+ def test_signal_fft() -> None:
120
+ """1D FFT validation test."""
121
+ freq = 50.0
122
+ size = 10000
123
+
124
+ # See note in function `test_signal_ifft` below.
125
+ xmin = 0.0
126
+
127
+ s1 = sigima.tests.data.create_periodic_signal(
128
+ sigima.objects.SignalTypes.COSINE, freq=freq, size=size, xmin=xmin
129
+ )
130
+ fft = sigima.proc.signal.fft(s1)
131
+ ifft = sigima.proc.signal.ifft(fft)
132
+
133
+ # Check that the inverse FFT reconstructs the original signal.
134
+ check_array_result("Original and recovered x data", s1.y, ifft.y.real)
135
+ check_array_result("Original and recovered y data", s1.x, ifft.x.real)
136
+
137
+
138
+ @pytest.mark.validation
139
+ def test_signal_ifft() -> None:
140
+ """1D iFFT validation test.
141
+
142
+ Check that the original and reconstructed signals are equal.
143
+ """
144
+ param = sigima.objects.CosineParam.create(size=500)
145
+
146
+ # *** Note ***
147
+ #
148
+ # We set xmin to 0.0 to be able to compare the X data of the original and
149
+ # reconstructed signals, because the FFT do not preserve the X data (phase is
150
+ # lost, sampling rate is assumed to be constant), so that comparing the X data
151
+ # is not meaningful if xmin is different.
152
+ param.xmin = 0.0
153
+
154
+ s1 = sigima.objects.create_signal_from_param(param)
155
+ assert s1.xydata is not None
156
+ t1, y1 = s1.xydata
157
+ for shift in (True, False):
158
+ f1, sp1 = fourier.fft1d(t1, y1, shift=shift)
159
+ t2, y2 = fourier.ifft1d(f1, sp1)
160
+
161
+ execenv.print(
162
+ f"Comparing original and recovered signals for `shift={shift}`...",
163
+ end=" ",
164
+ )
165
+ check_array_result("Original and recovered x data", t2, t1, verbose=False)
166
+ check_array_result("Original and recovered y data", y2, y1, verbose=False)
167
+ execenv.print("OK")
168
+
169
+ guiutils.view_curves_if_gui(
170
+ [
171
+ s1,
172
+ sigima.objects.create_signal("Recovered", t2, y2),
173
+ sigima.objects.create_signal("Difference", t1, np.abs(y2 - y1)),
174
+ ]
175
+ )
176
+
177
+
178
+ @pytest.mark.validation
179
+ def test_signal_magnitude_spectrum() -> None:
180
+ """1D magnitude spectrum validation test."""
181
+ freq = 50.0
182
+ size = 10000
183
+
184
+ s1 = sigima.tests.data.create_periodic_signal(
185
+ sigima.objects.SignalTypes.COSINE, freq=freq, size=size
186
+ )
187
+ fft = sigima.proc.signal.fft(s1)
188
+ mag = sigima.proc.signal.magnitude_spectrum(s1)
189
+
190
+ # Check that the peak frequencies are correct.
191
+ ipk1 = np.argmax(mag.y[: size // 2])
192
+ ipk2 = np.argmax(mag.y[size // 2 :]) + size // 2
193
+ fpk1 = fft.x[ipk1]
194
+ fpk2 = fft.x[ipk2]
195
+ check_scalar_result("Frequency of the first peak", fpk1, -freq, rtol=1e-4)
196
+ check_scalar_result("Frequency of the second peak", fpk2, freq, rtol=1e-4)
197
+
198
+ # Check that magnitude spectrum is symmetric.
199
+ check_array_result("Symmetry of magnitude spectrum", mag.y[1::], mag.y[-1:0:-1])
200
+
201
+ # Check the magnitude of the peaks.
202
+ exp_mag = size / 2
203
+ check_scalar_result("Magnitude of the first peak", mag.y[ipk1], exp_mag, rtol=0.05)
204
+ check_scalar_result("Magnitude of the second peak", mag.y[ipk2], exp_mag, rtol=0.05)
205
+
206
+ # Check that the magnitude spectrum is correct.
207
+ check_array_result("Cosine signal magnitude spectrum X", mag.x, fft.x.real)
208
+ check_array_result("Cosine signal magnitude spectrum Y", mag.y, np.abs(fft.y))
209
+
210
+ guiutils.view_curves_if_gui(
211
+ [
212
+ sigima.objects.create_signal("FFT-real", fft.x.real, fft.x.real),
213
+ sigima.objects.create_signal("FFT-imag", fft.x.real, fft.y.imag),
214
+ sigima.objects.create_signal("FFT-magnitude", mag.x.real, mag.y),
215
+ ]
216
+ )
217
+
218
+
219
+ @pytest.mark.validation
220
+ def test_signal_phase_spectrum() -> None:
221
+ """1D phase spectrum validation test."""
222
+ freq = 50.0
223
+ size = 10000
224
+
225
+ s1 = sigima.tests.data.create_periodic_signal(
226
+ sigima.objects.SignalTypes.COSINE, freq=freq, size=size
227
+ )
228
+ fft = sigima.proc.signal.fft(s1)
229
+ phase = sigima.proc.signal.phase_spectrum(s1)
230
+
231
+ # Check that the phase spectrum is correct.
232
+ check_array_result("Cosine signal phase spectrum X", phase.x, fft.x.real)
233
+ exp_phase = np.rad2deg(np.angle(fft.y))
234
+ check_array_result("Cosine signal phase spectrum Y", phase.y, exp_phase)
235
+
236
+ guiutils.view_curves_if_gui(
237
+ [
238
+ sigima.objects.create_signal("FFT-real", fft.x.real, fft.x.real),
239
+ sigima.objects.create_signal("FFT-imag", fft.x.real, fft.y.imag),
240
+ sigima.objects.create_signal("Phase", phase.x.real, phase.y),
241
+ ]
242
+ )
243
+
244
+
245
+ @pytest.mark.validation
246
+ def test_signal_psd() -> None:
247
+ """1D Power Spectral Density validation test."""
248
+ freq = 50.0
249
+ size = 10000
250
+
251
+ s1 = sigima.tests.data.create_periodic_signal(
252
+ sigima.objects.SignalTypes.COSINE, freq=freq, size=size
253
+ )
254
+ param = sigima.params.SpectrumParam()
255
+ for decibel in (False, True):
256
+ param.decibel = decibel
257
+ psd = sigima.proc.signal.psd(s1, param)
258
+
259
+ # Check that the PSD is correct.
260
+ exp_x, exp_y = sps.welch(s1.y, fs=1.0 / (s1.x[1] - s1.x[0]))
261
+ if decibel:
262
+ exp_y = 10 * np.log10(exp_y)
263
+
264
+ fpk1 = psd.x[np.argmax(psd.y)]
265
+ check_scalar_result("Frequency of the maximum", fpk1, freq, rtol=2e-2)
266
+
267
+ check_array_result(f"Cosine signal PSD X (dB={decibel})", psd.x, exp_x)
268
+ check_array_result(f"Cosine signal PSD Y (dB={decibel})", psd.y, exp_y)
269
+
270
+ guiutils.view_curves_if_gui(
271
+ [
272
+ sigima.objects.create_signal("PSD", psd.x, psd.y),
273
+ ]
274
+ )
275
+
276
+
277
+ @pytest.mark.gui
278
+ def test_signal_spectrum() -> None:
279
+ """Test several FFT-related functions on `dynamic_parameters.txt`."""
280
+ with guiutils.lazy_qt_app_context(force=True):
281
+ # pylint: disable=import-outside-toplevel
282
+ from sigima.tests.vistools import view_curves
283
+
284
+ sig = get_test_signal("dynamic_parameters.txt")
285
+ view_curves([sig])
286
+ p = sigima.params.SpectrumParam.create(decibel=True)
287
+ ms = sigima.proc.signal.magnitude_spectrum(sig, p)
288
+ view_curves([ms], title="Magnitude spectrum")
289
+ ps = sigima.proc.signal.phase_spectrum(sig)
290
+ view_curves([ps], title="Phase spectrum")
291
+ psd = sigima.proc.signal.psd(sig, p)
292
+ view_curves([psd], title="Power spectral density")
293
+
294
+
295
+ if __name__ == "__main__":
296
+ guiutils.enable_gui()
297
+ test_signal_zero_padding()
298
+ test_signal_fft()
299
+ test_signal_ifft()
300
+ test_signal_magnitude_spectrum()
301
+ test_signal_phase_spectrum()
302
+ test_signal_psd()
303
+ test_signal_spectrum()