sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,603 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests around the `SignalObj` class and its creation from parameters.
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 os.path as osp
13
+
14
+ import numpy as np
15
+ import pytest
16
+
17
+ import sigima.io
18
+ import sigima.objects
19
+ from sigima.io.signal import SignalIORegistry
20
+ from sigima.tests import guiutils
21
+ from sigima.tests.data import iterate_signal_creation
22
+ from sigima.tests.env import execenv
23
+ from sigima.tests.helpers import (
24
+ WorkdirRestoringTempDir,
25
+ compare_metadata,
26
+ read_test_objects,
27
+ )
28
+
29
+
30
+ # pylint: disable=unused-argument
31
+ def preprocess_signal_parameters(param: sigima.objects.NewSignalParam) -> None:
32
+ """Preprocess signal parameters before creating the signal.
33
+
34
+ Args:
35
+ param: The signal parameters to preprocess.
36
+ """
37
+ # Add here specific preprocessing for signal parameters if needed
38
+
39
+
40
+ def postprocess_signal_object(
41
+ obj: sigima.objects.SignalObj, stype: sigima.objects.SignalTypes
42
+ ) -> None:
43
+ """Postprocess signal object after creation.
44
+
45
+ Args:
46
+ obj: The signal object to postprocess.
47
+ stype: The type of the signal.
48
+ """
49
+ if stype == sigima.objects.SignalTypes.ZERO:
50
+ assert (obj.y == 0).all()
51
+
52
+
53
+ def test_all_signal_types() -> None:
54
+ """Test all combinations of signal types and data sizes"""
55
+ execenv.print(f"{test_all_signal_types.__doc__}:")
56
+ for signal in iterate_signal_creation(
57
+ preproc=preprocess_signal_parameters, postproc=postprocess_signal_object
58
+ ):
59
+ assert signal.x is not None and signal.y is not None
60
+ execenv.print(f"{test_all_signal_types.__doc__}: OK")
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ "fname, orig_signal", list(read_test_objects(SignalIORegistry))
65
+ )
66
+ def test_hdf5_signal_io(fname: str, orig_signal: sigima.objects.SignalObj) -> None:
67
+ """Test HDF5 I/O for signal objects"""
68
+ if orig_signal is None:
69
+ pytest.skip(f"Skipping {fname} (not implemented)")
70
+ execenv.print(f"{test_hdf5_signal_io.__doc__}:")
71
+ with WorkdirRestoringTempDir() as tmpdir:
72
+ # Save to HDF5
73
+ filename = osp.join(tmpdir, f"test_{osp.basename(fname)}.h5sig")
74
+ sigima.io.write_signal(filename, orig_signal)
75
+ execenv.print(f" Saved {filename}")
76
+ # Read back
77
+ fetch_signal = sigima.io.read_signal(filename)
78
+ execenv.print(f" Read {filename}")
79
+ orig_x, orig_y = orig_signal.x, orig_signal.y
80
+ orig_x: np.ndarray
81
+ orig_y: np.ndarray
82
+ x, y = fetch_signal.x, fetch_signal.y
83
+ assert isinstance(x, np.ndarray)
84
+ assert isinstance(y, np.ndarray)
85
+ assert x.shape == orig_x.shape
86
+ assert y.shape == orig_y.shape
87
+ assert x.dtype == orig_x.dtype
88
+ assert y.dtype == orig_y.dtype
89
+ assert np.isclose(x, orig_x, atol=0.0).all()
90
+ assert np.isclose(y, orig_y, atol=0.0).all()
91
+ try:
92
+ compare_metadata(
93
+ fetch_signal.metadata, orig_signal.metadata.copy(), raise_on_diff=True
94
+ )
95
+ except AssertionError as exc:
96
+ raise AssertionError(
97
+ f"Signal metadata read from file does not match original ({fname})"
98
+ ) from exc
99
+ execenv.print(f"{test_hdf5_signal_io.__doc__}: OK")
100
+
101
+
102
+ @pytest.mark.gui
103
+ def test_signal_parameters_interactive() -> None:
104
+ """Test interactive creation of signal parameters"""
105
+ execenv.print(f"{test_signal_parameters_interactive.__doc__}:")
106
+ with guiutils.lazy_qt_app_context(force=True):
107
+ for stype in sigima.objects.SignalTypes:
108
+ param = sigima.objects.create_signal_parameters(stype)
109
+ if isinstance(param, sigima.objects.CustomSignalParam):
110
+ param.setup_array()
111
+ if param.edit():
112
+ execenv.print(f" Edited parameters for {stype.value}:")
113
+ execenv.print(f" {param}")
114
+ else:
115
+ execenv.print(f" Skipped editing parameters for {stype.value}")
116
+ execenv.print(f"{test_signal_parameters_interactive.__doc__}: OK")
117
+
118
+
119
+ def test_create_signal() -> None:
120
+ """Test creation of a signal object using `create_signal` function"""
121
+ execenv.print(f"{test_create_signal.__doc__}:")
122
+ # pylint: disable=import-outside-toplevel
123
+
124
+ # Test all combinations of input parameters
125
+ x = np.linspace(0, 10, 100)
126
+ y = np.sin(x)
127
+ dx = np.full_like(x, 0.1)
128
+ dy = np.full_like(y, 0.01)
129
+ metadata = {"source": "test", "description": "Test signal"}
130
+ units = ("s", "V")
131
+ labels = ("Time", "Amplitude")
132
+
133
+ # 1. Create signal with all parameters
134
+ title = "Some Signal"
135
+ signal = sigima.objects.create_signal(
136
+ title=title,
137
+ x=x,
138
+ y=y,
139
+ dx=dx,
140
+ dy=dy,
141
+ metadata=metadata,
142
+ units=units,
143
+ labels=labels,
144
+ )
145
+ assert isinstance(signal, sigima.objects.SignalObj)
146
+ assert signal.title == title
147
+ assert np.array_equal(signal.x, x)
148
+ assert np.array_equal(signal.y, y)
149
+ assert np.array_equal(signal.dx, dx)
150
+ assert np.array_equal(signal.dy, dy)
151
+ assert signal.metadata == metadata
152
+ assert (signal.xunit, signal.yunit) == units
153
+ assert (signal.xlabel, signal.ylabel) == labels
154
+
155
+ # 2. Create signal with only x and y
156
+ signal = sigima.objects.create_signal("", x=x, y=y)
157
+ assert isinstance(signal, sigima.objects.SignalObj)
158
+ assert np.array_equal(signal.x, x)
159
+ assert np.array_equal(signal.y, y)
160
+ assert signal.dx is None
161
+ assert signal.dy is None
162
+ assert not signal.metadata
163
+ assert (signal.xunit, signal.yunit) == ("", "")
164
+ assert (signal.xlabel, signal.ylabel) == ("", "")
165
+
166
+ # 3. Create signal with only x, y, and dx
167
+ signal = sigima.objects.create_signal("", x=x, y=y, dx=dx)
168
+ assert isinstance(signal, sigima.objects.SignalObj)
169
+ assert np.array_equal(signal.x, x)
170
+ assert np.array_equal(signal.y, y)
171
+ assert np.array_equal(signal.dx, dx)
172
+ assert signal.dy is None
173
+
174
+ # 4. Create signal with only x, y, and dy
175
+ signal = sigima.objects.create_signal("", x=x, y=y, dy=dy)
176
+ assert isinstance(signal, sigima.objects.SignalObj)
177
+ assert np.array_equal(signal.x, x)
178
+ assert np.array_equal(signal.y, y)
179
+ assert signal.dx is None
180
+ assert np.array_equal(signal.dy, dy)
181
+
182
+ execenv.print(f"{test_create_signal.__doc__}: OK")
183
+
184
+
185
+ def test_create_signal_from_param() -> None:
186
+ """Test creation of a signal object using `create_signal_from_param` function"""
187
+ execenv.print(f"{test_create_signal_from_param.__doc__}:")
188
+
189
+ # Test with different signal parameter types
190
+ test_cases = [
191
+ # Basic periodic functions
192
+ (sigima.objects.SineParam, "sine"),
193
+ (sigima.objects.CosineParam, "cosine"),
194
+ (sigima.objects.SawtoothParam, "sawtooth"),
195
+ (sigima.objects.TriangleParam, "triangle"),
196
+ (sigima.objects.SquareParam, "square"),
197
+ (sigima.objects.SincParam, "sinc"),
198
+ # Mathematical functions
199
+ (sigima.objects.GaussParam, "gaussian"),
200
+ (sigima.objects.LorentzParam, "lorentzian"),
201
+ (sigima.objects.ExponentialParam, "exponential"),
202
+ (sigima.objects.LogisticParam, "logistic"),
203
+ (sigima.objects.LinearChirpParam, "linear_chirp"),
204
+ (sigima.objects.StepParam, "step"),
205
+ (sigima.objects.PulseParam, "pulse"),
206
+ (sigima.objects.SquarePulseParam, "square_pulse"),
207
+ (sigima.objects.StepPulseParam, "step_pulse"),
208
+ (sigima.objects.PolyParam, "polynomial"),
209
+ # Noise and random signals
210
+ (sigima.objects.NormalDistribution1DParam, "normal_noise"),
211
+ (sigima.objects.PoissonDistribution1DParam, "poisson_noise"),
212
+ (sigima.objects.UniformDistribution1DParam, "uniform_noise"),
213
+ (sigima.objects.ZeroParam, "zero"),
214
+ # Other signals
215
+ (sigima.objects.CustomSignalParam, "custom"),
216
+ (sigima.objects.VoigtParam, "voigt"),
217
+ (sigima.objects.PlanckParam, "planck"),
218
+ ]
219
+
220
+ # Raise an exception if sigima.objects.signal contain *Param classes not listed here
221
+ param_classes = dict(test_cases)
222
+ for attr_name in dir(sigima.objects):
223
+ attr = getattr(sigima.objects, attr_name)
224
+ if (
225
+ isinstance(attr, type)
226
+ and issubclass(attr, sigima.objects.NewSignalParam)
227
+ and attr is not sigima.objects.NewSignalParam
228
+ and attr is not sigima.objects.CustomSignalParam
229
+ and attr not in param_classes
230
+ ):
231
+ raise AssertionError(f"Missing test case for {attr.__name__}")
232
+
233
+ for param_class, name in test_cases:
234
+ # Create parameter instance with default values
235
+ param = param_class.create(size=100, xmin=1.0, xmax=10.0)
236
+ param.title = f"Test {name} signal"
237
+
238
+ # Test the function
239
+ signal = sigima.objects.create_signal_from_param(param)
240
+
241
+ # Verify the returned object
242
+ assert isinstance(signal, sigima.objects.SignalObj), (
243
+ f"Expected SignalObj, got {type(signal)} for {name}"
244
+ )
245
+ assert signal.title == f"Test {name} signal", (
246
+ f"Title mismatch for {name}: expected 'Test {name} signal', "
247
+ f"got '{signal.title}'"
248
+ )
249
+ assert signal.x is not None, f"X data is None for {name}"
250
+ assert signal.y is not None, f"Y data is None for {name}"
251
+ assert len(signal.x) == 100, f"X length mismatch for {name}"
252
+ assert len(signal.y) == 100, f"Y length mismatch for {name}"
253
+ assert isinstance(signal.x, np.ndarray), f"X is not ndarray for {name}"
254
+ assert isinstance(signal.y, np.ndarray), f"Y is not ndarray for {name}"
255
+
256
+ # Test automatic title generation for parameters that support it
257
+ param_autotitle = param_class.create(size=100, xmin=1.0, xmax=10.0)
258
+ param_autotitle.title = "" # Empty title to trigger auto-generation
259
+ signal_autotitle = sigima.objects.create_signal_from_param(param_autotitle)
260
+ # Distribution params should generate descriptive titles
261
+ if "Distribution" in param_class.__name__:
262
+ assert signal_autotitle.title != "", (
263
+ f"Title should be auto-generated for {name}"
264
+ )
265
+ assert "Random" in signal_autotitle.title, (
266
+ f"Auto-generated title should contain 'Random' for {name}"
267
+ )
268
+
269
+ execenv.print(f" Created {name} signal: OK")
270
+
271
+ # Test with custom parameters and title generation
272
+ param = sigima.objects.GaussParam.create(size=50, xmin=-5.0, xmax=5.0)
273
+ param.title = "" # Empty title should trigger automatic numbering
274
+ signal = sigima.objects.create_signal_from_param(param)
275
+
276
+ assert signal.title != "", "Empty title should be replaced"
277
+
278
+ # Test parameter validation with units and labels
279
+ param = sigima.objects.SineParam()
280
+ param.title = "Sine wave test"
281
+ # xunit is set by default to "s" in SineParam
282
+ assert param.xunit == "s"
283
+ param.yunit = "V"
284
+ param.xlabel = "Time"
285
+ param.ylabel = "Amplitude"
286
+
287
+ signal = sigima.objects.create_signal_from_param(param)
288
+
289
+ expected_xunit = "s"
290
+ assert signal.xunit == expected_xunit, (
291
+ f"X unit mismatch: expected '{expected_xunit}', got '{signal.xunit}'"
292
+ )
293
+ expected_yunit = "V"
294
+ assert signal.yunit == expected_yunit, (
295
+ f"Y unit mismatch: expected '{expected_yunit}', got '{signal.yunit}'"
296
+ )
297
+ expected_xlabel = "Time"
298
+ assert signal.xlabel == expected_xlabel, (
299
+ f"X label mismatch: expected '{expected_xlabel}', got '{signal.xlabel}'"
300
+ )
301
+ expected_ylabel = "Amplitude"
302
+ assert signal.ylabel == expected_ylabel, (
303
+ f"Y label mismatch: expected '{expected_ylabel}', got '{signal.ylabel}'"
304
+ )
305
+
306
+ execenv.print(f"{test_create_signal_from_param.__doc__}: OK")
307
+
308
+
309
+ def test_signal_copy() -> None:
310
+ """Test copying signal objects with all attributes"""
311
+ execenv.print(f"{test_signal_copy.__doc__}:")
312
+
313
+ # Create a base signal with some data
314
+ x = np.linspace(0, 10, 100)
315
+ y = np.sin(x)
316
+ dx = np.full_like(x, 0.1)
317
+ dy = np.full_like(y, 0.01)
318
+ title = "Original Signal"
319
+ metadata = {"key1": "value1", "key2": 42}
320
+ units = ("s", "V")
321
+ labels = ("Time", "Voltage")
322
+
323
+ # Test 1: Copy signal with all attributes
324
+ execenv.print(" Test 1: Copy signal with all attributes")
325
+ signal = sigima.objects.create_signal(
326
+ title=title,
327
+ x=x,
328
+ y=y,
329
+ dx=dx,
330
+ dy=dy,
331
+ metadata=metadata.copy(),
332
+ units=units,
333
+ labels=labels,
334
+ )
335
+
336
+ # Set scale attributes
337
+ signal.autoscale = False
338
+ signal.xscalelog = True
339
+ signal.xscalemin = 1.0
340
+ signal.xscalemax = 9.0
341
+ signal.yscalelog = False
342
+ signal.yscalemin = -1.5
343
+ signal.yscalemax = 1.5
344
+
345
+ # Copy the signal
346
+ copied = signal.copy()
347
+
348
+ # Verify the copy
349
+ assert copied is not signal
350
+ assert copied.title == signal.title
351
+ assert np.array_equal(copied.x, signal.x)
352
+ assert np.array_equal(copied.y, signal.y)
353
+ assert np.array_equal(copied.dx, signal.dx)
354
+ assert np.array_equal(copied.dy, signal.dy)
355
+ assert copied.xydata is not signal.xydata # Different array objects
356
+ assert copied.metadata == signal.metadata
357
+ assert copied.metadata is not signal.metadata
358
+ assert (copied.xunit, copied.yunit) == units
359
+ assert (copied.xlabel, copied.ylabel) == labels
360
+
361
+ # Verify scale attributes are preserved
362
+ assert copied.autoscale == signal.autoscale
363
+ assert copied.xscalelog == signal.xscalelog
364
+ assert copied.xscalemin == signal.xscalemin
365
+ assert copied.xscalemax == signal.xscalemax
366
+ assert copied.yscalelog == signal.yscalelog
367
+ assert copied.yscalemin == signal.yscalemin
368
+ assert copied.yscalemax == signal.yscalemax
369
+ execenv.print(" ✓ All attributes correctly copied")
370
+
371
+ # Test 2: Copy with title override
372
+ execenv.print(" Test 2: Copy with custom title")
373
+ new_title = "Copied Signal"
374
+ copied_with_title = signal.copy(title=new_title)
375
+ assert copied_with_title.title == new_title
376
+ assert copied_with_title.autoscale == signal.autoscale
377
+ assert np.array_equal(copied_with_title.x, signal.x)
378
+ execenv.print(" ✓ Title override works correctly")
379
+
380
+ # Test 3: Copy with metadata filtering
381
+ execenv.print(" Test 3: Copy with metadata filtering")
382
+ copied_basic_meta = signal.copy(all_metadata=False)
383
+ assert copied_basic_meta.autoscale == signal.autoscale
384
+ assert copied_basic_meta.xscalelog == signal.xscalelog
385
+ execenv.print(" ✓ Metadata filtering works correctly")
386
+
387
+ # Test 4: Copy signal without error bars
388
+ execenv.print(" Test 4: Copy signal without error bars")
389
+ signal_no_err = sigima.objects.create_signal(
390
+ title="Signal without error bars",
391
+ x=x,
392
+ y=y,
393
+ units=units,
394
+ labels=labels,
395
+ )
396
+ signal_no_err.autoscale = True
397
+ signal_no_err.yscalelog = True
398
+
399
+ copied_no_err = signal_no_err.copy()
400
+ assert copied_no_err.dx is None
401
+ assert copied_no_err.dy is None
402
+ assert copied_no_err.autoscale is True
403
+ assert copied_no_err.yscalelog is True
404
+ execenv.print(" ✓ Signal without error bars copied correctly")
405
+
406
+ execenv.print(f"{test_signal_copy.__doc__}: OK")
407
+
408
+
409
+ def test_coordinate_conversion() -> None:
410
+ """Test physical_to_indices and indices_to_physical methods"""
411
+ execenv.print(f"{test_coordinate_conversion.__doc__}:")
412
+
413
+ # Create test signals with different x-coordinate patterns
414
+ n = 100
415
+
416
+ # ==================== Test 1: Uniform spacing ====================
417
+ execenv.print(" Test 1: Uniform spacing - basic conversion")
418
+ x_uniform = np.linspace(0.0, 10.0, n)
419
+ y_uniform = np.sin(x_uniform)
420
+ signal_uniform = sigima.objects.create_signal(
421
+ title="Uniform Spacing Test", x=x_uniform, y=y_uniform
422
+ )
423
+
424
+ # Test forward conversion (physical → indices)
425
+ # Since SignalObj uses argmin to find closest x, we test with exact x values
426
+ test_coords = [0.0, 5.0, 10.0]
427
+ indices = signal_uniform.physical_to_indices(test_coords)
428
+ assert len(indices) == 3
429
+ assert indices[0] == 0 # Closest to x[0] = 0.0
430
+ assert indices[1] == 49 # Closest to x[49] ≈ 5.0 (for n=100, linspace 0-10)
431
+ assert indices[2] == 99 # Closest to x[99] = 10.0
432
+ execenv.print(" ✓ Forward conversion (physical → indices) correct")
433
+
434
+ # Test backward conversion (indices → physical)
435
+ test_indices = [0, 49, 99]
436
+ coords = signal_uniform.indices_to_physical(test_indices)
437
+ assert len(coords) == 3
438
+ np.testing.assert_allclose(coords[0], 0.0, rtol=1e-10)
439
+ np.testing.assert_allclose(coords[1], 5.0, rtol=0.02) # ~1% tolerance
440
+ np.testing.assert_allclose(coords[2], 10.0, rtol=1e-10)
441
+ execenv.print(" ✓ Backward conversion (indices → physical) correct")
442
+
443
+ # Test round-trip accuracy
444
+ execenv.print(" Test 2: Uniform spacing - round-trip accuracy")
445
+ # Use exact x values for perfect round-trip
446
+ original_coords = [x_uniform[10], x_uniform[50], x_uniform[80]]
447
+ indices_rt = signal_uniform.physical_to_indices(original_coords)
448
+ recovered_coords = signal_uniform.indices_to_physical(indices_rt)
449
+ np.testing.assert_allclose(recovered_coords, original_coords, rtol=1e-10)
450
+ execenv.print(" ✓ Round-trip (physical → indices → physical) preserves values")
451
+
452
+ # ==================== Test 3: Non-uniform spacing ====================
453
+ execenv.print(" Test 3: Non-uniform spacing - logarithmic")
454
+ x_log = np.logspace(0, 2, n) # 1 to 100, logarithmic spacing
455
+ y_log = np.sin(x_log)
456
+ signal_log = sigima.objects.create_signal(
457
+ title="Logarithmic Spacing Test", x=x_log, y=y_log
458
+ )
459
+
460
+ # Test with exact x values
461
+ test_coords_log = [x_log[0], x_log[50], x_log[99]]
462
+ indices_log = signal_log.physical_to_indices(test_coords_log)
463
+ assert indices_log[0] == 0
464
+ assert indices_log[1] == 50
465
+ assert indices_log[2] == 99
466
+ execenv.print(" ✓ Non-uniform forward conversion correct")
467
+
468
+ # Test backward conversion
469
+ coords_log = signal_log.indices_to_physical([0, 50, 99])
470
+ np.testing.assert_allclose(coords_log[0], x_log[0], rtol=1e-10)
471
+ np.testing.assert_allclose(coords_log[1], x_log[50], rtol=1e-10)
472
+ np.testing.assert_allclose(coords_log[2], x_log[99], rtol=1e-10)
473
+ execenv.print(" ✓ Non-uniform backward conversion correct")
474
+
475
+ # ==================== Test 4: Finding closest value ====================
476
+ execenv.print(" Test 4: Finding closest value (argmin behavior)")
477
+ # Test that physical_to_indices finds the closest x value
478
+ # For uniform spacing, test a value between grid points
479
+ test_val = 5.05 # Between x[49] and x[50]
480
+ idx = signal_uniform.physical_to_indices([test_val])
481
+ # Should return index of closest value
482
+ expected_idx = np.abs(x_uniform - test_val).argmin()
483
+ assert idx[0] == expected_idx
484
+ execenv.print(" ✓ Finds closest x value correctly (argmin)")
485
+
486
+ # Test with multiple values not on grid
487
+ test_vals = [1.23, 4.56, 7.89]
488
+ indices_approx = signal_uniform.physical_to_indices(test_vals)
489
+ for i, val in enumerate(test_vals):
490
+ expected = np.abs(x_uniform - val).argmin()
491
+ assert indices_approx[i] == expected
492
+ execenv.print(" ✓ Multiple approximate values handled correctly")
493
+
494
+ # ==================== Test 5: Quadratic spacing ====================
495
+ execenv.print(" Test 5: Non-uniform spacing - quadratic")
496
+ x_quad = np.linspace(0, 1, n) ** 2 * 100 # Quadratic spacing, denser near 0
497
+ y_quad = np.exp(-x_quad / 10)
498
+ signal_quad = sigima.objects.create_signal(
499
+ title="Quadratic Spacing Test", x=x_quad, y=y_quad
500
+ )
501
+
502
+ # Round-trip test with exact values
503
+ test_indices_quad = [0, 25, 50, 75, 99]
504
+ coords_quad = signal_quad.indices_to_physical(test_indices_quad)
505
+ indices_back = signal_quad.physical_to_indices(coords_quad)
506
+ assert indices_back == test_indices_quad
507
+ execenv.print(" ✓ Round-trip for quadratic spacing preserves indices")
508
+
509
+ # ==================== Test 6: Edge cases ====================
510
+ execenv.print(" Test 6: Edge cases")
511
+
512
+ # Empty coordinate list
513
+ empty_coords = []
514
+ empty_indices = signal_uniform.physical_to_indices(empty_coords)
515
+ assert len(empty_indices) == 0
516
+ execenv.print(" ✓ Empty coordinate list handled")
517
+
518
+ # Single point
519
+ single_coord = [5.0]
520
+ single_idx = signal_uniform.physical_to_indices(single_coord)
521
+ assert len(single_idx) == 1
522
+ execenv.print(" ✓ Single point conversion works")
523
+
524
+ # Multiple points
525
+ multi_coords = [0.0, 2.5, 5.0, 7.5, 10.0]
526
+ multi_idx = signal_uniform.physical_to_indices(multi_coords)
527
+ assert len(multi_idx) == 5
528
+ execenv.print(" ✓ Multiple points conversion works")
529
+
530
+ # Boundary values
531
+ boundary_coords = [x_uniform[0], x_uniform[-1]]
532
+ boundary_idx = signal_uniform.physical_to_indices(boundary_coords)
533
+ assert boundary_idx[0] == 0
534
+ assert boundary_idx[1] == n - 1
535
+ execenv.print(" ✓ Boundary values handled correctly")
536
+
537
+ # ==================== Test 7: Out-of-range values ====================
538
+ execenv.print(" Test 7: Out-of-range values")
539
+ # Values outside the x range should map to closest endpoint
540
+ out_of_range = [-100.0, 200.0]
541
+ out_idx = signal_uniform.physical_to_indices(out_of_range)
542
+ assert out_idx[0] == 0 # Closest to minimum x
543
+ assert out_idx[1] == n - 1 # Closest to maximum x
544
+ execenv.print(" ✓ Out-of-range values map to closest endpoint")
545
+
546
+ # ==================== Test 8: Complex y data ====================
547
+ execenv.print(" Test 8: Complex y data")
548
+ # SignalObj can have complex y values, but x is always real
549
+ x_complex = np.linspace(0, 2 * np.pi, n)
550
+ y_complex = np.exp(1j * x_complex) # Complex exponential
551
+ signal_complex = sigima.objects.create_signal(
552
+ title="Complex Signal Test", x=x_complex, y=y_complex
553
+ )
554
+
555
+ # Test that coordinate conversion still works with complex y
556
+ test_coords_complex = [0.0, np.pi, 2 * np.pi]
557
+ indices_complex = signal_complex.physical_to_indices(test_coords_complex)
558
+ assert len(indices_complex) == 3
559
+ # Verify we can recover coordinates
560
+ coords_complex = signal_complex.indices_to_physical(indices_complex)
561
+ np.testing.assert_allclose(coords_complex, test_coords_complex, rtol=0.02)
562
+ execenv.print(" ✓ Complex y data doesn't affect coordinate conversion")
563
+
564
+ # ==================== Test 9: Dense data near specific region ====================
565
+ execenv.print(" Test 9: Non-uniform spacing - dense region")
566
+ # Create a signal with very dense sampling in middle region
567
+ x_dense = np.concatenate(
568
+ [
569
+ np.linspace(0, 1, 10), # Sparse
570
+ np.linspace(1, 2, 70), # Dense
571
+ np.linspace(2, 3, 10), # Sparse
572
+ ]
573
+ )
574
+ y_dense = np.sin(x_dense * 2 * np.pi)
575
+ signal_dense = sigima.objects.create_signal(
576
+ title="Dense Region Test", x=x_dense, y=y_dense
577
+ )
578
+
579
+ # Test conversion in dense region
580
+ dense_coords = [1.5] # Middle of dense region
581
+ dense_idx = signal_dense.physical_to_indices(dense_coords)
582
+ recovered = signal_dense.indices_to_physical(dense_idx)
583
+ # Should find a very close match due to dense sampling
584
+ assert abs(recovered[0] - 1.5) < 0.02
585
+ execenv.print(" ✓ Dense sampling region handled correctly")
586
+
587
+ execenv.print(f"{test_coordinate_conversion.__doc__}: OK")
588
+
589
+
590
+ def run_all_tests() -> None:
591
+ """Run all tests in this module"""
592
+ test_signal_parameters_interactive()
593
+ test_all_signal_types()
594
+ for fname, orig_signal in read_test_objects(SignalIORegistry):
595
+ test_hdf5_signal_io(fname, orig_signal)
596
+ test_create_signal()
597
+ test_create_signal_from_param()
598
+ test_signal_copy()
599
+ test_coordinate_conversion()
600
+
601
+
602
+ if __name__ == "__main__":
603
+ run_all_tests()