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,489 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Unit tests for signal operations
|
|
5
|
+
--------------------------------
|
|
6
|
+
|
|
7
|
+
Features from the "Operations" menu are covered by this test.
|
|
8
|
+
The "Operations" menu contains basic operations on signals, such as
|
|
9
|
+
addition, multiplication, division, and more.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
import sigima.objects
|
|
22
|
+
import sigima.params
|
|
23
|
+
import sigima.proc.signal
|
|
24
|
+
import sigima.tests.data
|
|
25
|
+
from sigima.enums import (
|
|
26
|
+
AngleUnit,
|
|
27
|
+
MathOperator,
|
|
28
|
+
NormalizationMethod,
|
|
29
|
+
SignalsToImageOrientation,
|
|
30
|
+
)
|
|
31
|
+
from sigima.objects.signal import SignalObj
|
|
32
|
+
from sigima.proc.base import AngleUnitParam
|
|
33
|
+
from sigima.proc.signal import complex_from_magnitude_phase, complex_from_real_imag
|
|
34
|
+
from sigima.tests.helpers import check_array_result
|
|
35
|
+
from sigima.tools.coordinates import polar_to_complex
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __create_two_signals() -> tuple[sigima.objects.SignalObj, sigima.objects.SignalObj]:
|
|
39
|
+
"""Create two signals for testing."""
|
|
40
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
41
|
+
sigima.objects.SignalTypes.COSINE, freq=50.0, size=100
|
|
42
|
+
)
|
|
43
|
+
s1.dy = 0.05 * np.ones_like(s1.y)
|
|
44
|
+
s2 = sigima.tests.data.create_periodic_signal(
|
|
45
|
+
sigima.objects.SignalTypes.SINE, freq=25.0, size=100
|
|
46
|
+
)
|
|
47
|
+
s2.dy = 0.8 * np.ones_like(s2.y)
|
|
48
|
+
return s1, s2
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __create_n_signals(n: int = 100) -> list[sigima.objects.SignalObj]:
|
|
52
|
+
"""Create a list of `n` different signals for testing."""
|
|
53
|
+
signals = []
|
|
54
|
+
for i in range(n):
|
|
55
|
+
s = sigima.tests.data.create_periodic_signal(
|
|
56
|
+
sigima.objects.SignalTypes.COSINE,
|
|
57
|
+
freq=50.0 + i,
|
|
58
|
+
size=100,
|
|
59
|
+
a=(i + 1) * 0.1,
|
|
60
|
+
)
|
|
61
|
+
s.dy = 0.5 * np.ones_like(s.y)
|
|
62
|
+
signals.append(s)
|
|
63
|
+
return signals
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __create_one_signal_and_constant() -> tuple[
|
|
67
|
+
sigima.objects.SignalObj, sigima.params.ConstantParam
|
|
68
|
+
]:
|
|
69
|
+
"""Create one signal and a constant for testing."""
|
|
70
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
71
|
+
sigima.objects.SignalTypes.COSINE, freq=50.0, size=100
|
|
72
|
+
)
|
|
73
|
+
s1.dy = 0.5 * np.ones_like(s1.y)
|
|
74
|
+
param = sigima.params.ConstantParam.create(value=-np.pi)
|
|
75
|
+
return s1, param
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.validation
|
|
79
|
+
def test_signal_addition() -> None:
|
|
80
|
+
"""Signal addition test."""
|
|
81
|
+
slist = __create_n_signals()
|
|
82
|
+
n = len(slist)
|
|
83
|
+
s1 = sigima.proc.signal.addition(slist)
|
|
84
|
+
exp_y = np.zeros_like(s1.y)
|
|
85
|
+
for s in slist:
|
|
86
|
+
exp_y += s.y
|
|
87
|
+
check_array_result(f"Addition of {n} signals", s1.y, exp_y)
|
|
88
|
+
expected_dy = np.sqrt(sum(sig.dy**2 for sig in slist))
|
|
89
|
+
check_array_result("Addition error propagation", s1.dy, expected_dy)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.validation
|
|
93
|
+
def test_signal_average() -> None:
|
|
94
|
+
"""Signal average test."""
|
|
95
|
+
slist = __create_n_signals()
|
|
96
|
+
n = len(slist)
|
|
97
|
+
s1 = sigima.proc.signal.average(slist)
|
|
98
|
+
exp_y = np.zeros_like(s1.y)
|
|
99
|
+
for s in slist:
|
|
100
|
+
exp_y += s.y
|
|
101
|
+
exp_y /= n
|
|
102
|
+
check_array_result(f"Average of {n} signals", s1.y, exp_y)
|
|
103
|
+
expected_dy = np.sqrt(sum(s.dy**2 for s in slist)) / n
|
|
104
|
+
check_array_result("Average error propagation", s1.dy, expected_dy)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.validation
|
|
108
|
+
def test_signal_standard_deviation() -> None:
|
|
109
|
+
"""Signal standard deviation test."""
|
|
110
|
+
slist = __create_n_signals()
|
|
111
|
+
n = len(slist)
|
|
112
|
+
s1 = sigima.proc.signal.standard_deviation(slist)
|
|
113
|
+
exp = np.zeros_like(s1.y)
|
|
114
|
+
average = np.mean([s.y for s in slist], axis=0)
|
|
115
|
+
for s in slist:
|
|
116
|
+
exp += (s.y - average) ** 2
|
|
117
|
+
exp = np.sqrt(exp / n)
|
|
118
|
+
check_array_result(f"Standard Deviation of {n} signals", s1.y, exp)
|
|
119
|
+
# Add uncertainty to source signals:
|
|
120
|
+
for sig in slist:
|
|
121
|
+
sig.dy = np.abs(0.1 * sig.y) + 0.1
|
|
122
|
+
s2 = sigima.proc.signal.standard_deviation(slist)
|
|
123
|
+
expected_dy = exp / np.sqrt(2 * (n - 1))
|
|
124
|
+
check_array_result("Standard Deviation error propagation", s2.dy, expected_dy)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.validation
|
|
128
|
+
def test_signal_product() -> None:
|
|
129
|
+
"""Signal multiplication test."""
|
|
130
|
+
slist = __create_n_signals()
|
|
131
|
+
n = len(slist)
|
|
132
|
+
s1 = sigima.proc.signal.product(slist)
|
|
133
|
+
exp_y = np.ones_like(s1.y)
|
|
134
|
+
for s in slist:
|
|
135
|
+
exp_y *= s.y
|
|
136
|
+
check_array_result(f"Product of {n} signals", s1.y, exp_y)
|
|
137
|
+
expected_dy = np.abs(exp_y) * np.sqrt(sum((s.dy / s.y) ** 2 for s in slist))
|
|
138
|
+
check_array_result("Product error propagation", s1.dy, expected_dy)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.validation
|
|
142
|
+
def test_signal_difference() -> None:
|
|
143
|
+
"""Signal difference test."""
|
|
144
|
+
s1, s2 = __create_two_signals()
|
|
145
|
+
s3 = sigima.proc.signal.difference(s1, s2)
|
|
146
|
+
check_array_result("Signal difference", s3.y, s1.y - s2.y)
|
|
147
|
+
expected_dy = np.sqrt(s1.dy**2 + s2.dy**2)
|
|
148
|
+
check_array_result("Difference error propagation", s3.dy, expected_dy)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.validation
|
|
152
|
+
def test_signal_quadratic_difference() -> None:
|
|
153
|
+
"""Signal quadratic difference validation test."""
|
|
154
|
+
s1, s2 = __create_two_signals()
|
|
155
|
+
s3 = sigima.proc.signal.quadratic_difference(s1, s2)
|
|
156
|
+
check_array_result("Signal quadratic difference", s3.y, (s1.y - s2.y) / np.sqrt(2))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@pytest.mark.validation
|
|
160
|
+
def test_signal_division() -> None:
|
|
161
|
+
"""Signal division test."""
|
|
162
|
+
s1, s2 = __create_two_signals()
|
|
163
|
+
s3 = sigima.proc.signal.division(s1, s2)
|
|
164
|
+
check_array_result("Signal division", s3.y, s1.y / s2.y)
|
|
165
|
+
expected_dy = np.abs(s1.y / s2.y) * np.sqrt(
|
|
166
|
+
(s1.dy / s1.y) ** 2 + (s2.dy / s2.y) ** 2
|
|
167
|
+
)
|
|
168
|
+
check_array_result("Division error propagation", s3.dy, expected_dy)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.validation
|
|
172
|
+
def test_signal_addition_constant() -> None:
|
|
173
|
+
"""Signal addition with constant test."""
|
|
174
|
+
s1, param = __create_one_signal_and_constant()
|
|
175
|
+
s2 = sigima.proc.signal.addition_constant(s1, param)
|
|
176
|
+
check_array_result("Signal addition with constant", s2.y, s1.y + param.value)
|
|
177
|
+
# Error should be unchanged after addition of a constant
|
|
178
|
+
check_array_result("Addition constant error propagation", s2.dy, s1.dy)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.validation
|
|
182
|
+
def test_signal_product_constant() -> None:
|
|
183
|
+
"""Signal multiplication by constant test."""
|
|
184
|
+
s1, param = __create_one_signal_and_constant()
|
|
185
|
+
s2 = sigima.proc.signal.product_constant(s1, param)
|
|
186
|
+
check_array_result("Signal multiplication by constant", s2.y, s1.y * param.value)
|
|
187
|
+
# Error is scaled by the absolute value of the constant
|
|
188
|
+
assert param.value is not None
|
|
189
|
+
expected_dy = np.abs(param.value) * s1.dy
|
|
190
|
+
check_array_result("Product constant error propagation", s2.dy, expected_dy)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.validation
|
|
194
|
+
def test_signal_difference_constant() -> None:
|
|
195
|
+
"""Signal difference with constant test."""
|
|
196
|
+
s1, param = __create_one_signal_and_constant()
|
|
197
|
+
s2 = sigima.proc.signal.difference_constant(s1, param)
|
|
198
|
+
check_array_result("Signal difference with constant", s2.y, s1.y - param.value)
|
|
199
|
+
# Error is unchanged after subtraction of a constant
|
|
200
|
+
check_array_result("Difference constant error propagation", s2.dy, s1.dy)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@pytest.mark.validation
|
|
204
|
+
def test_signal_division_constant() -> None:
|
|
205
|
+
"""Signal division by constant test."""
|
|
206
|
+
s1, param = __create_one_signal_and_constant()
|
|
207
|
+
s2 = sigima.proc.signal.division_constant(s1, param)
|
|
208
|
+
check_array_result("Signal division by constant", s2.y, s1.y / param.value)
|
|
209
|
+
assert param.value is not None
|
|
210
|
+
expected_dy = s1.dy / np.abs(param.value)
|
|
211
|
+
check_array_result("Division constant error propagation", s2.dy, expected_dy)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@pytest.mark.validation
|
|
215
|
+
def test_signal_inverse() -> None:
|
|
216
|
+
"""Signal inversion validation test."""
|
|
217
|
+
s1 = __create_two_signals()[0]
|
|
218
|
+
inv_signal = sigima.proc.signal.inverse(s1)
|
|
219
|
+
with warnings.catch_warnings():
|
|
220
|
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
221
|
+
exp_y = 1.0 / s1.y
|
|
222
|
+
exp_y[np.isinf(exp_y)] = np.nan
|
|
223
|
+
expected_dy = np.abs(exp_y) * s1.dy / np.abs(s1.y)
|
|
224
|
+
expected_dy[np.isinf(expected_dy)] = np.nan
|
|
225
|
+
check_array_result("Signal inverse", inv_signal.y, exp_y)
|
|
226
|
+
check_array_result("Inverse error propagation", inv_signal.dy, expected_dy)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@pytest.mark.validation
|
|
230
|
+
def test_signal_absolute() -> None:
|
|
231
|
+
"""Absolute value validation test."""
|
|
232
|
+
s1 = __create_two_signals()[0]
|
|
233
|
+
abs_signal = sigima.proc.signal.absolute(s1)
|
|
234
|
+
check_array_result("Absolute value", abs_signal.y, np.abs(s1.y))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.mark.validation
|
|
238
|
+
def test_signal_real() -> None:
|
|
239
|
+
"""Real part validation test."""
|
|
240
|
+
s1 = __create_two_signals()[0]
|
|
241
|
+
re_signal = sigima.proc.signal.real(s1)
|
|
242
|
+
check_array_result("Real part", re_signal.y, np.real(s1.y))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.validation
|
|
246
|
+
def test_signal_imag() -> None:
|
|
247
|
+
"""Imaginary part validation test."""
|
|
248
|
+
s1 = __create_two_signals()[0]
|
|
249
|
+
im_signal = sigima.proc.signal.imag(s1)
|
|
250
|
+
check_array_result("Imaginary part", im_signal.y, np.imag(s1.y))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@pytest.mark.validation
|
|
254
|
+
def test_signal_complex_from_real_imag() -> None:
|
|
255
|
+
"""Test :py:func:`sigima.proc.signal.complex_from_real_imag`."""
|
|
256
|
+
x = np.linspace(0.0, 1.0, 5)
|
|
257
|
+
real = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
|
258
|
+
imag = np.array([10.0, 20.0, 30.0, 40.0, 50.0])
|
|
259
|
+
# Create SignalObj instances for real and imaginary parts
|
|
260
|
+
s_real = SignalObj("real")
|
|
261
|
+
s_real.set_xydata(x, real)
|
|
262
|
+
s_imag = SignalObj("imag")
|
|
263
|
+
s_imag.set_xydata(x, imag)
|
|
264
|
+
# Create complex signal from real and imaginary parts
|
|
265
|
+
result = complex_from_real_imag(s_real, s_imag)
|
|
266
|
+
check_array_result(
|
|
267
|
+
"complex_from_real_imag",
|
|
268
|
+
result.y,
|
|
269
|
+
real + 1j * imag,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@pytest.mark.validation
|
|
274
|
+
def test_signal_phase() -> None:
|
|
275
|
+
"""Phase angle validation test."""
|
|
276
|
+
# Create a base signal and make it complex for testing
|
|
277
|
+
base_signal = __create_two_signals()[0]
|
|
278
|
+
y_complex = base_signal.y + 1j * base_signal.y[::-1]
|
|
279
|
+
complex_signal = sigima.objects.create_signal("complex", base_signal.x, y_complex)
|
|
280
|
+
|
|
281
|
+
# Test phase extraction in radians without unwrapping
|
|
282
|
+
param_rad = sigima.params.PhaseParam.create(unit=AngleUnit.RADIAN, unwrap=False)
|
|
283
|
+
result_rad = sigima.proc.signal.phase(complex_signal, param_rad)
|
|
284
|
+
check_array_result("Phase in radians", result_rad.y, np.angle(y_complex))
|
|
285
|
+
|
|
286
|
+
# Test phase extraction in degrees without unwrapping
|
|
287
|
+
param_deg = sigima.params.PhaseParam.create(unit=AngleUnit.DEGREE, unwrap=False)
|
|
288
|
+
result_deg = sigima.proc.signal.phase(complex_signal, param_deg)
|
|
289
|
+
check_array_result("Phase in degrees", result_deg.y, np.angle(y_complex, deg=True))
|
|
290
|
+
|
|
291
|
+
# Test phase extraction in radians with unwrapping
|
|
292
|
+
param_rad_unwrap = sigima.params.PhaseParam.create(
|
|
293
|
+
unit=AngleUnit.RADIAN, unwrap=True
|
|
294
|
+
)
|
|
295
|
+
result_rad_unwrap = sigima.proc.signal.phase(complex_signal, param_rad_unwrap)
|
|
296
|
+
check_array_result(
|
|
297
|
+
"Phase in radians with unwrapping",
|
|
298
|
+
result_rad_unwrap.y,
|
|
299
|
+
np.unwrap(np.angle(y_complex)),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Test phase extraction in degrees with unwrapping
|
|
303
|
+
param_deg_unwrap = sigima.params.PhaseParam.create(
|
|
304
|
+
unit=AngleUnit.DEGREE, unwrap=True
|
|
305
|
+
)
|
|
306
|
+
result_deg_unwrap = sigima.proc.signal.phase(complex_signal, param_deg_unwrap)
|
|
307
|
+
check_array_result(
|
|
308
|
+
"Phase in degrees with unwrapping",
|
|
309
|
+
result_deg_unwrap.y,
|
|
310
|
+
np.unwrap(np.angle(y_complex, deg=True), period=360.0),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
MAGNITUDE_PHASE_TEST_CASES = [
|
|
315
|
+
(np.array([0.0, np.pi / 2, np.pi, 3.0 * np.pi / 2.0, 0.0]), AngleUnit.RADIAN),
|
|
316
|
+
(np.array([0.0, 90.0, 180.0, 270.0, 0.0]), AngleUnit.DEGREE),
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@pytest.mark.parametrize("phase, unit", MAGNITUDE_PHASE_TEST_CASES)
|
|
321
|
+
@pytest.mark.validation
|
|
322
|
+
def test_signal_complex_from_magnitude_phase(
|
|
323
|
+
phase: np.ndarray, unit: AngleUnit
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Test :py:func:`sigima.proc.signal.complex_from_magnitude_phase`.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
phase (np.ndarray): Angles in radians or degrees.
|
|
329
|
+
unit (AngleUnit): Unit of the angles, either radian or degree.
|
|
330
|
+
"""
|
|
331
|
+
x = np.linspace(0.0, 1.0, 5)
|
|
332
|
+
magnitude = np.array([2.0, 3.0, 4.0, 5.0, 6.0])
|
|
333
|
+
# Create signal instances for magnitude and phase
|
|
334
|
+
s_mag = SignalObj("magnitude")
|
|
335
|
+
s_mag.set_xydata(x, magnitude)
|
|
336
|
+
s_phase = SignalObj("phase")
|
|
337
|
+
s_phase.set_xydata(x, phase)
|
|
338
|
+
# Create complex signal from magnitude and phase
|
|
339
|
+
p = AngleUnitParam.create(unit=unit)
|
|
340
|
+
result = complex_from_magnitude_phase(s_mag, s_phase, p)
|
|
341
|
+
unit_str = "rad" if unit == AngleUnit.RADIAN else "°"
|
|
342
|
+
check_array_result(
|
|
343
|
+
f"complex_from_magnitude_phase_{unit_str}",
|
|
344
|
+
result.y,
|
|
345
|
+
polar_to_complex(magnitude, phase, unit=unit_str),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def __test_all_complex_from_magnitude_phase() -> None:
|
|
350
|
+
"""Test all combinations of magnitude and phase."""
|
|
351
|
+
for phase, unit in MAGNITUDE_PHASE_TEST_CASES:
|
|
352
|
+
test_signal_complex_from_magnitude_phase(phase, unit)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@pytest.mark.validation
|
|
356
|
+
def test_signal_astype() -> None:
|
|
357
|
+
"""Data type conversion validation test."""
|
|
358
|
+
s1 = __create_two_signals()[0]
|
|
359
|
+
for dtype_str in sigima.objects.SignalObj.get_valid_dtypenames():
|
|
360
|
+
p = sigima.params.DataTypeSParam.create(dtype_str=dtype_str)
|
|
361
|
+
astype_signal = sigima.proc.signal.astype(s1, p)
|
|
362
|
+
assert astype_signal.y.dtype == np.dtype(dtype_str)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@pytest.mark.validation
|
|
366
|
+
def test_signal_exp() -> None:
|
|
367
|
+
"""Exponential validation test."""
|
|
368
|
+
s1 = __create_two_signals()[0]
|
|
369
|
+
exp_signal = sigima.proc.signal.exp(s1)
|
|
370
|
+
check_array_result("Exponential", exp_signal.y, np.exp(s1.y))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.mark.validation
|
|
374
|
+
def test_signal_log10() -> None:
|
|
375
|
+
"""Logarithm base 10 validation test."""
|
|
376
|
+
s1 = __create_two_signals()[0]
|
|
377
|
+
log10_signal = sigima.proc.signal.log10(sigima.proc.signal.exp(s1))
|
|
378
|
+
check_array_result("Logarithm base 10", log10_signal.y, np.log10(np.exp(s1.y)))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@pytest.mark.validation
|
|
382
|
+
def test_signal_sqrt() -> None:
|
|
383
|
+
"""Square root validation test."""
|
|
384
|
+
s1 = sigima.tests.data.get_test_signal("paracetamol.txt")
|
|
385
|
+
sqrt_signal = sigima.proc.signal.sqrt(s1)
|
|
386
|
+
check_array_result("Square root", sqrt_signal.y, np.sqrt(s1.y))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@pytest.mark.validation
|
|
390
|
+
def test_signal_power() -> None:
|
|
391
|
+
"""Power validation test."""
|
|
392
|
+
s1 = sigima.tests.data.get_test_signal("paracetamol.txt")
|
|
393
|
+
p = sigima.params.PowerParam.create(power=2.0)
|
|
394
|
+
power_signal = sigima.proc.signal.power(s1, p)
|
|
395
|
+
check_array_result("Power", power_signal.y, s1.y**p.power)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@pytest.mark.validation
|
|
399
|
+
def test_signal_arithmetic() -> None:
|
|
400
|
+
"""Arithmetic operations validation test."""
|
|
401
|
+
s1, s2 = __create_two_signals()
|
|
402
|
+
p = sigima.params.ArithmeticParam.create()
|
|
403
|
+
for operator in MathOperator:
|
|
404
|
+
p.operator = operator
|
|
405
|
+
for factor in (0.0, 1.0, 2.0):
|
|
406
|
+
p.factor = factor
|
|
407
|
+
for constant in (0.0, 1.0, 2.0):
|
|
408
|
+
p.constant = constant
|
|
409
|
+
s3 = sigima.proc.signal.arithmetic(s1, s2, p)
|
|
410
|
+
if operator == MathOperator.ADD:
|
|
411
|
+
exp = s1.y + s2.y
|
|
412
|
+
elif operator == MathOperator.MULTIPLY:
|
|
413
|
+
exp = s1.y * s2.y
|
|
414
|
+
elif operator == MathOperator.SUBTRACT:
|
|
415
|
+
exp = s1.y - s2.y
|
|
416
|
+
elif operator == MathOperator.DIVIDE:
|
|
417
|
+
exp = s1.y / s2.y
|
|
418
|
+
else:
|
|
419
|
+
raise ValueError(f"Unknown operator {operator}")
|
|
420
|
+
exp = exp * factor + constant
|
|
421
|
+
check_array_result(f"Arithmetic [{p.get_operation()}]", s3.y, exp)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@pytest.mark.validation
|
|
425
|
+
def test_signal_signals_to_image() -> None:
|
|
426
|
+
"""Signals to image conversion test."""
|
|
427
|
+
# Create test signals
|
|
428
|
+
slist = __create_n_signals(n=5)
|
|
429
|
+
n = len(slist)
|
|
430
|
+
size = len(slist[0].y)
|
|
431
|
+
|
|
432
|
+
# Test without normalization, as rows
|
|
433
|
+
p = sigima.params.SignalsToImageParam()
|
|
434
|
+
p.orientation = SignalsToImageOrientation.ROWS
|
|
435
|
+
p.normalize = False
|
|
436
|
+
img = sigima.proc.signal.signals_to_image(slist, p)
|
|
437
|
+
assert img.data.shape == (n, size), (
|
|
438
|
+
f"Expected shape ({n}, {size}), got {img.data.shape}"
|
|
439
|
+
)
|
|
440
|
+
for i, sig in enumerate(slist):
|
|
441
|
+
title = f"Signals to image (rows) - signal {i}"
|
|
442
|
+
check_array_result(title, img.data[i], sig.y)
|
|
443
|
+
|
|
444
|
+
# Test without normalization, as columns
|
|
445
|
+
p.orientation = SignalsToImageOrientation.COLUMNS
|
|
446
|
+
img = sigima.proc.signal.signals_to_image(slist, p)
|
|
447
|
+
assert img.data.shape == (size, n), (
|
|
448
|
+
f"Expected shape ({size}, {n}), got {img.data.shape}"
|
|
449
|
+
)
|
|
450
|
+
for i, sig in enumerate(slist):
|
|
451
|
+
title = f"Signals to image (columns) - signal {i}"
|
|
452
|
+
check_array_result(title, img.data[:, i], sig.y)
|
|
453
|
+
|
|
454
|
+
# Test with normalization
|
|
455
|
+
p.normalize = True
|
|
456
|
+
p.normalize_method = NormalizationMethod.MAXIMUM
|
|
457
|
+
p.orientation = SignalsToImageOrientation.ROWS
|
|
458
|
+
img = sigima.proc.signal.signals_to_image(slist, p)
|
|
459
|
+
for i, sig in enumerate(slist):
|
|
460
|
+
expected = sig.y / np.max(np.abs(sig.y))
|
|
461
|
+
title = f"Signals to image (normalized rows) - signal {i}"
|
|
462
|
+
check_array_result(title, img.data[i], expected)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
if __name__ == "__main__":
|
|
466
|
+
test_signal_addition()
|
|
467
|
+
test_signal_average()
|
|
468
|
+
test_signal_product()
|
|
469
|
+
test_signal_difference()
|
|
470
|
+
test_signal_quadratic_difference()
|
|
471
|
+
test_signal_division()
|
|
472
|
+
test_signal_addition_constant()
|
|
473
|
+
test_signal_product_constant()
|
|
474
|
+
test_signal_difference_constant()
|
|
475
|
+
test_signal_division_constant()
|
|
476
|
+
test_signal_inverse()
|
|
477
|
+
test_signal_absolute()
|
|
478
|
+
test_signal_real()
|
|
479
|
+
test_signal_imag()
|
|
480
|
+
test_signal_complex_from_real_imag()
|
|
481
|
+
test_signal_phase()
|
|
482
|
+
__test_all_complex_from_magnitude_phase()
|
|
483
|
+
test_signal_astype()
|
|
484
|
+
test_signal_exp()
|
|
485
|
+
test_signal_log10()
|
|
486
|
+
test_signal_sqrt()
|
|
487
|
+
test_signal_power()
|
|
488
|
+
test_signal_arithmetic()
|
|
489
|
+
test_signal_signals_to_image()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Peak detection 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.objects
|
|
16
|
+
import sigima.params
|
|
17
|
+
import sigima.proc.signal
|
|
18
|
+
from sigima.objects import GaussParam, SineParam, create_signal_from_param
|
|
19
|
+
from sigima.tests.data import create_paracetamol_signal
|
|
20
|
+
from sigima.tests.helpers import check_scalar_result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.validation
|
|
24
|
+
def test_signal_peak_detection() -> None:
|
|
25
|
+
"""Peak detection validation test."""
|
|
26
|
+
# Test 1: Use a known signal with multiple peaks (paracetamol spectrum)
|
|
27
|
+
src = create_paracetamol_signal()
|
|
28
|
+
|
|
29
|
+
# Create peak detection parameters
|
|
30
|
+
param = sigima.params.PeakDetectionParam.create(threshold=20, min_dist=5)
|
|
31
|
+
|
|
32
|
+
# Apply peak detection
|
|
33
|
+
dst = sigima.proc.signal.peak_detection(src, param)
|
|
34
|
+
|
|
35
|
+
# Check that we got some peaks
|
|
36
|
+
assert dst.y.size > 0, "Peak detection should find at least some peaks"
|
|
37
|
+
assert dst.x.size == dst.y.size, "X and Y arrays should have same size"
|
|
38
|
+
|
|
39
|
+
# Check that all detected peaks are from the original signal
|
|
40
|
+
assert np.all(dst.x >= src.x.min()) and np.all(dst.x <= src.x.max()), (
|
|
41
|
+
"All detected peak positions should be within the original signal range"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Check that peak y-values are from the original signal
|
|
45
|
+
for i in range(dst.x.size):
|
|
46
|
+
# Find closest point in original signal
|
|
47
|
+
idx = np.argmin(np.abs(src.x - dst.x[i]))
|
|
48
|
+
expected_y = src.y[idx]
|
|
49
|
+
check_scalar_result(f"Peak {i} y-value", dst.y[i], expected_y, rtol=1e-10)
|
|
50
|
+
|
|
51
|
+
# Test 2: Synthetic signal with known peaks (multiple Gaussians)
|
|
52
|
+
# Create a signal with 3 well-separated Gaussian peaks
|
|
53
|
+
x = np.linspace(-10, 10, 1000)
|
|
54
|
+
y = (
|
|
55
|
+
np.exp(-((x + 4) ** 2) / 2) # Peak at x=-4
|
|
56
|
+
+ 0.5 * np.exp(-((x - 0) ** 2) / 2) # Peak at x=0 (smaller)
|
|
57
|
+
+ np.exp(-((x - 4) ** 2) / 2) # Peak at x=4
|
|
58
|
+
)
|
|
59
|
+
synthetic_signal = sigima.objects.create_signal("Multi-peak test", x, y)
|
|
60
|
+
|
|
61
|
+
# Detect peaks with appropriate parameters
|
|
62
|
+
param = sigima.params.PeakDetectionParam.create(threshold=40, min_dist=100)
|
|
63
|
+
dst_synthetic = sigima.proc.signal.peak_detection(synthetic_signal, param)
|
|
64
|
+
|
|
65
|
+
# Should detect exactly 3 peaks
|
|
66
|
+
assert dst_synthetic.x.size == 3, f"Expected 3 peaks, got {dst_synthetic.x.size}"
|
|
67
|
+
|
|
68
|
+
# Check peak positions (approximately)
|
|
69
|
+
expected_positions = np.array([-4.0, 0.0, 4.0])
|
|
70
|
+
detected_positions = np.sort(dst_synthetic.x)
|
|
71
|
+
|
|
72
|
+
for i, (detected, expected) in enumerate(
|
|
73
|
+
zip(detected_positions, expected_positions)
|
|
74
|
+
):
|
|
75
|
+
check_scalar_result(f"Peak {i} position", detected, expected, atol=0.2)
|
|
76
|
+
|
|
77
|
+
# Test 3: Edge case - signal with minimal peaks
|
|
78
|
+
# Create a simple sinusoidal signal and use very restrictive parameters
|
|
79
|
+
param_simple = SineParam.create(size=100, xmin=0, xmax=10, freq=1, a=0.1)
|
|
80
|
+
simple_signal = create_signal_from_param(param_simple)
|
|
81
|
+
|
|
82
|
+
# Use a very high threshold to minimize peak detection
|
|
83
|
+
param = sigima.params.PeakDetectionParam.create(threshold=99, min_dist=1)
|
|
84
|
+
dst_minimal = sigima.proc.signal.peak_detection(simple_signal, param)
|
|
85
|
+
|
|
86
|
+
# With such a high threshold, few or no peaks should be detected
|
|
87
|
+
assert dst_minimal.x.size >= 0, "Peak count should be non-negative"
|
|
88
|
+
# If peaks are found, they should be within signal range
|
|
89
|
+
if dst_minimal.x.size > 0:
|
|
90
|
+
assert np.all(dst_minimal.x >= simple_signal.x.min())
|
|
91
|
+
assert np.all(dst_minimal.x <= simple_signal.x.max())
|
|
92
|
+
|
|
93
|
+
# Test 4: Single peak signal
|
|
94
|
+
param_single = GaussParam.create(size=200, xmin=-5, xmax=5, a=1, sigma=1, mu=0)
|
|
95
|
+
single_peak_signal = create_signal_from_param(param_single)
|
|
96
|
+
|
|
97
|
+
param = sigima.params.PeakDetectionParam.create(threshold=30, min_dist=10)
|
|
98
|
+
dst_single = sigima.proc.signal.peak_detection(single_peak_signal, param)
|
|
99
|
+
|
|
100
|
+
# Should detect exactly 1 peak
|
|
101
|
+
assert dst_single.x.size == 1, f"Expected 1 peak, got {dst_single.x.size}"
|
|
102
|
+
|
|
103
|
+
# Peak should be near x=0 (the center of the Gaussian)
|
|
104
|
+
check_scalar_result("Single peak position", dst_single.x[0], 0.0, atol=0.1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_signal_peak_detection_parameters() -> None:
|
|
108
|
+
"""Test peak detection with different parameter values."""
|
|
109
|
+
# Create a test signal with multiple peaks
|
|
110
|
+
param_signal = SineParam.create(size=500, xmin=0, xmax=10, freq=2, a=1)
|
|
111
|
+
test_signal = create_signal_from_param(param_signal)
|
|
112
|
+
|
|
113
|
+
# Test different threshold values
|
|
114
|
+
thresholds = [10, 30, 50, 70]
|
|
115
|
+
peak_counts = []
|
|
116
|
+
|
|
117
|
+
for threshold in thresholds:
|
|
118
|
+
param = sigima.params.PeakDetectionParam.create(threshold=threshold, min_dist=5)
|
|
119
|
+
result = sigima.proc.signal.peak_detection(test_signal, param)
|
|
120
|
+
peak_counts.append(result.x.size)
|
|
121
|
+
|
|
122
|
+
# Higher thresholds should generally detect fewer peaks
|
|
123
|
+
# (though this isn't guaranteed for all signals)
|
|
124
|
+
assert all(count >= 0 for count in peak_counts), (
|
|
125
|
+
"All peak counts should be non-negative"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Test different minimum distances
|
|
129
|
+
min_distances = [1, 5, 10, 20]
|
|
130
|
+
param = sigima.params.PeakDetectionParam.create(threshold=30, min_dist=1)
|
|
131
|
+
result_ref = sigima.proc.signal.peak_detection(test_signal, param)
|
|
132
|
+
|
|
133
|
+
for min_dist in min_distances[1:]: # Skip the first one (reference)
|
|
134
|
+
param = sigima.params.PeakDetectionParam.create(threshold=30, min_dist=min_dist)
|
|
135
|
+
result = sigima.proc.signal.peak_detection(test_signal, param)
|
|
136
|
+
|
|
137
|
+
# Larger minimum distances should generally detect fewer or equal peaks
|
|
138
|
+
assert result.x.size <= result_ref.x.size, (
|
|
139
|
+
f"min_dist={min_dist} should not detect more peaks than min_dist=1"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
test_signal_peak_detection()
|
|
145
|
+
test_signal_peak_detection_parameters()
|