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,403 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Frequency filters 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.enums
16
+ import sigima.proc.signal
17
+ from sigima.objects.signal import SignalObj, create_signal
18
+ from sigima.tests import guiutils
19
+ from sigima.tests.helpers import check_array_result, check_scalar_result
20
+ from sigima.tools.signal.fourier import brickwall_filter
21
+
22
+
23
+ def build_clean_noisy_signals(
24
+ length: int = 2**15,
25
+ freq: int | float | np.ndarray = 1,
26
+ noise_level: float = 0.2,
27
+ ) -> tuple[SignalObj, SignalObj]:
28
+ """Create a test 1D signal + high-freq noise.
29
+
30
+ Args:
31
+ length: Length of the signal.
32
+ freq: Frequency of the sine wave, can be a single value or an array of
33
+ frequencies
34
+ noise_level: Standard deviation of the Gaussian noise to be added.
35
+
36
+ Returns:
37
+ Tuple of (clean_signal, noisy_signal) where:
38
+ - clean_signal: The clean sine wave signal.
39
+ - noisy_signal: The noisy signal with added Gaussian noise.
40
+ """
41
+ x = np.linspace(0, 1, length)
42
+ if np.isscalar(freq):
43
+ y_clean = np.sin(2 * np.pi * freq * x)
44
+ else:
45
+ freq = np.asarray(freq)
46
+ y_clean = np.sum([np.sin(2 * np.pi * f * x) for f in freq], axis=0)
47
+ rng = np.random.default_rng(seed=0)
48
+ y_noisy = y_clean + noise_level * rng.standard_normal(size=length)
49
+ noisy = create_signal("noisy signal", x, y_noisy)
50
+ clean = create_signal("clean signal", x, y_clean)
51
+ return clean, noisy
52
+
53
+
54
+ def _validate_scipy_filter_output(
55
+ result_signal: SignalObj,
56
+ method: sigima.enums.FrequencyFilterMethod,
57
+ filter_type: str,
58
+ original_signal: SignalObj | None = None,
59
+ ) -> bool:
60
+ """Validate scipy filter output for basic functionality.
61
+
62
+ Args:
63
+ result_signal: The filtered signal to validate
64
+ method: The filter method used
65
+ filter_type: Type of filter (lowpass, highpass, etc.) for messages
66
+ original_signal: Original signal for variance comparison (optional)
67
+
68
+ Returns:
69
+ True if validation passed, False if should skip this filter
70
+ """
71
+ # Check that output is finite
72
+ if not np.all(np.isfinite(result_signal.y)):
73
+ print(
74
+ f"⚠ {method.value} {filter_type} filter: produced non-finite "
75
+ "values, skipping"
76
+ )
77
+ return False
78
+
79
+ # Check that output has reasonable magnitude
80
+ max_magnitude = 100 if filter_type in ["highpass", "bandstop", "bandpass"] else 10
81
+ if not np.max(np.abs(result_signal.y)) < max_magnitude:
82
+ print(
83
+ f"⚠ {method.value} {filter_type} filter: produced excessively "
84
+ "large values, skipping"
85
+ )
86
+ return False
87
+
88
+ # For lowpass, check that variance didn't increase too much
89
+ if filter_type == "lowpass" and original_signal is not None:
90
+ original_var = np.var(original_signal.y)
91
+ filtered_var = np.var(result_signal.y)
92
+ if filtered_var > original_var * 2:
93
+ print(
94
+ f"⚠ {method.value} {filter_type} filter: increased variance "
95
+ "too much, skipping"
96
+ )
97
+ return False
98
+
99
+ print(f"✓ {method.value} {filter_type} filter: working correctly")
100
+ return True
101
+
102
+
103
+ def _test_filter_method(
104
+ filter_func,
105
+ param_class,
106
+ method: sigima.enums.FrequencyFilterMethod,
107
+ filter_type: str,
108
+ test_signal: SignalObj,
109
+ expected_signal: SignalObj | None = None,
110
+ tolerance: float | None = None,
111
+ original_signal: SignalObj | None = None,
112
+ **filter_params,
113
+ ) -> None:
114
+ """Test a single filter method with given parameters.
115
+
116
+ Args:
117
+ filter_func: The filter function to call (lowpass, highpass, etc.)
118
+ param_class: The parameter class for the filter
119
+ method: The filter method to test
120
+ filter_type: Type of filter for validation messages
121
+ test_signal: Signal to filter
122
+ expected_signal: Expected result for comparison (None for basic validation)
123
+ tolerance: Tolerance for comparison (None for basic validation)
124
+ original_signal: Original signal for variance checks
125
+ **filter_params: Additional parameters for the filter
126
+ """
127
+ for zero_padding in (True, False):
128
+ param = param_class.create(
129
+ method=method, zero_padding=zero_padding, **filter_params
130
+ )
131
+
132
+ prefix = f"{method.value} {filter_type} (zero_padding={zero_padding}) "
133
+
134
+ # Store original x data to verify it's not modified
135
+ original_x = test_signal.x.copy()
136
+
137
+ result_signal: SignalObj = filter_func(test_signal, param)
138
+ guiutils.view_curves_if_gui([expected_signal or test_signal, result_signal])
139
+
140
+ # CRITICAL: Check that the input signal's X data was NOT modified
141
+ check_array_result(
142
+ f"{prefix} input X data unchanged", test_signal.x, original_x
143
+ )
144
+
145
+ if expected_signal is not None:
146
+ # Check that X data is unchanged
147
+ check_array_result(
148
+ f"{prefix} X data check", result_signal.x, expected_signal.x
149
+ )
150
+
151
+ # Validate based on whether we have expected results
152
+ if expected_signal is not None and tolerance is not None:
153
+ # Detailed comparison for brickwall filters
154
+ if filter_type == "highpass":
155
+ # Special case for highpass: check mean is close to zero
156
+ check_scalar_result(
157
+ f"{prefix} removes low freq",
158
+ float(np.mean(result_signal.y)),
159
+ 0,
160
+ atol=tolerance,
161
+ )
162
+ else:
163
+ # Array comparison for other filters
164
+ check_array_result(
165
+ f"{prefix}",
166
+ result_signal.y[10 : len(result_signal.y) - 10],
167
+ expected_signal.y[10 : len(expected_signal.y) - 10],
168
+ atol=tolerance,
169
+ )
170
+ elif not zero_padding:
171
+ # Basic validation for scipy filters
172
+ _validate_scipy_filter_output(
173
+ result_signal, method, filter_type, original_signal
174
+ )
175
+
176
+
177
+ @pytest.mark.validation
178
+ def test_signal_lowpass() -> None:
179
+ """Validation test for frequency filtering."""
180
+ clean, noisy = build_clean_noisy_signals()
181
+
182
+ # Test all filter methods
183
+ for method in sigima.enums.FrequencyFilterMethod:
184
+ if method == sigima.enums.FrequencyFilterMethod.BRICKWALL:
185
+ # For brickwall, we expect very close match to the clean signal
186
+ _test_filter_method(
187
+ filter_func=sigima.proc.signal.lowpass,
188
+ param_class=sigima.proc.signal.LowPassFilterParam,
189
+ method=method,
190
+ filter_type="lowpass",
191
+ test_signal=noisy,
192
+ expected_signal=clean,
193
+ tolerance=0.15,
194
+ cut0=2.0,
195
+ )
196
+ else:
197
+ # For scipy filters, just check basic functionality
198
+ _test_filter_method(
199
+ filter_func=sigima.proc.signal.lowpass,
200
+ param_class=sigima.proc.signal.LowPassFilterParam,
201
+ method=method,
202
+ filter_type="lowpass",
203
+ test_signal=noisy,
204
+ original_signal=noisy,
205
+ cut0=5000.0,
206
+ )
207
+
208
+
209
+ @pytest.mark.validation
210
+ def test_signal_highpass() -> None:
211
+ """Validation test for highpass frequency filtering."""
212
+ noise_level = 0.2
213
+ clean, noisy = build_clean_noisy_signals(noise_level=noise_level)
214
+
215
+ # Test all filter methods
216
+ for method in sigima.enums.FrequencyFilterMethod:
217
+ if method == sigima.enums.FrequencyFilterMethod.BRICKWALL:
218
+ # For brickwall, the highpass should remove the low freq signal
219
+ # and leave mostly noise (mean should be close to zero)
220
+ mean_variance = np.sqrt(noise_level / len(clean.x))
221
+ expected_err = 3 * mean_variance
222
+
223
+ # Create a dummy expected signal with zero mean for validation
224
+ expected_signal = create_signal("zero", clean.x, np.zeros_like(clean.y))
225
+
226
+ _test_filter_method(
227
+ filter_func=sigima.proc.signal.highpass,
228
+ param_class=sigima.proc.signal.HighPassFilterParam,
229
+ method=method,
230
+ filter_type="highpass",
231
+ test_signal=noisy,
232
+ expected_signal=expected_signal,
233
+ tolerance=expected_err,
234
+ cut0=2.0,
235
+ )
236
+ else:
237
+ # For scipy filters, use higher cutoff and basic validation
238
+ _test_filter_method(
239
+ filter_func=sigima.proc.signal.highpass,
240
+ param_class=sigima.proc.signal.HighPassFilterParam,
241
+ method=method,
242
+ filter_type="highpass",
243
+ test_signal=noisy,
244
+ cut0=1000.0,
245
+ )
246
+
247
+
248
+ @pytest.mark.validation
249
+ def test_signal_bandstop() -> None:
250
+ """Validation test for stopband frequency filtering."""
251
+ # Test all filter methods
252
+ for method in sigima.enums.FrequencyFilterMethod:
253
+ if method == sigima.enums.FrequencyFilterMethod.BRICKWALL:
254
+ # Original test setup works well for brickwall
255
+ tst_sig, _ = build_clean_noisy_signals(
256
+ freq=np.array([1, 3, 5]), noise_level=0
257
+ )
258
+ exp_sig, _ = build_clean_noisy_signals(freq=np.array([1, 5]), noise_level=0)
259
+ _test_filter_method(
260
+ filter_func=sigima.proc.signal.bandstop,
261
+ param_class=sigima.proc.signal.BandStopFilterParam,
262
+ method=method,
263
+ filter_type="bandstop",
264
+ test_signal=tst_sig,
265
+ expected_signal=exp_sig,
266
+ tolerance=1e-3,
267
+ cut0=2.0,
268
+ cut1=4.0,
269
+ )
270
+ else:
271
+ # For scipy filters, use simpler test signal
272
+ x = np.linspace(0, 1, 1000)
273
+ y = (
274
+ np.sin(2 * np.pi * 10 * x)
275
+ + np.sin(2 * np.pi * 100 * x)
276
+ + np.sin(2 * np.pi * 200 * x)
277
+ )
278
+ test_sig = create_signal("test", x, y)
279
+ _test_filter_method(
280
+ filter_func=sigima.proc.signal.bandstop,
281
+ param_class=sigima.proc.signal.BandStopFilterParam,
282
+ method=method,
283
+ filter_type="bandstop",
284
+ test_signal=test_sig,
285
+ cut0=50.0,
286
+ cut1=150.0,
287
+ )
288
+
289
+
290
+ @pytest.mark.validation
291
+ def test_signal_bandpass() -> None:
292
+ """Validation test for bandpass frequency filtering."""
293
+ # Test all filter methods
294
+ for method in sigima.enums.FrequencyFilterMethod:
295
+ if method == sigima.enums.FrequencyFilterMethod.BRICKWALL:
296
+ # Original test setup works well for brickwall
297
+ tst_sig, _ = build_clean_noisy_signals(
298
+ freq=np.array([1, 3, 5]), noise_level=0
299
+ )
300
+ exp_sig, _ = build_clean_noisy_signals(freq=np.array([3]), noise_level=0)
301
+ _test_filter_method(
302
+ filter_func=sigima.proc.signal.bandpass,
303
+ param_class=sigima.proc.signal.BandPassFilterParam,
304
+ method=method,
305
+ filter_type="bandpass",
306
+ test_signal=tst_sig,
307
+ expected_signal=exp_sig,
308
+ tolerance=1e-3,
309
+ cut0=2.0,
310
+ cut1=4.0,
311
+ )
312
+ else:
313
+ # For scipy filters, use simpler test signal
314
+ x = np.linspace(0, 1, 1000)
315
+ y = (
316
+ np.sin(2 * np.pi * 10 * x)
317
+ + np.sin(2 * np.pi * 100 * x)
318
+ + np.sin(2 * np.pi * 200 * x)
319
+ )
320
+ test_sig = create_signal("test", x, y)
321
+ _test_filter_method(
322
+ filter_func=sigima.proc.signal.bandpass,
323
+ param_class=sigima.proc.signal.BandPassFilterParam,
324
+ method=method,
325
+ filter_type="bandpass",
326
+ test_signal=test_sig,
327
+ cut0=50.0,
328
+ cut1=150.0,
329
+ )
330
+
331
+
332
+ def test_brickwall_filter_invalid_x():
333
+ """Test brickwall_filter raises on non-uniform x."""
334
+ clean, noisy = build_clean_noisy_signals()
335
+ x_bad = clean.x.copy()
336
+ x_bad[5] += 0.01 # break uniformity
337
+ with pytest.raises(ValueError, match="evenly spaced"):
338
+ brickwall_filter(x_bad, noisy.y, "lowpass", cut0=0.1)
339
+
340
+
341
+ def test_tools_to_proc_interface():
342
+ """Test that the `brickwall_filter` function is properly interfaced
343
+ with the `sigima.proc` module, via the `lowpass`, `highpass`, `bandpass`,
344
+ and `stopband` functions.
345
+ """
346
+ _clean, tst_sig = build_clean_noisy_signals(freq=np.array([1, 3, 5]))
347
+
348
+ # Lowpass
349
+ tools_res = brickwall_filter(tst_sig.x, tst_sig.y, "lowpass", cut0=2.0)
350
+ param = sigima.proc.signal.LowPassFilterParam.create(
351
+ cut0=2.0,
352
+ method=sigima.enums.FrequencyFilterMethod.BRICKWALL,
353
+ zero_padding=False,
354
+ )
355
+ for cut0 in (None, 2.0):
356
+ param.cut0 = cut0
357
+ # Just test the 'update_from_obj' method, not needed here (and no need to test
358
+ # it for each filter function because they all use the same base class).
359
+ param.update_from_obj(tst_sig)
360
+ proc_res = sigima.proc.signal.lowpass(tst_sig, param)
361
+ check_array_result("Lowpass filter result", tools_res[1], proc_res.y, atol=1e-3)
362
+
363
+ # Highpass
364
+ tools_res = brickwall_filter(tst_sig.x, tst_sig.y, "highpass", cut0=2.0)
365
+ param = sigima.proc.signal.HighPassFilterParam.create(
366
+ cut0=2.0,
367
+ method=sigima.enums.FrequencyFilterMethod.BRICKWALL,
368
+ zero_padding=False,
369
+ )
370
+ proc_res = sigima.proc.signal.highpass(tst_sig, param)
371
+ check_array_result("Highpass filter result", tools_res[1], proc_res.y, atol=1e-3)
372
+
373
+ # Bandpass
374
+ tools_res = brickwall_filter(tst_sig.x, tst_sig.y, "bandpass", cut0=2.0, cut1=4.0)
375
+ param = sigima.proc.signal.BandPassFilterParam.create(
376
+ cut0=2.0,
377
+ cut1=4.0,
378
+ method=sigima.enums.FrequencyFilterMethod.BRICKWALL,
379
+ zero_padding=False,
380
+ )
381
+ proc_res = sigima.proc.signal.bandpass(tst_sig, param)
382
+ check_array_result("Bandpass filter result", tools_res[1], proc_res.y, atol=1e-3)
383
+
384
+ # Bandstop
385
+ tools_res = brickwall_filter(tst_sig.x, tst_sig.y, "bandstop", cut0=2.0, cut1=4.0)
386
+ param = sigima.proc.signal.BandStopFilterParam.create(
387
+ cut0=2.0,
388
+ cut1=4.0,
389
+ method=sigima.enums.FrequencyFilterMethod.BRICKWALL,
390
+ zero_padding=False,
391
+ )
392
+ proc_res = sigima.proc.signal.bandstop(tst_sig, param)
393
+ check_array_result("Bandstop filter result", tools_res[1], proc_res.y, atol=1e-3)
394
+
395
+
396
+ if __name__ == "__main__":
397
+ guiutils.enable_gui()
398
+ # test_signal_lowpass()
399
+ # test_signal_highpass()
400
+ # test_signal_bandstop()
401
+ test_signal_bandpass()
402
+ test_brickwall_filter_invalid_x()
403
+ test_tools_to_proc_interface()