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,176 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
DateTime support unit tests
|
|
5
|
+
===========================
|
|
6
|
+
|
|
7
|
+
Unit tests for datetime functionality in SignalObj.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from sigima.objects import create_signal
|
|
20
|
+
from sigima.objects.signal.constants import VALID_TIME_UNITS
|
|
21
|
+
from sigima.tests.env import execenv
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_signal_datetime_methods() -> None:
|
|
25
|
+
"""Test SignalObj datetime methods."""
|
|
26
|
+
execenv.print("Testing SignalObj datetime methods...")
|
|
27
|
+
|
|
28
|
+
# Create datetime data
|
|
29
|
+
base_time = datetime(2025, 10, 6, 10, 0, 0)
|
|
30
|
+
timestamps = [base_time + timedelta(seconds=i) for i in range(10)]
|
|
31
|
+
values = np.sin(np.arange(10) * 0.5)
|
|
32
|
+
|
|
33
|
+
format_str = "%Y-%m-%d %H:%M:%S"
|
|
34
|
+
|
|
35
|
+
# Test different units
|
|
36
|
+
for unit in VALID_TIME_UNITS:
|
|
37
|
+
# Create signal with initial data
|
|
38
|
+
signal = create_signal(
|
|
39
|
+
"Test Signal", x=np.arange(10, dtype=float), y=values.copy()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Initially should not be datetime
|
|
43
|
+
assert not signal.is_x_datetime()
|
|
44
|
+
|
|
45
|
+
# Set x from datetime
|
|
46
|
+
signal.set_x_from_datetime(timestamps, unit=unit, format_str=format_str)
|
|
47
|
+
|
|
48
|
+
# Check datetime flag
|
|
49
|
+
assert signal.is_x_datetime()
|
|
50
|
+
assert signal.metadata["x_datetime"] is True
|
|
51
|
+
assert signal.xunit == unit
|
|
52
|
+
assert signal.metadata["x_datetime_format"] == format_str
|
|
53
|
+
|
|
54
|
+
# Check x data is float
|
|
55
|
+
assert isinstance(signal.x, np.ndarray)
|
|
56
|
+
assert signal.x.dtype in (np.float32, np.float64)
|
|
57
|
+
|
|
58
|
+
# Get x as datetime
|
|
59
|
+
dt_values = signal.get_x_as_datetime()
|
|
60
|
+
assert isinstance(dt_values, np.ndarray)
|
|
61
|
+
assert dt_values.dtype == np.dtype("datetime64[ns]")
|
|
62
|
+
|
|
63
|
+
# Verify y values are unchanged
|
|
64
|
+
assert np.allclose(signal.y, values)
|
|
65
|
+
|
|
66
|
+
execenv.print(" ✓ SignalObj datetime methods test passed")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_datetime_with_string_input() -> None:
|
|
70
|
+
"""Test datetime conversion from string input."""
|
|
71
|
+
execenv.print("Testing datetime conversion from strings...")
|
|
72
|
+
|
|
73
|
+
signal = create_signal("String DateTime Test")
|
|
74
|
+
|
|
75
|
+
# Create datetime strings
|
|
76
|
+
date_strings = [
|
|
77
|
+
"2025-10-06 10:00:00",
|
|
78
|
+
"2025-10-06 10:00:01",
|
|
79
|
+
"2025-10-06 10:00:02",
|
|
80
|
+
]
|
|
81
|
+
values = [1.0, 2.0, 3.0]
|
|
82
|
+
|
|
83
|
+
# Set from strings
|
|
84
|
+
signal.set_x_from_datetime(date_strings, unit="s")
|
|
85
|
+
signal.y = values
|
|
86
|
+
|
|
87
|
+
# Verify it worked
|
|
88
|
+
assert signal.is_x_datetime()
|
|
89
|
+
assert len(signal.x) == len(date_strings)
|
|
90
|
+
|
|
91
|
+
# Get back as datetime
|
|
92
|
+
dt_values = signal.get_x_as_datetime()
|
|
93
|
+
assert len(dt_values) == len(date_strings)
|
|
94
|
+
|
|
95
|
+
execenv.print(" ✓ String datetime conversion test passed")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_datetime_copy() -> None:
|
|
99
|
+
"""Test that datetime metadata is preserved when copying signal."""
|
|
100
|
+
execenv.print("Testing datetime metadata preservation in copy...")
|
|
101
|
+
|
|
102
|
+
signal = create_signal("Original")
|
|
103
|
+
timestamps = [datetime(2025, 10, 6, 10, 0, i) for i in range(5)]
|
|
104
|
+
signal.set_x_from_datetime(timestamps, unit="ms")
|
|
105
|
+
signal.y = np.arange(5, dtype=float)
|
|
106
|
+
|
|
107
|
+
# Copy signal
|
|
108
|
+
signal_copy = signal.copy()
|
|
109
|
+
|
|
110
|
+
# Verify datetime metadata is preserved
|
|
111
|
+
assert signal_copy.is_x_datetime()
|
|
112
|
+
assert signal_copy.xunit == "ms"
|
|
113
|
+
assert np.array_equal(signal.x, signal_copy.x)
|
|
114
|
+
|
|
115
|
+
execenv.print(" ✓ Datetime metadata preservation test passed")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_datetime_non_datetime_signal() -> None:
|
|
119
|
+
"""Test that non-datetime signals work correctly."""
|
|
120
|
+
execenv.print("Testing non-datetime signal behavior...")
|
|
121
|
+
|
|
122
|
+
signal = create_signal("Regular Signal", x=np.arange(10), y=np.sin(np.arange(10)))
|
|
123
|
+
|
|
124
|
+
# Should not be datetime
|
|
125
|
+
assert not signal.is_x_datetime()
|
|
126
|
+
|
|
127
|
+
# get_x_as_datetime should return regular x
|
|
128
|
+
x_data = signal.get_x_as_datetime()
|
|
129
|
+
assert np.array_equal(x_data, signal.x)
|
|
130
|
+
|
|
131
|
+
execenv.print(" ✓ Non-datetime signal test passed")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_datetime_invalid_unit() -> None:
|
|
135
|
+
"""Test that invalid units raise appropriate errors."""
|
|
136
|
+
execenv.print("Testing invalid unit handling...")
|
|
137
|
+
|
|
138
|
+
timestamps = [datetime(2025, 10, 6, 10, 0, 0)]
|
|
139
|
+
|
|
140
|
+
# Test SignalObj.set_x_from_datetime with invalid unit
|
|
141
|
+
signal = create_signal("Test")
|
|
142
|
+
with pytest.raises(ValueError, match="Invalid unit"):
|
|
143
|
+
signal.set_x_from_datetime(timestamps, unit="invalid")
|
|
144
|
+
|
|
145
|
+
execenv.print(" ✓ Invalid unit handling test passed")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_datetime_arithmetic_operations() -> None:
|
|
149
|
+
"""Test that datetime signals work with arithmetic operations."""
|
|
150
|
+
execenv.print("Testing datetime signal arithmetic...")
|
|
151
|
+
|
|
152
|
+
# Create two signals with datetime x
|
|
153
|
+
base_time = datetime(2025, 10, 6, 10, 0, 0)
|
|
154
|
+
timestamps = [base_time + timedelta(seconds=i) for i in range(10)]
|
|
155
|
+
|
|
156
|
+
signal1 = create_signal("Signal 1")
|
|
157
|
+
signal1.set_x_from_datetime(timestamps, unit="s")
|
|
158
|
+
signal1.y = np.arange(10, dtype=float)
|
|
159
|
+
|
|
160
|
+
signal2 = create_signal("Signal 2")
|
|
161
|
+
signal2.set_x_from_datetime(timestamps, unit="s")
|
|
162
|
+
signal2.y = np.arange(10, dtype=float) * 2
|
|
163
|
+
|
|
164
|
+
# The x data should be identical floats
|
|
165
|
+
assert np.array_equal(signal1.x, signal2.x)
|
|
166
|
+
|
|
167
|
+
# Verify we can do arithmetic on y
|
|
168
|
+
result_y = signal1.y + signal2.y
|
|
169
|
+
expected_y = np.arange(10, dtype=float) * 3
|
|
170
|
+
assert np.allclose(result_y, expected_y)
|
|
171
|
+
|
|
172
|
+
execenv.print(" ✓ Datetime signal arithmetic test passed")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""Signal FFT unit test."""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
6
|
+
# pylint: disable=duplicate-code
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
import scipy.signal as sps
|
|
13
|
+
|
|
14
|
+
import sigima.objects
|
|
15
|
+
import sigima.params
|
|
16
|
+
import sigima.proc.signal
|
|
17
|
+
import sigima.tests.data
|
|
18
|
+
from sigima.enums import PadLocation1D
|
|
19
|
+
from sigima.tests import guiutils
|
|
20
|
+
from sigima.tests.data import get_test_signal
|
|
21
|
+
from sigima.tests.env import execenv
|
|
22
|
+
from sigima.tests.helpers import check_array_result, check_scalar_result
|
|
23
|
+
from sigima.tools.signal import fourier
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.validation
|
|
27
|
+
def test_signal_zero_padding() -> None:
|
|
28
|
+
"""1D FFT zero padding validation test."""
|
|
29
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
30
|
+
sigima.objects.SignalTypes.COSINE, freq=50.0, size=1000
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Validate padding length computation
|
|
34
|
+
for strategy, expected_length in (
|
|
35
|
+
("next_pow2", 24),
|
|
36
|
+
("double", 1000),
|
|
37
|
+
("triple", 2000),
|
|
38
|
+
):
|
|
39
|
+
param = sigima.params.ZeroPadding1DParam.create(strategy=strategy)
|
|
40
|
+
param.update_from_obj(s1)
|
|
41
|
+
assert param.n == expected_length, (
|
|
42
|
+
f"Wrong length for '{param.strategy}' strategy: {param.n}"
|
|
43
|
+
f" (expected {expected_length})"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Validate zero padding
|
|
47
|
+
param = sigima.params.ZeroPadding1DParam.create(strategy="custom", n=250)
|
|
48
|
+
assert param.n is not None
|
|
49
|
+
for location in PadLocation1D:
|
|
50
|
+
execenv.print(f"Validating zero padding with location = {location.value}...")
|
|
51
|
+
param.location = location
|
|
52
|
+
param.update_from_obj(s1)
|
|
53
|
+
s2 = sigima.proc.signal.zero_padding(s1, param)
|
|
54
|
+
len1 = s1.y.size
|
|
55
|
+
n = param.n
|
|
56
|
+
exp_len2 = len1 + n
|
|
57
|
+
assert s2.y.size == exp_len2, f"Wrong length: {len(s2.y)} (expected {exp_len2})"
|
|
58
|
+
if location == PadLocation1D.APPEND:
|
|
59
|
+
dx = s1.x[1] - s1.x[0]
|
|
60
|
+
expected_x = np.pad(
|
|
61
|
+
s1.x,
|
|
62
|
+
(0, n),
|
|
63
|
+
mode="linear_ramp",
|
|
64
|
+
end_values=(s1.x[-1] + dx * n,),
|
|
65
|
+
)
|
|
66
|
+
check_array_result(f"{location.value}: Check x-data", s2.x, expected_x)
|
|
67
|
+
check_array_result(
|
|
68
|
+
f"{location.value}: Check original y-data", s2.y[:len1], s1.y
|
|
69
|
+
)
|
|
70
|
+
check_array_result(
|
|
71
|
+
f"{location.value}: Check padded y-data", s2.y[len1:], np.zeros(n)
|
|
72
|
+
)
|
|
73
|
+
elif location == PadLocation1D.PREPEND:
|
|
74
|
+
dx = s1.x[1] - s1.x[0]
|
|
75
|
+
expected_x = np.pad(
|
|
76
|
+
s1.x,
|
|
77
|
+
(n, 0),
|
|
78
|
+
mode="linear_ramp",
|
|
79
|
+
end_values=(s1.x[0] - dx * n,),
|
|
80
|
+
)
|
|
81
|
+
check_array_result(f"{location.value}: Check x-data", s2.x, expected_x)
|
|
82
|
+
check_array_result(
|
|
83
|
+
f"{location.value}: Check original y-data", s2.y[-len1:], s1.y
|
|
84
|
+
)
|
|
85
|
+
check_array_result(
|
|
86
|
+
f"{location.value}: Check padded y-data", s2.y[:n], np.zeros(n)
|
|
87
|
+
)
|
|
88
|
+
elif location == PadLocation1D.BOTH:
|
|
89
|
+
dx = s1.x[1] - s1.x[0]
|
|
90
|
+
expected_x = np.pad(
|
|
91
|
+
s1.x,
|
|
92
|
+
(n // 2, n - n // 2),
|
|
93
|
+
mode="linear_ramp",
|
|
94
|
+
end_values=(
|
|
95
|
+
s1.x[0] - dx * (n // 2),
|
|
96
|
+
s1.x[-1] + dx * (n - n // 2),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
check_array_result(f"{location.value}: Check x-data", s2.x, expected_x)
|
|
100
|
+
check_array_result(
|
|
101
|
+
f"{location.value}: Check original y-data",
|
|
102
|
+
s2.y[n // 2 : n // 2 + len1],
|
|
103
|
+
s1.y,
|
|
104
|
+
)
|
|
105
|
+
check_array_result(
|
|
106
|
+
f"{location.value}: Check padded y-data (before)",
|
|
107
|
+
s2.y[: n // 2],
|
|
108
|
+
np.zeros(n // 2),
|
|
109
|
+
)
|
|
110
|
+
check_array_result(
|
|
111
|
+
f"{location.value}: Check padded y-data (after)",
|
|
112
|
+
s2.y[-(n - n // 2) :],
|
|
113
|
+
np.zeros(n - n // 2),
|
|
114
|
+
)
|
|
115
|
+
execenv.print("OK")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.validation
|
|
119
|
+
def test_signal_fft() -> None:
|
|
120
|
+
"""1D FFT validation test."""
|
|
121
|
+
freq = 50.0
|
|
122
|
+
size = 10000
|
|
123
|
+
|
|
124
|
+
# See note in function `test_signal_ifft` below.
|
|
125
|
+
xmin = 0.0
|
|
126
|
+
|
|
127
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
128
|
+
sigima.objects.SignalTypes.COSINE, freq=freq, size=size, xmin=xmin
|
|
129
|
+
)
|
|
130
|
+
fft = sigima.proc.signal.fft(s1)
|
|
131
|
+
ifft = sigima.proc.signal.ifft(fft)
|
|
132
|
+
|
|
133
|
+
# Check that the inverse FFT reconstructs the original signal.
|
|
134
|
+
check_array_result("Original and recovered x data", s1.y, ifft.y.real)
|
|
135
|
+
check_array_result("Original and recovered y data", s1.x, ifft.x.real)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.validation
|
|
139
|
+
def test_signal_ifft() -> None:
|
|
140
|
+
"""1D iFFT validation test.
|
|
141
|
+
|
|
142
|
+
Check that the original and reconstructed signals are equal.
|
|
143
|
+
"""
|
|
144
|
+
param = sigima.objects.CosineParam.create(size=500)
|
|
145
|
+
|
|
146
|
+
# *** Note ***
|
|
147
|
+
#
|
|
148
|
+
# We set xmin to 0.0 to be able to compare the X data of the original and
|
|
149
|
+
# reconstructed signals, because the FFT do not preserve the X data (phase is
|
|
150
|
+
# lost, sampling rate is assumed to be constant), so that comparing the X data
|
|
151
|
+
# is not meaningful if xmin is different.
|
|
152
|
+
param.xmin = 0.0
|
|
153
|
+
|
|
154
|
+
s1 = sigima.objects.create_signal_from_param(param)
|
|
155
|
+
assert s1.xydata is not None
|
|
156
|
+
t1, y1 = s1.xydata
|
|
157
|
+
for shift in (True, False):
|
|
158
|
+
f1, sp1 = fourier.fft1d(t1, y1, shift=shift)
|
|
159
|
+
t2, y2 = fourier.ifft1d(f1, sp1)
|
|
160
|
+
|
|
161
|
+
execenv.print(
|
|
162
|
+
f"Comparing original and recovered signals for `shift={shift}`...",
|
|
163
|
+
end=" ",
|
|
164
|
+
)
|
|
165
|
+
check_array_result("Original and recovered x data", t2, t1, verbose=False)
|
|
166
|
+
check_array_result("Original and recovered y data", y2, y1, verbose=False)
|
|
167
|
+
execenv.print("OK")
|
|
168
|
+
|
|
169
|
+
guiutils.view_curves_if_gui(
|
|
170
|
+
[
|
|
171
|
+
s1,
|
|
172
|
+
sigima.objects.create_signal("Recovered", t2, y2),
|
|
173
|
+
sigima.objects.create_signal("Difference", t1, np.abs(y2 - y1)),
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.validation
|
|
179
|
+
def test_signal_magnitude_spectrum() -> None:
|
|
180
|
+
"""1D magnitude spectrum validation test."""
|
|
181
|
+
freq = 50.0
|
|
182
|
+
size = 10000
|
|
183
|
+
|
|
184
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
185
|
+
sigima.objects.SignalTypes.COSINE, freq=freq, size=size
|
|
186
|
+
)
|
|
187
|
+
fft = sigima.proc.signal.fft(s1)
|
|
188
|
+
mag = sigima.proc.signal.magnitude_spectrum(s1)
|
|
189
|
+
|
|
190
|
+
# Check that the peak frequencies are correct.
|
|
191
|
+
ipk1 = np.argmax(mag.y[: size // 2])
|
|
192
|
+
ipk2 = np.argmax(mag.y[size // 2 :]) + size // 2
|
|
193
|
+
fpk1 = fft.x[ipk1]
|
|
194
|
+
fpk2 = fft.x[ipk2]
|
|
195
|
+
check_scalar_result("Frequency of the first peak", fpk1, -freq, rtol=1e-4)
|
|
196
|
+
check_scalar_result("Frequency of the second peak", fpk2, freq, rtol=1e-4)
|
|
197
|
+
|
|
198
|
+
# Check that magnitude spectrum is symmetric.
|
|
199
|
+
check_array_result("Symmetry of magnitude spectrum", mag.y[1::], mag.y[-1:0:-1])
|
|
200
|
+
|
|
201
|
+
# Check the magnitude of the peaks.
|
|
202
|
+
exp_mag = size / 2
|
|
203
|
+
check_scalar_result("Magnitude of the first peak", mag.y[ipk1], exp_mag, rtol=0.05)
|
|
204
|
+
check_scalar_result("Magnitude of the second peak", mag.y[ipk2], exp_mag, rtol=0.05)
|
|
205
|
+
|
|
206
|
+
# Check that the magnitude spectrum is correct.
|
|
207
|
+
check_array_result("Cosine signal magnitude spectrum X", mag.x, fft.x.real)
|
|
208
|
+
check_array_result("Cosine signal magnitude spectrum Y", mag.y, np.abs(fft.y))
|
|
209
|
+
|
|
210
|
+
guiutils.view_curves_if_gui(
|
|
211
|
+
[
|
|
212
|
+
sigima.objects.create_signal("FFT-real", fft.x.real, fft.x.real),
|
|
213
|
+
sigima.objects.create_signal("FFT-imag", fft.x.real, fft.y.imag),
|
|
214
|
+
sigima.objects.create_signal("FFT-magnitude", mag.x.real, mag.y),
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@pytest.mark.validation
|
|
220
|
+
def test_signal_phase_spectrum() -> None:
|
|
221
|
+
"""1D phase spectrum validation test."""
|
|
222
|
+
freq = 50.0
|
|
223
|
+
size = 10000
|
|
224
|
+
|
|
225
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
226
|
+
sigima.objects.SignalTypes.COSINE, freq=freq, size=size
|
|
227
|
+
)
|
|
228
|
+
fft = sigima.proc.signal.fft(s1)
|
|
229
|
+
phase = sigima.proc.signal.phase_spectrum(s1)
|
|
230
|
+
|
|
231
|
+
# Check that the phase spectrum is correct.
|
|
232
|
+
check_array_result("Cosine signal phase spectrum X", phase.x, fft.x.real)
|
|
233
|
+
exp_phase = np.rad2deg(np.angle(fft.y))
|
|
234
|
+
check_array_result("Cosine signal phase spectrum Y", phase.y, exp_phase)
|
|
235
|
+
|
|
236
|
+
guiutils.view_curves_if_gui(
|
|
237
|
+
[
|
|
238
|
+
sigima.objects.create_signal("FFT-real", fft.x.real, fft.x.real),
|
|
239
|
+
sigima.objects.create_signal("FFT-imag", fft.x.real, fft.y.imag),
|
|
240
|
+
sigima.objects.create_signal("Phase", phase.x.real, phase.y),
|
|
241
|
+
]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.validation
|
|
246
|
+
def test_signal_psd() -> None:
|
|
247
|
+
"""1D Power Spectral Density validation test."""
|
|
248
|
+
freq = 50.0
|
|
249
|
+
size = 10000
|
|
250
|
+
|
|
251
|
+
s1 = sigima.tests.data.create_periodic_signal(
|
|
252
|
+
sigima.objects.SignalTypes.COSINE, freq=freq, size=size
|
|
253
|
+
)
|
|
254
|
+
param = sigima.params.SpectrumParam()
|
|
255
|
+
for decibel in (False, True):
|
|
256
|
+
param.decibel = decibel
|
|
257
|
+
psd = sigima.proc.signal.psd(s1, param)
|
|
258
|
+
|
|
259
|
+
# Check that the PSD is correct.
|
|
260
|
+
exp_x, exp_y = sps.welch(s1.y, fs=1.0 / (s1.x[1] - s1.x[0]))
|
|
261
|
+
if decibel:
|
|
262
|
+
exp_y = 10 * np.log10(exp_y)
|
|
263
|
+
|
|
264
|
+
fpk1 = psd.x[np.argmax(psd.y)]
|
|
265
|
+
check_scalar_result("Frequency of the maximum", fpk1, freq, rtol=2e-2)
|
|
266
|
+
|
|
267
|
+
check_array_result(f"Cosine signal PSD X (dB={decibel})", psd.x, exp_x)
|
|
268
|
+
check_array_result(f"Cosine signal PSD Y (dB={decibel})", psd.y, exp_y)
|
|
269
|
+
|
|
270
|
+
guiutils.view_curves_if_gui(
|
|
271
|
+
[
|
|
272
|
+
sigima.objects.create_signal("PSD", psd.x, psd.y),
|
|
273
|
+
]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@pytest.mark.gui
|
|
278
|
+
def test_signal_spectrum() -> None:
|
|
279
|
+
"""Test several FFT-related functions on `dynamic_parameters.txt`."""
|
|
280
|
+
with guiutils.lazy_qt_app_context(force=True):
|
|
281
|
+
# pylint: disable=import-outside-toplevel
|
|
282
|
+
from sigima.tests.vistools import view_curves
|
|
283
|
+
|
|
284
|
+
sig = get_test_signal("dynamic_parameters.txt")
|
|
285
|
+
view_curves([sig])
|
|
286
|
+
p = sigima.params.SpectrumParam.create(decibel=True)
|
|
287
|
+
ms = sigima.proc.signal.magnitude_spectrum(sig, p)
|
|
288
|
+
view_curves([ms], title="Magnitude spectrum")
|
|
289
|
+
ps = sigima.proc.signal.phase_spectrum(sig)
|
|
290
|
+
view_curves([ps], title="Phase spectrum")
|
|
291
|
+
psd = sigima.proc.signal.psd(sig, p)
|
|
292
|
+
view_curves([psd], title="Power spectral density")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
guiutils.enable_gui()
|
|
297
|
+
test_signal_zero_padding()
|
|
298
|
+
test_signal_fft()
|
|
299
|
+
test_signal_ifft()
|
|
300
|
+
test_signal_magnitude_spectrum()
|
|
301
|
+
test_signal_phase_spectrum()
|
|
302
|
+
test_signal_psd()
|
|
303
|
+
test_signal_spectrum()
|