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,484 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Licensed under the terms of the BSD 3-Clause
4
+ # (see sigima/LICENSE for details)
5
+
6
+ """
7
+ Filtering processing functions for signal objects
8
+ =================================================
9
+
10
+ This module provides filtering operations for signal objects:
11
+
12
+ - Gaussian filter
13
+ - Moving average and median filters
14
+ - Wiener filter
15
+ - Frequency filters (low-pass, high-pass, band-pass, band-stop)
16
+ - Noise addition functions
17
+
18
+ .. note::
19
+
20
+ Uses zero-phase filtering when possible for better phase response.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import warnings
26
+ from typing import Callable
27
+
28
+ import guidata.dataset as gds
29
+ import numpy as np
30
+ import scipy.ndimage as spi
31
+ import scipy.signal as sps
32
+
33
+ from sigima.config import _
34
+ from sigima.enums import FilterType, FrequencyFilterMethod, PadLocation1D
35
+ from sigima.objects import (
36
+ NormalDistribution1DParam,
37
+ PoissonDistribution1DParam,
38
+ SignalObj,
39
+ UniformDistribution1DParam,
40
+ create_signal_from_param,
41
+ )
42
+ from sigima.objects.base import (
43
+ NormalDistributionParam,
44
+ PoissonDistributionParam,
45
+ UniformDistributionParam,
46
+ )
47
+ from sigima.proc.base import GaussianParam, MovingAverageParam, MovingMedianParam
48
+ from sigima.proc.decorator import computation_function
49
+ from sigima.proc.signal.arithmetic import addition
50
+ from sigima.proc.signal.base import Wrap1to1Func, dst_1_to_1, restore_data_outside_roi
51
+ from sigima.proc.signal.fourier import ZeroPadding1DParam, zero_padding
52
+ from sigima.tools.signal import fourier
53
+
54
+
55
+ @computation_function()
56
+ def gaussian_filter(src: SignalObj, p: GaussianParam) -> SignalObj:
57
+ """Compute gaussian filter with :py:func:`scipy.ndimage.gaussian_filter`
58
+
59
+ Args:
60
+ src: source signal
61
+ p: parameters
62
+
63
+ Returns:
64
+ Result signal object
65
+ """
66
+ return Wrap1to1Func(spi.gaussian_filter, sigma=p.sigma)(src)
67
+
68
+
69
+ @computation_function()
70
+ def moving_average(src: SignalObj, p: MovingAverageParam) -> SignalObj:
71
+ """Compute moving average with :py:func:`scipy.ndimage.uniform_filter`
72
+
73
+ Args:
74
+ src: source signal
75
+ p: parameters
76
+
77
+ Returns:
78
+ Result signal object
79
+ """
80
+ return Wrap1to1Func(
81
+ spi.uniform_filter, size=p.n, mode=p.mode, func_name="moving_average"
82
+ )(src)
83
+
84
+
85
+ @computation_function()
86
+ def moving_median(src: SignalObj, p: MovingMedianParam) -> SignalObj:
87
+ """Compute moving median with :py:func:`scipy.ndimage.median_filter`
88
+
89
+ Args:
90
+ src: source signal
91
+ p: parameters
92
+
93
+ Returns:
94
+ Result signal object
95
+ """
96
+ return Wrap1to1Func(
97
+ spi.median_filter, size=p.n, mode=p.mode, func_name="moving_median"
98
+ )(src)
99
+
100
+
101
+ @computation_function()
102
+ def wiener(src: SignalObj) -> SignalObj:
103
+ """Compute Wiener filter with :py:func:`scipy.signal.wiener`
104
+
105
+ Args:
106
+ src: source signal
107
+
108
+ Returns:
109
+ Result signal object
110
+ """
111
+ return Wrap1to1Func(sps.wiener)(src)
112
+
113
+
114
+ class BaseHighLowBandParam(gds.DataSet, title=_("Filter")):
115
+ """Base class for high-pass, low-pass, band-pass and band-stop filters"""
116
+
117
+ TYPE = FilterType.LOWPASS
118
+ _type_prop = gds.GetAttrProp("TYPE")
119
+
120
+ # Must be overwriten by the child class
121
+ _method_prop = gds.GetAttrProp("method")
122
+ method = gds.ChoiceItem(
123
+ _("Filter method"),
124
+ [
125
+ (FrequencyFilterMethod.BUTTERWORTH, "Butterworth"),
126
+ (FrequencyFilterMethod.BESSEL, "Bessel"),
127
+ (FrequencyFilterMethod.CHEBYSHEV1, "Chebyshev I"),
128
+ (FrequencyFilterMethod.CHEBYSHEV2, "Chebyshev II"),
129
+ (FrequencyFilterMethod.ELLIPTIC, "Elliptic"),
130
+ (FrequencyFilterMethod.BRICKWALL, "Brickwall"),
131
+ ],
132
+ ).set_prop("display", store=_method_prop)
133
+
134
+ def get_filter_func(self) -> Callable:
135
+ """Get the scipy filter function corresponding to the method."""
136
+ filter_funcs = {
137
+ FrequencyFilterMethod.BESSEL: sps.bessel,
138
+ FrequencyFilterMethod.BUTTERWORTH: sps.butter,
139
+ FrequencyFilterMethod.CHEBYSHEV1: sps.cheby1,
140
+ FrequencyFilterMethod.CHEBYSHEV2: sps.cheby2,
141
+ FrequencyFilterMethod.ELLIPTIC: sps.ellip,
142
+ }
143
+ return filter_funcs.get(self.method)
144
+
145
+ order = gds.IntItem(_("Filter order"), default=3, min=1).set_prop(
146
+ "display",
147
+ active=gds.FuncProp(
148
+ _method_prop, lambda x: x != FrequencyFilterMethod.BRICKWALL
149
+ ),
150
+ )
151
+ cut0 = gds.FloatItem(
152
+ _("Low cutoff frequency"), min=0.0, nonzero=True, unit="Hz", allow_none=True
153
+ )
154
+ cut1 = gds.FloatItem(
155
+ _("High cutoff frequency"), min=0.0, nonzero=True, unit="Hz", allow_none=True
156
+ ).set_prop(
157
+ "display",
158
+ hide=gds.FuncProp(
159
+ _type_prop, lambda x: x in (FilterType.LOWPASS, FilterType.HIGHPASS)
160
+ ),
161
+ )
162
+ rp = gds.FloatItem(
163
+ _("Passband ripple"), min=0.0, default=1.0, nonzero=True, unit="dB"
164
+ ).set_prop(
165
+ "display",
166
+ active=gds.FuncProp(
167
+ _method_prop,
168
+ lambda x: x
169
+ in (FrequencyFilterMethod.CHEBYSHEV1, FrequencyFilterMethod.ELLIPTIC),
170
+ ),
171
+ )
172
+ rs = gds.FloatItem(
173
+ _("Stopband attenuation"), min=0.0, default=60.0, nonzero=True, unit="dB"
174
+ ).set_prop(
175
+ "display",
176
+ active=gds.FuncProp(
177
+ _method_prop,
178
+ lambda x: x
179
+ in (FrequencyFilterMethod.CHEBYSHEV2, FrequencyFilterMethod.ELLIPTIC),
180
+ ),
181
+ )
182
+
183
+ _zp_prop = gds.GetAttrProp("zero_padding")
184
+ zero_padding = gds.BoolItem(
185
+ _("Zero padding"),
186
+ default=True,
187
+ ).set_prop(
188
+ "display",
189
+ active=gds.FuncProp(
190
+ _method_prop, lambda x: x == FrequencyFilterMethod.BRICKWALL
191
+ ),
192
+ store=_zp_prop,
193
+ )
194
+ nfft = gds.IntItem(
195
+ _("Minimum FFT points number"),
196
+ default=0,
197
+ ).set_prop(
198
+ "display",
199
+ active=gds.FuncPropMulti(
200
+ [_method_prop, _zp_prop],
201
+ lambda x, y: x == FrequencyFilterMethod.BRICKWALL and y,
202
+ ),
203
+ )
204
+
205
+ @staticmethod
206
+ def get_nyquist_frequency(obj: SignalObj) -> float:
207
+ """Return the Nyquist frequency of a signal object
208
+
209
+ Args:
210
+ obj: signal object
211
+ """
212
+ fs = float(obj.x.size - 1) / (obj.x[-1] - obj.x[0])
213
+ return fs / 2.0
214
+
215
+ def update_from_obj(self, obj: SignalObj) -> None:
216
+ """Update the filter parameters from a signal object
217
+
218
+ Args:
219
+ obj: signal object
220
+ """
221
+ f_nyquist = self.get_nyquist_frequency(obj)
222
+ if self.cut0 is None:
223
+ if self.TYPE == FilterType.LOWPASS:
224
+ self.cut0 = 0.1 * f_nyquist
225
+ elif self.TYPE == FilterType.HIGHPASS:
226
+ self.cut0 = 0.9 * f_nyquist
227
+ elif self.TYPE == FilterType.BANDPASS:
228
+ self.cut0 = 0.1 * f_nyquist
229
+ self.cut1 = 0.9 * f_nyquist
230
+ elif self.TYPE == FilterType.BANDSTOP:
231
+ self.cut0 = 0.4 * f_nyquist
232
+ self.cut1 = 0.6 * f_nyquist
233
+
234
+ def get_filter_params(self, obj: SignalObj) -> tuple[float | str, float | str]:
235
+ """Return the filter parameters (a and b) as a tuple. These parameters are used
236
+ in the scipy.signal filter functions (eg. `scipy.signal.filtfilt`).
237
+
238
+ Args:
239
+ obj: signal object
240
+
241
+ Returns:
242
+ tuple: filter parameters
243
+ """
244
+ f_nyquist = self.get_nyquist_frequency(obj)
245
+ args: list[float | str | tuple[float, ...]] = [self.order] # type: ignore
246
+ if self.method == FrequencyFilterMethod.CHEBYSHEV1:
247
+ args += [self.rp]
248
+ elif self.method == FrequencyFilterMethod.CHEBYSHEV2:
249
+ args += [self.rs]
250
+ elif self.method == FrequencyFilterMethod.ELLIPTIC:
251
+ args += [self.rp, self.rs]
252
+ if self.TYPE in (FilterType.HIGHPASS, FilterType.LOWPASS):
253
+ args += [self.cut0 / f_nyquist]
254
+ else:
255
+ args += [[self.cut0 / f_nyquist, self.cut1 / f_nyquist]]
256
+ args += [self.TYPE.value]
257
+ return self.get_filter_func()(*args)
258
+
259
+
260
+ class LowPassFilterParam(BaseHighLowBandParam):
261
+ """Low-pass filter parameters"""
262
+
263
+ TYPE = FilterType.LOWPASS
264
+
265
+ # Redefine cut0 just to change its label (instead of "Low cutoff frequency")
266
+ cut0 = gds.FloatItem(
267
+ _("Cutoff frequency"), min=0, nonzero=True, unit="Hz", allow_none=True
268
+ )
269
+
270
+
271
+ class HighPassFilterParam(BaseHighLowBandParam):
272
+ """High-pass filter parameters"""
273
+
274
+ TYPE = FilterType.HIGHPASS
275
+
276
+ # Redefine cut0 just to change its label (instead of "High cutoff frequency")
277
+ cut0 = gds.FloatItem(
278
+ _("Cutoff frequency"), min=0, nonzero=True, unit="Hz", allow_none=True
279
+ )
280
+
281
+
282
+ class BandPassFilterParam(BaseHighLowBandParam):
283
+ """Band-pass filter parameters"""
284
+
285
+ TYPE = FilterType.BANDPASS
286
+
287
+
288
+ class BandStopFilterParam(BaseHighLowBandParam):
289
+ """Band-stop filter parameters"""
290
+
291
+ TYPE = FilterType.BANDSTOP
292
+
293
+
294
+ def frequency_filter(src: SignalObj, p: BaseHighLowBandParam) -> SignalObj:
295
+ """Compute frequency filter (low-pass, high-pass, band-pass, band-stop),
296
+ with :py:func:`scipy.signal.filtfilt`
297
+
298
+ Args:
299
+ src: source signal
300
+ p: parameters
301
+
302
+ Returns:
303
+ Result signal object
304
+
305
+ .. note::
306
+
307
+ Uses zero-phase filtering (`filtfilt`) when possible for better phase response.
308
+ If numerical instability occurs (e.g., singular matrix errors), automatically
309
+ falls back to forward filtering (`lfilter`) with a warning. This ensures
310
+ cross-platform compatibility while maintaining optimal filtering when possible.
311
+ """
312
+ name = f"{p.TYPE.value}"
313
+ suffix = ""
314
+ if p.method != FrequencyFilterMethod.BRICKWALL:
315
+ suffix = f"order={p.order:d}, "
316
+ if p.TYPE in (FilterType.LOWPASS, FilterType.HIGHPASS):
317
+ suffix += f"cutoff={p.cut0:.2f}"
318
+ else:
319
+ suffix += f"cutoff={p.cut0:.2f}:{p.cut1:.2f}"
320
+ dst = dst_1_to_1(src, name, suffix)
321
+
322
+ if p.method == FrequencyFilterMethod.BRICKWALL:
323
+ original_size = src.y.size
324
+ src_padded = src.copy()
325
+ if p.zero_padding and p.nfft is not None:
326
+ size_padded = ZeroPadding1DParam.next_power_of_two(max(p.nfft, src.y.size))
327
+ n_to_add = size_padded - src.y.size
328
+ if n_to_add > 0:
329
+ src_padded = zero_padding(
330
+ src_padded,
331
+ ZeroPadding1DParam.create(
332
+ location=PadLocation1D.APPEND,
333
+ strategy="custom",
334
+ n=n_to_add,
335
+ ),
336
+ )
337
+ x_padded, y_padded = src_padded.get_data()
338
+ x, y = fourier.brickwall_filter(
339
+ x_padded, y_padded, p.TYPE.value, p.cut0, p.cut1
340
+ )
341
+ # Trim back to original size if padding was applied
342
+ x = x[:original_size]
343
+ y = y[:original_size]
344
+ dst.set_xydata(x, y)
345
+ else:
346
+ b, a = p.get_filter_params(dst)
347
+ try:
348
+ # Prefer zero-phase filtering
349
+ dst.y = sps.filtfilt(b, a, dst.y)
350
+ except np.linalg.LinAlgError:
351
+ # Fallback to forward filtering if filtfilt fails due to numerical issues
352
+ warnings.warn(
353
+ "Zero-phase filtering failed due to numerical instability. "
354
+ "Using forward filtering instead.",
355
+ UserWarning,
356
+ stacklevel=2,
357
+ )
358
+ dst.y = sps.lfilter(b, a, dst.y)
359
+
360
+ restore_data_outside_roi(dst, src)
361
+ return dst
362
+
363
+
364
+ @computation_function()
365
+ def lowpass(src: SignalObj, p: LowPassFilterParam) -> SignalObj:
366
+ """Compute low-pass filter with :py:func:`scipy.signal.filtfilt`
367
+
368
+ Args:
369
+ src: source signal
370
+ p: parameters
371
+
372
+ Returns:
373
+ Result signal object
374
+ """
375
+ return frequency_filter(src, p)
376
+
377
+
378
+ @computation_function()
379
+ def highpass(src: SignalObj, p: HighPassFilterParam) -> SignalObj:
380
+ """Compute high-pass filter with :py:func:`scipy.signal.filtfilt`
381
+
382
+ Args:
383
+ src: source signal
384
+ p: parameters
385
+
386
+ Returns:
387
+ Result signal object
388
+ """
389
+ return frequency_filter(src, p)
390
+
391
+
392
+ @computation_function()
393
+ def bandpass(src: SignalObj, p: BandPassFilterParam) -> SignalObj:
394
+ """Compute band-pass filter with :py:func:`scipy.signal.filtfilt`
395
+
396
+ Args:
397
+ src: source signal
398
+ p: parameters
399
+
400
+ Returns:
401
+ Result signal object
402
+ """
403
+ return frequency_filter(src, p)
404
+
405
+
406
+ @computation_function()
407
+ def bandstop(src: SignalObj, p: BandStopFilterParam) -> SignalObj:
408
+ """Compute band-stop filter with :py:func:`scipy.signal.filtfilt`
409
+
410
+ Args:
411
+ src: source signal
412
+ p: parameters
413
+
414
+ Returns:
415
+ Result signal object
416
+ """
417
+ return frequency_filter(src, p)
418
+
419
+
420
+ # Noise addition functions
421
+ @computation_function()
422
+ def add_gaussian_noise(src: SignalObj, p: NormalDistributionParam) -> SignalObj:
423
+ """Add normal noise to the input signal.
424
+
425
+ Args:
426
+ src: Source signal.
427
+ p: Parameters.
428
+
429
+ Returns:
430
+ Result signal object.
431
+ """
432
+ param = NormalDistribution1DParam() # Do not confuse with NormalDistributionParam
433
+ gds.update_dataset(param, p)
434
+ param.xmin = src.x[0]
435
+ param.xmax = src.x[-1]
436
+ param.size = src.x.size
437
+ noise = create_signal_from_param(param)
438
+ dst = dst_1_to_1(src, "add_gaussian_noise", f"µ={p.mu}, σ={p.sigma}")
439
+ dst.xydata = addition([src, noise]).xydata
440
+ return dst
441
+
442
+
443
+ @computation_function()
444
+ def add_poisson_noise(src: SignalObj, p: PoissonDistributionParam) -> SignalObj:
445
+ """Add Poisson noise to the input signal.
446
+
447
+ Args:
448
+ src: Source signal.
449
+ p: Parameters.
450
+
451
+ Returns:
452
+ Result signal object.
453
+ """
454
+ param = PoissonDistribution1DParam() # Do not confuse with PoissonDistributionParam
455
+ gds.update_dataset(param, p)
456
+ param.xmin = src.x[0]
457
+ param.xmax = src.x[-1]
458
+ param.size = src.x.size
459
+ noise = create_signal_from_param(param)
460
+ dst = dst_1_to_1(src, "add_poisson_noise", f"λ={p.lam}")
461
+ dst.xydata = addition([src, noise]).xydata
462
+ return dst
463
+
464
+
465
+ @computation_function()
466
+ def add_uniform_noise(src: SignalObj, p: UniformDistributionParam) -> SignalObj:
467
+ """Add uniform noise to the input signal.
468
+
469
+ Args:
470
+ src: Source signal.
471
+ p: Parameters.
472
+
473
+ Returns:
474
+ Result signal object.
475
+ """
476
+ param = UniformDistribution1DParam() # Do not confuse with UniformDistributionParam
477
+ gds.update_dataset(param, p)
478
+ param.xmin = src.x[0]
479
+ param.xmax = src.x[-1]
480
+ param.size = src.x.size
481
+ noise = create_signal_from_param(param)
482
+ dst = dst_1_to_1(src, "add_uniform_noise", f"low={p.vmin}, high={p.vmax}")
483
+ dst.xydata = addition([src, noise]).xydata
484
+ return dst