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,392 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ ROI advanced unit tests
5
+ """
6
+
7
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
8
+ # guitest: show
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.tests.data import create_paracetamol_signal
19
+ from sigima.tests.helpers import print_obj_data_dimensions
20
+
21
+ SIZE = 200
22
+
23
+
24
+ def __create_test_signal() -> sigima.objects.SignalObj:
25
+ """Create a test signal."""
26
+ return create_paracetamol_signal(size=SIZE)
27
+
28
+
29
+ def test_signal_roi_merge() -> None:
30
+ """Test signal ROI merge"""
31
+ # Create a signal object with a single ROI, and another one with another ROI.
32
+ # Compute the average of the two objects, and check if the resulting object
33
+ # has the expected ROI (i.e. the union of the original object's ROI).
34
+ obj1 = __create_test_signal()
35
+ obj2 = __create_test_signal()
36
+ obj2.roi = sigima.objects.create_signal_roi([60, 120], indices=True)
37
+ obj1.roi = sigima.objects.create_signal_roi([50, 100], indices=True)
38
+
39
+ # Compute the average of the two objects
40
+ obj3 = sigima.proc.signal.average([obj1, obj2])
41
+ assert obj3.roi is not None, "Merged object should have a ROI"
42
+ assert len(obj3.roi) == 2, "Merged object should have two single ROIs"
43
+ for single_roi in obj3.roi:
44
+ assert single_roi.get_indices_coords(obj3) in ([50, 100], [60, 120]), (
45
+ "Merged object should have the union of the original object's ROIs"
46
+ )
47
+
48
+
49
+ def test_signal_roi_combine() -> None:
50
+ """Test `SignalROI.combine_with` method"""
51
+ coords1, coords2 = [60, 120], [50, 100]
52
+ roi1 = sigima.objects.create_signal_roi(coords1, indices=True)
53
+ roi2 = sigima.objects.create_signal_roi(coords2, indices=True)
54
+ exp_combined = sigima.objects.create_signal_roi([coords1, coords2], indices=True)
55
+ # Check that combining two ROIs results in a new ROI with both coordinates:
56
+ roi3 = roi1.combine_with(roi2)
57
+ assert roi3 == exp_combined, "Combined ROI should match expected"
58
+ # Check that combining again with the same ROI does not change it:
59
+ roi3 = roi1.combine_with(roi2)
60
+ assert roi3 == exp_combined, "Combining with the same ROI should not change it"
61
+ # Check that combining with an image ROI raises an error:
62
+ with pytest.raises(
63
+ TypeError, match=r"Cannot combine([\S ]*)SignalROI([\S ]*)ImageROI"
64
+ ):
65
+ roi1.combine_with(sigima.objects.create_image_roi("rectangle", [0, 0, 10, 10]))
66
+
67
+
68
+ # Signal ROIs:
69
+ SROI1 = [26, 41]
70
+ SROI2 = [125, 146]
71
+
72
+
73
+ def __roi_str(obj: sigima.objects.SignalObj) -> str:
74
+ """Return a string representation of a SignalROI object for context."""
75
+ if obj.roi is None:
76
+ return "None"
77
+ if obj.roi.is_empty():
78
+ return "Empty"
79
+ return ", ".join(
80
+ f"[{r.get_indices_coords(obj)[0]}, {r.get_indices_coords(obj)[1]}]"
81
+ for r in obj.roi.single_rois
82
+ )
83
+
84
+
85
+ def __create_test_roi() -> sigima.objects.SignalROI:
86
+ """Create a test ROI."""
87
+ return sigima.objects.create_signal_roi([SROI1, SROI2], indices=True)
88
+
89
+
90
+ def __test_processing_in_roi(src: sigima.objects.SignalObj) -> None:
91
+ """Run signal processing in ROI.
92
+
93
+ Args:
94
+ src: The source signal object (with or without ROI)
95
+ """
96
+ print_obj_data_dimensions(src)
97
+ value = 1
98
+ p = sigima.params.ConstantParam.create(value=value)
99
+ dst = sigima.proc.signal.addition_constant(src, p)
100
+ orig = src.data
101
+ new = dst.data
102
+ context = f" [ROI: {__roi_str(src)}]"
103
+ if src.roi is not None and not src.roi.is_empty():
104
+ # Check if the processed data is correct: signal should be the same as the
105
+ # original data outside the ROI, and should be different inside the ROI.
106
+ assert not np.any(new[SROI1[0] : SROI1[1]] == orig[SROI1[0] : SROI1[1]]), (
107
+ f"Signal ROI 1 data mismatch{context}"
108
+ )
109
+ assert not np.any(new[SROI2[0] : SROI2[1]] == orig[SROI2[0] : SROI2[1]]), (
110
+ f"Signal ROI 2 data mismatch{context}"
111
+ )
112
+ assert np.all(new[: SROI1[0]] == orig[: SROI1[0]]), (
113
+ f"Signal before ROI 1 data mismatch{context}"
114
+ )
115
+ assert np.all(new[SROI1[1] : SROI2[0]] == orig[SROI1[1] : SROI2[0]]), (
116
+ f"Signal between ROIs data mismatch{context}"
117
+ )
118
+ assert np.all(new[SROI2[1] :] == orig[SROI2[1] :]), (
119
+ f"Signal after ROI 2 data mismatch{context}"
120
+ )
121
+ else:
122
+ # No ROI: all data should be changed
123
+ assert np.all(new == orig + value), f"Signal data mismatch{context}"
124
+
125
+
126
+ def test_signal_roi_processing() -> None:
127
+ """Test signal ROI processing"""
128
+ src = __create_test_signal()
129
+ base_roi = __create_test_roi()
130
+ empty_roi = sigima.objects.SignalROI()
131
+ for roi in (None, empty_roi, base_roi):
132
+ src.roi = roi
133
+ __test_processing_in_roi(src)
134
+
135
+
136
+ def test_empty_signal_roi() -> None:
137
+ """Test empty signal ROI"""
138
+ src = __create_test_signal()
139
+ empty_roi = sigima.objects.SignalROI()
140
+ for roi in (None, empty_roi):
141
+ src.roi = roi
142
+ context = f" [ROI: {__roi_str(src)}]"
143
+ assert src.roi is None or src.roi.is_empty(), (
144
+ f"Source object ROI should be empty or None{context}"
145
+ )
146
+ if src.roi is not None:
147
+ # No ROI has been set in the source signal
148
+ sig1 = sigima.proc.signal.extract_roi(src, src.roi.to_params(src))
149
+ assert sig1.data.size == 0, f"Extracted signal should be empty{context}"
150
+
151
+
152
+ @pytest.mark.validation
153
+ def test_signal_extract_rois() -> None:
154
+ """Validation test for signal ROI extraction into a single object"""
155
+ src = __create_test_signal()
156
+ src.roi = __create_test_roi()
157
+ context = f" [ROI: {__roi_str(src)}]"
158
+ size_roi1, size_roi2 = SROI1[1] - SROI1[0], SROI2[1] - SROI2[0]
159
+ assert len(src.roi) == 2, f"Source object should have two ROIs{context}"
160
+ # Single object mode: merge all ROIs into a single object
161
+ sig1 = sigima.proc.signal.extract_rois(src, src.roi.to_params(src))
162
+ assert sig1.data.size == size_roi1 + size_roi2, f"Signal size mismatch{context}"
163
+ assert np.all(sig1.data[:size_roi1] == src.data[SROI1[0] : SROI1[1]]), (
164
+ f"Signal 1 data mismatch{context}"
165
+ )
166
+ assert np.all(sig1.data[size_roi1:] == src.data[SROI2[0] : SROI2[1]]), (
167
+ f"Signal 2 data mismatch{context}"
168
+ )
169
+
170
+
171
+ @pytest.mark.validation
172
+ def test_signal_extract_roi() -> None:
173
+ """Validation test for signal ROI extraction into multiple objects"""
174
+ src = __create_test_signal()
175
+ src.roi = __create_test_roi()
176
+ context = f" [ROI: {__roi_str(src)}]"
177
+ size_roi1, size_roi2 = SROI1[1] - SROI1[0], SROI2[1] - SROI2[0]
178
+ assert len(src.roi) == 2, f"Source object should have two ROIs{context}"
179
+ # Multiple objects mode: extract each ROI as a separate object
180
+ signals: list[sigima.objects.SignalObj] = []
181
+ for index, single_roi in enumerate(src.roi):
182
+ roiparam = single_roi.to_param(src, index)
183
+ signal = sigima.proc.signal.extract_roi(src, roiparam)
184
+ signals.append(signal)
185
+ assert len(signals) == len(src.roi), (
186
+ f"Number of extracted signals mismatch{context}"
187
+ )
188
+ assert signals[0].data.size == size_roi1, f"Signal 1 size mismatch{context}"
189
+ assert signals[1].data.size == size_roi2, f"Signal 2 size mismatch{context}"
190
+ assert np.all(signals[0].data == src.data[SROI1[0] : SROI1[1]]), (
191
+ f"Signal 1 data mismatch{context}"
192
+ )
193
+ assert np.all(signals[1].data == src.data[SROI2[0] : SROI2[1]]), (
194
+ f"Signal 2 data mismatch{context}"
195
+ )
196
+
197
+
198
+ def test_signal_roi_union() -> None:
199
+ """Test signal ROI union operation"""
200
+ # Test union of overlapping ROIs
201
+ roi1 = sigima.objects.create_signal_roi([[10, 30], [20, 40]], indices=True)
202
+ roi_union = roi1.union()
203
+ assert len(roi_union) == 1, "Overlapping ROIs should merge into one"
204
+ assert np.array_equal(roi_union.single_rois[0].coords, [10, 40]), (
205
+ "Union should span full range"
206
+ )
207
+
208
+ # Test union of non-overlapping ROIs
209
+ roi2 = sigima.objects.create_signal_roi([[10, 20], [30, 40]], indices=True)
210
+ roi_union2 = roi2.union()
211
+ assert len(roi_union2) == 2, "Non-overlapping ROIs should remain separate"
212
+
213
+ # Test union of adjacent ROIs
214
+ roi3 = sigima.objects.create_signal_roi([[10, 20], [20, 30]], indices=True)
215
+ roi_union3 = roi3.union()
216
+ assert len(roi_union3) == 1, "Adjacent ROIs should merge"
217
+ assert np.array_equal(roi_union3.single_rois[0].coords, [10, 30]), (
218
+ "Adjacent union should span full range"
219
+ )
220
+
221
+ # Test empty ROI union
222
+ empty_roi = sigima.objects.SignalROI()
223
+ empty_union = empty_roi.union()
224
+ assert len(empty_union) == 0, "Empty ROI union should be empty"
225
+
226
+
227
+ def test_signal_roi_clipping() -> None:
228
+ """Test signal ROI clipping operation"""
229
+ src = __create_test_signal()
230
+ x_min, x_max = src.x[0], src.x[-1]
231
+
232
+ # Test clipping ROI within signal range
233
+ roi = sigima.objects.create_signal_roi([[x_min + 10, x_max - 10]], indices=False)
234
+ original_coords = roi.single_rois[0].coords.copy()
235
+ clipped_roi = roi.clipped(x_min, x_max)
236
+ assert len(clipped_roi) == 1, "ROI within range should remain"
237
+ assert np.array_equal(clipped_roi.single_rois[0].coords, original_coords), (
238
+ "ROI within range should be unchanged"
239
+ )
240
+
241
+ # Test clipping ROI partially outside signal range
242
+ roi2 = sigima.objects.create_signal_roi(
243
+ [[x_min - 5, x_min + 10], [x_max - 10, x_max + 5]], indices=False
244
+ )
245
+ clipped_roi2 = roi2.clipped(x_min, x_max)
246
+ assert len(clipped_roi2) == 2, "Partially outside ROIs should be clipped"
247
+ assert clipped_roi2.single_rois[0].coords[0] == x_min, (
248
+ "Left boundary should be clipped to x_min"
249
+ )
250
+ assert clipped_roi2.single_rois[1].coords[1] == x_max, (
251
+ "Right boundary should be clipped to x_max"
252
+ )
253
+
254
+ # Test clipping ROI completely outside signal range
255
+ roi3 = sigima.objects.create_signal_roi([[x_max + 1, x_max + 10]], indices=False)
256
+ clipped_roi3 = roi3.clipped(x_min, x_max)
257
+ assert len(clipped_roi3) == 0, "ROI completely outside range should be removed"
258
+
259
+
260
+ def test_signal_roi_inversion() -> None:
261
+ """Test signal ROI inversion operation"""
262
+ src = __create_test_signal()
263
+ x_min, x_max = src.x[0], src.x[-1]
264
+
265
+ # Test inversion of single ROI in middle
266
+ roi = sigima.objects.create_signal_roi([[20, 30]], indices=False)
267
+ inverted = roi.inverted(x_min, x_max)
268
+ assert len(inverted) == 2, "Single middle ROI should create two inverted segments"
269
+ assert inverted.single_rois[0].coords[0] == x_min, (
270
+ "First segment should start at x_min"
271
+ )
272
+ assert inverted.single_rois[0].coords[1] == 20, (
273
+ "First segment should end at ROI start"
274
+ )
275
+ assert inverted.single_rois[1].coords[0] == 30, (
276
+ "Second segment should start at ROI end"
277
+ )
278
+ assert inverted.single_rois[1].coords[1] == x_max, (
279
+ "Second segment should end at x_max"
280
+ )
281
+
282
+ # Test inversion of ROI at signal start
283
+ roi_start = sigima.objects.create_signal_roi([[x_min, 20]], indices=False)
284
+ inverted_start = roi_start.inverted(x_min, x_max)
285
+ assert len(inverted_start) == 1, "ROI at start should create one inverted segment"
286
+ assert inverted_start.single_rois[0].coords[0] == 20, (
287
+ "Inverted segment should start after ROI"
288
+ )
289
+ assert inverted_start.single_rois[0].coords[1] == x_max, (
290
+ "Inverted segment should end at x_max"
291
+ )
292
+
293
+ # Test inversion of ROI at signal end
294
+ roi_end = sigima.objects.create_signal_roi([[30, x_max]], indices=False)
295
+ inverted_end = roi_end.inverted(x_min, x_max)
296
+ assert len(inverted_end) == 1, "ROI at end should create one inverted segment"
297
+ assert inverted_end.single_rois[0].coords[0] == x_min, (
298
+ "Inverted segment should start at x_min"
299
+ )
300
+ assert inverted_end.single_rois[0].coords[1] == 30, (
301
+ "Inverted segment should end before ROI"
302
+ )
303
+
304
+ # Test inversion of multiple ROIs
305
+ roi_multi = sigima.objects.create_signal_roi([[10, 15], [20, 30]], indices=False)
306
+ inverted_multi = roi_multi.inverted(x_min, x_max)
307
+ assert len(inverted_multi) == 3, "Two ROIs should create three inverted segments"
308
+
309
+ # Test error case: empty ROI inversion
310
+ empty_roi = sigima.objects.SignalROI()
311
+ with pytest.raises(ValueError, match="No ROIs defined, cannot invert"):
312
+ empty_roi.inverted(x_min, x_max)
313
+
314
+
315
+ def test_signal_roi_mask() -> None:
316
+ """Test signal ROI mask creation"""
317
+ src = __create_test_signal()
318
+
319
+ # Test mask for single ROI
320
+ roi = sigima.objects.create_signal_roi([SROI1], indices=True)
321
+ mask = roi.to_mask(src)
322
+ assert mask.shape == src.xydata.shape, "Mask should have same shape as data"
323
+ assert mask.dtype == bool, "Mask should be boolean array"
324
+ # Check that ROI region is masked (False values)
325
+ assert not np.any(mask[:, SROI1[0] : SROI1[1]]), "ROI region should be masked"
326
+ # Check that non-ROI regions are not masked (True values)
327
+ assert np.all(mask[:, : SROI1[0]]), "Region before ROI should not be masked"
328
+ assert np.all(mask[:, SROI1[1] :]), "Region after ROI should not be masked"
329
+
330
+ # Test mask for multiple ROIs
331
+ roi_multi = __create_test_roi()
332
+ mask_multi = roi_multi.to_mask(src)
333
+ assert not np.any(mask_multi[:, SROI1[0] : SROI1[1]]), (
334
+ "First ROI region should be masked"
335
+ )
336
+ assert not np.any(mask_multi[:, SROI2[0] : SROI2[1]]), (
337
+ "Second ROI region should be masked"
338
+ )
339
+ assert np.all(mask_multi[:, SROI1[1] : SROI2[0]]), (
340
+ "Region between ROIs should not be masked"
341
+ )
342
+
343
+ # Test mask for empty ROI
344
+ empty_roi = sigima.objects.SignalROI()
345
+ empty_mask = empty_roi.to_mask(src)
346
+ assert not np.any(empty_mask), "Empty ROI should mask everything"
347
+
348
+
349
+ def test_signal_roi_operations_edge_cases() -> None:
350
+ """Test edge cases for signal ROI operations"""
351
+ src = __create_test_signal()
352
+ x_min, x_max = src.x[0], src.x[-1]
353
+
354
+ # Test union with identical ROIs
355
+ roi_identical = sigima.objects.create_signal_roi([[10, 20], [10, 20]], indices=True)
356
+ union_identical = roi_identical.union()
357
+ assert len(union_identical) == 1, "Identical ROIs should merge to one"
358
+
359
+ # Test clipping with ROI exactly at boundaries
360
+ roi_boundary = sigima.objects.create_signal_roi([[x_min, x_max]], indices=False)
361
+ roi_boundary.clipped(x_min, x_max)
362
+ assert len(roi_boundary) == 1, "ROI at exact boundaries should remain"
363
+ assert np.array_equal(roi_boundary.single_rois[0].coords, [x_min, x_max]), (
364
+ "Boundary ROI should be unchanged"
365
+ )
366
+
367
+ # Test inversion with ROI covering entire signal
368
+ roi_full = sigima.objects.create_signal_roi([[x_min, x_max]], indices=False)
369
+ inverted_full = roi_full.inverted(x_min, x_max)
370
+ assert len(inverted_full) == 0, "Full signal ROI should invert to empty"
371
+
372
+ # Test operations with very small ROIs
373
+ small_roi = sigima.objects.create_signal_roi([[10, 10.1]], indices=False)
374
+ small_union = small_roi.union()
375
+ assert len(small_union) == 1, "Small ROI should remain in union"
376
+
377
+ small_roi.clipped(0, 100)
378
+ assert len(small_roi) == 1, "Small ROI within range should remain after clipping"
379
+
380
+
381
+ if __name__ == "__main__":
382
+ test_signal_roi_merge()
383
+ test_signal_roi_combine()
384
+ test_signal_roi_processing()
385
+ test_empty_signal_roi()
386
+ test_signal_extract_rois()
387
+ test_signal_extract_roi()
388
+ test_signal_roi_union()
389
+ test_signal_roi_clipping()
390
+ test_signal_roi_inversion()
391
+ test_signal_roi_mask()
392
+ test_signal_roi_operations_edge_cases()