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.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {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()
|