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,470 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for convenience I/O functions with SaveToDirectoryParam.
5
+
6
+ This module tests the `write_signals` and `write_images` functions from
7
+ `sigima.io.convenience`, showcasing the usage of `SaveToDirectoryParam`
8
+ for saving multiple objects to a directory with various configuration options.
9
+ """
10
+
11
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
12
+ # pylint: disable=duplicate-code
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import os.path as osp
18
+
19
+ import numpy as np
20
+
21
+ from sigima.io import ImageIORegistry, SignalIORegistry, write_images, write_signals
22
+ from sigima.objects import create_image, create_signal
23
+ from sigima.params import SaveToDirectoryParam
24
+ from sigima.tests.env import execenv
25
+ from sigima.tests.helpers import WorkdirRestoringTempDir
26
+
27
+
28
+ def create_test_signals() -> list:
29
+ """Create a list of test signals for testing."""
30
+ signals = []
31
+
32
+ # Create signals with different titles and properties
33
+ x = np.linspace(0, 10, 100)
34
+
35
+ # Signal 1: Sine wave
36
+ y1 = np.sin(x)
37
+ signal1 = create_signal(
38
+ title="Sine Wave",
39
+ x=x,
40
+ y=y1,
41
+ metadata={"type": "sine", "frequency": "1 Hz"},
42
+ units=("s", "V"),
43
+ )
44
+ signals.append(signal1)
45
+
46
+ # Signal 2: Cosine wave
47
+ y2 = np.cos(x * 2)
48
+ signal2 = create_signal(
49
+ title="Cosine Wave",
50
+ x=x,
51
+ y=y2,
52
+ metadata={"type": "cosine", "frequency": "2 Hz"},
53
+ units=("s", "A"),
54
+ )
55
+ signals.append(signal2)
56
+
57
+ # Signal 3: Exponential decay
58
+ y3 = np.exp(-x / 3)
59
+ signal3 = create_signal(
60
+ title="Exponential Decay",
61
+ x=x,
62
+ y=y3,
63
+ metadata={"type": "exponential", "time_constant": "3 s"},
64
+ units=("s", "V"),
65
+ )
66
+ signals.append(signal3)
67
+
68
+ return signals
69
+
70
+
71
+ def create_test_images() -> list:
72
+ """Create a list of test images for testing."""
73
+ images = []
74
+
75
+ # Create images with different properties
76
+
77
+ # Image 1: Random noise
78
+ data1 = np.random.rand(50, 50)
79
+ image1 = create_image(
80
+ title="Random Noise",
81
+ data=data1,
82
+ metadata={"type": "noise", "distribution": "uniform"},
83
+ units=("px", "px", "intensity"),
84
+ )
85
+ images.append(image1)
86
+
87
+ # Image 2: Gaussian pattern
88
+ x, y = np.meshgrid(np.linspace(-5, 5, 50), np.linspace(-5, 5, 50))
89
+ data2 = np.exp(-(x**2 + y**2) / 2)
90
+ image2 = create_image(
91
+ title="Gaussian Pattern",
92
+ data=data2,
93
+ metadata={"type": "gaussian", "sigma": "1.0"},
94
+ units=("mm", "mm", "intensity"),
95
+ )
96
+ images.append(image2)
97
+
98
+ # Image 3: Checkerboard pattern
99
+ data3 = np.zeros((50, 50))
100
+ data3[::10, ::10] = 1
101
+ data3[5::10, 5::10] = 1
102
+ image3 = create_image(
103
+ title="Checkerboard",
104
+ data=data3,
105
+ metadata={"type": "pattern", "period": "10 px"},
106
+ units=("px", "px", "binary"),
107
+ )
108
+ images.append(image3)
109
+
110
+ return images
111
+
112
+
113
+ def test_write_signals_basic() -> None:
114
+ """Test basic functionality of write_signals with SaveToDirectoryParam."""
115
+ execenv.print(f"{test_write_signals_basic.__doc__}:")
116
+
117
+ with WorkdirRestoringTempDir() as tmpdir:
118
+ # Create test signals
119
+ signals = create_test_signals()
120
+ execenv.print(f" Created {len(signals)} test signals")
121
+
122
+ # Configure SaveToDirectoryParam
123
+ param = SaveToDirectoryParam()
124
+ param.directory = tmpdir
125
+ param.basename = "{title}"
126
+ param.extension = ".h5sig"
127
+ param.overwrite = False
128
+
129
+ # Write signals to directory
130
+ write_signals(param, signals)
131
+ execenv.print(f" Wrote signals to {tmpdir}")
132
+
133
+ # Verify files were created
134
+ expected_files = [
135
+ "Sine Wave.h5sig",
136
+ "Cosine Wave.h5sig",
137
+ "Exponential Decay.h5sig",
138
+ ]
139
+
140
+ for expected_file in expected_files:
141
+ filepath = osp.join(tmpdir, expected_file)
142
+ assert osp.exists(filepath), f"Expected file {expected_file} not found"
143
+ execenv.print(f" ✓ Created: {expected_file}")
144
+
145
+ # Verify we can read the signal back
146
+ loaded_signal = SignalIORegistry.read(filepath)[0]
147
+ assert loaded_signal is not None
148
+ execenv.print(f" ✓ Verified: {expected_file}")
149
+
150
+ execenv.print(f"{test_write_signals_basic.__doc__}: OK")
151
+
152
+
153
+ def test_write_images_basic() -> None:
154
+ """Test basic functionality of write_images with SaveToDirectoryParam."""
155
+ execenv.print(f"{test_write_images_basic.__doc__}:")
156
+
157
+ with WorkdirRestoringTempDir() as tmpdir:
158
+ # Create test images
159
+ images = create_test_images()
160
+ execenv.print(f" Created {len(images)} test images")
161
+
162
+ # Configure SaveToDirectoryParam
163
+ param = SaveToDirectoryParam()
164
+ param.directory = tmpdir
165
+ param.basename = "{title}"
166
+ param.extension = ".h5ima"
167
+ param.overwrite = False
168
+
169
+ # Write images to directory
170
+ write_images(param, images)
171
+ execenv.print(f" Wrote images to {tmpdir}")
172
+
173
+ # Verify files were created
174
+ expected_files = [
175
+ "Random Noise.h5ima",
176
+ "Gaussian Pattern.h5ima",
177
+ "Checkerboard.h5ima",
178
+ ]
179
+
180
+ for expected_file in expected_files:
181
+ filepath = osp.join(tmpdir, expected_file)
182
+ assert osp.exists(filepath), f"Expected file {expected_file} not found"
183
+ execenv.print(f" ✓ Created: {expected_file}")
184
+
185
+ # Verify we can read the image back
186
+ loaded_image = ImageIORegistry.read(filepath)[0]
187
+ assert loaded_image is not None
188
+ execenv.print(f" ✓ Verified: {expected_file}")
189
+
190
+ execenv.print(f"{test_write_images_basic.__doc__}: OK")
191
+
192
+
193
+ def test_savetodir_param_formatting() -> None:
194
+ """Test SaveToDirectoryParam formatting options and basename patterns."""
195
+ execenv.print(f"{test_savetodir_param_formatting.__doc__}:")
196
+
197
+ with WorkdirRestoringTempDir() as tmpdir:
198
+ # Create test signals with specific metadata
199
+ signals = create_test_signals()
200
+
201
+ # Test different basename patterns
202
+ test_cases = [
203
+ ("{title}", ["Sine Wave.csv", "Cosine Wave.csv", "Exponential Decay.csv"]),
204
+ (
205
+ "{index:03d}_{title}",
206
+ [
207
+ "001_Sine Wave.csv",
208
+ "002_Cosine Wave.csv",
209
+ "003_Exponential Decay.csv",
210
+ ],
211
+ ),
212
+ (
213
+ "signal_{count}_{index}",
214
+ ["signal_3_1.csv", "signal_3_2.csv", "signal_3_3.csv"],
215
+ ),
216
+ (
217
+ "{metadata[type]}_signal",
218
+ ["sine_signal.csv", "cosine_signal.csv", "exponential_signal.csv"],
219
+ ),
220
+ ]
221
+
222
+ for basename_pattern, expected_files in test_cases:
223
+ execenv.print(f" Testing pattern: {basename_pattern}")
224
+
225
+ # Configure SaveToDirectoryParam
226
+ param = SaveToDirectoryParam()
227
+ param.directory = tmpdir
228
+ param.basename = basename_pattern
229
+ param.extension = ".csv"
230
+ param.overwrite = True # Allow overwriting for multiple test cases
231
+
232
+ # Build filenames to verify pattern
233
+ filenames = param.build_filenames(signals)
234
+ execenv.print(f" Generated filenames: {filenames}")
235
+
236
+ # Verify expected filenames
237
+ assert filenames == expected_files, (
238
+ f"Expected {expected_files}, got {filenames}"
239
+ )
240
+ execenv.print(" ✓ Pattern matched expected filenames")
241
+
242
+ execenv.print(f"{test_savetodir_param_formatting.__doc__}: OK")
243
+
244
+
245
+ def test_savetodir_param_collision_handling() -> None:
246
+ """Test SaveToDirectoryParam collision handling and overwrite behavior."""
247
+ execenv.print(f"{test_savetodir_param_collision_handling.__doc__}:")
248
+
249
+ with WorkdirRestoringTempDir() as tmpdir:
250
+ # Create test signals with duplicate titles to force collision
251
+ signals = []
252
+ x = np.linspace(0, 10, 100)
253
+
254
+ for i in range(3):
255
+ y = np.sin(x + i)
256
+ signal = create_signal(
257
+ title="Test Signal", # Same title for all
258
+ x=x,
259
+ y=y,
260
+ metadata={"index": i},
261
+ )
262
+ signals.append(signal)
263
+
264
+ # Test collision handling without overwrite
265
+ param = SaveToDirectoryParam()
266
+ param.directory = tmpdir
267
+ param.basename = "{title}"
268
+ param.extension = ".h5sig"
269
+ param.overwrite = False
270
+
271
+ # Build filenames - should generate unique names
272
+ filenames = param.build_filenames(signals)
273
+ expected_files = [
274
+ "Test Signal.h5sig",
275
+ "Test Signal_1.h5sig",
276
+ "Test Signal_2.h5sig",
277
+ ]
278
+ assert filenames == expected_files, (
279
+ f"Expected {expected_files}, got {filenames}"
280
+ )
281
+ execenv.print(f" ✓ Collision handling generated unique filenames: {filenames}")
282
+
283
+ # Write signals and verify files exist
284
+ write_signals(param, signals)
285
+
286
+ for filename in expected_files:
287
+ filepath = osp.join(tmpdir, filename)
288
+ assert osp.exists(filepath), f"File {filename} was not created"
289
+ execenv.print(f" ✓ Created: {filename}")
290
+
291
+ # Test overwrite behavior
292
+ param.overwrite = True
293
+ write_signals(param, signals[:1]) # Write only first signal
294
+
295
+ # With overwrite=True, should only create the first file
296
+ filepath = osp.join(tmpdir, "Test Signal.h5sig")
297
+ assert osp.exists(filepath), "File should exist after overwrite"
298
+ execenv.print(" ✓ Overwrite test passed")
299
+
300
+ execenv.print(f"{test_savetodir_param_collision_handling.__doc__}: OK")
301
+
302
+
303
+ def test_savetodir_param_metadata_access() -> None:
304
+ """Test SaveToDirectoryParam accessing metadata fields in basename patterns."""
305
+ execenv.print(f"{test_savetodir_param_metadata_access.__doc__}:")
306
+
307
+ with WorkdirRestoringTempDir() as tmpdir:
308
+ # Create signals with rich metadata
309
+ signals = []
310
+ x = np.linspace(0, 10, 100)
311
+
312
+ metadata_list = [
313
+ {"experiment": "exp001", "sensor": "A", "temperature": "25C"},
314
+ {"experiment": "exp002", "sensor": "B", "temperature": "30C"},
315
+ {"experiment": "exp003", "sensor": "C", "temperature": "35C"},
316
+ ]
317
+
318
+ for i, metadata in enumerate(metadata_list):
319
+ y = np.sin(x + i)
320
+ signal = create_signal(title=f"Signal {i + 1}", x=x, y=y, metadata=metadata)
321
+ signals.append(signal)
322
+
323
+ # Test accessing nested metadata in basename pattern
324
+ param = SaveToDirectoryParam()
325
+ param.directory = tmpdir
326
+ param.basename = (
327
+ "{metadata[experiment]}_{metadata[sensor]}_{metadata[temperature]}"
328
+ )
329
+ param.extension = ".csv"
330
+ param.overwrite = False
331
+
332
+ # Build and verify filenames
333
+ filenames = param.build_filenames(signals)
334
+ expected_files = ["exp001_A_25C.csv", "exp002_B_30C.csv", "exp003_C_35C.csv"]
335
+
336
+ assert filenames == expected_files, (
337
+ f"Expected {expected_files}, got {filenames}"
338
+ )
339
+ execenv.print(f" ✓ Metadata access in basename patterns: {filenames}")
340
+
341
+ # Write signals to verify the full workflow
342
+ write_signals(param, signals)
343
+
344
+ for filename in expected_files:
345
+ filepath = osp.join(tmpdir, filename)
346
+ assert osp.exists(filepath), f"File {filename} was not created"
347
+ execenv.print(f" ✓ Created: {filename}")
348
+
349
+ execenv.print(f"{test_savetodir_param_metadata_access.__doc__}: OK")
350
+
351
+
352
+ def test_savetodir_param_units_formatting() -> None:
353
+ """Test SaveToDirectoryParam accessing units in basename patterns."""
354
+ execenv.print(f"{test_savetodir_param_units_formatting.__doc__}:")
355
+
356
+ with WorkdirRestoringTempDir() as tmpdir:
357
+ # Create signals with different units
358
+ signals = []
359
+ x = np.linspace(0, 10, 100)
360
+
361
+ units_list = [("s", "V"), ("ms", "mV"), ("us", "uV")]
362
+
363
+ for i, (xunit, yunit) in enumerate(units_list):
364
+ y = np.sin(x + i)
365
+ signal = create_signal(
366
+ title=f"Signal {i + 1}", x=x, y=y, units=(xunit, yunit)
367
+ )
368
+ signals.append(signal)
369
+
370
+ # Test accessing units in basename pattern
371
+ param = SaveToDirectoryParam()
372
+ param.directory = tmpdir
373
+ param.basename = "{title}_{xunit}_{yunit}"
374
+ param.extension = ".csv"
375
+ param.overwrite = False
376
+
377
+ # Build and verify filenames
378
+ filenames = param.build_filenames(signals)
379
+ expected_files = [
380
+ "Signal 1_s_V.csv",
381
+ "Signal 2_ms_mV.csv",
382
+ "Signal 3_us_uV.csv",
383
+ ]
384
+
385
+ assert filenames == expected_files, (
386
+ f"Expected {expected_files}, got {filenames}"
387
+ )
388
+ execenv.print(f" ✓ Units access in basename patterns: {filenames}")
389
+
390
+ # Write signals to verify the full workflow
391
+ write_signals(param, signals)
392
+
393
+ for filename in expected_files:
394
+ filepath = osp.join(tmpdir, filename)
395
+ assert osp.exists(filepath), f"File {filename} was not created"
396
+ execenv.print(f" ✓ Created: {filename}")
397
+
398
+ execenv.print(f"{test_savetodir_param_units_formatting.__doc__}: OK")
399
+
400
+
401
+ def test_savetodir_param_edge_cases() -> None:
402
+ """Test SaveToDirectoryParam edge cases and error handling."""
403
+ execenv.print(f"{test_savetodir_param_edge_cases.__doc__}:")
404
+
405
+ with WorkdirRestoringTempDir() as tmpdir:
406
+ # Test with empty signals list
407
+ param = SaveToDirectoryParam()
408
+ param.directory = tmpdir
409
+ param.basename = "{title}"
410
+ param.extension = ".h5sig"
411
+ param.overwrite = False
412
+
413
+ # Should handle empty list gracefully
414
+ write_signals(param, [])
415
+ execenv.print(" ✓ Handled empty signals list")
416
+
417
+ # Test with signals having special characters in titles
418
+ signals = []
419
+ x = np.linspace(0, 10, 100)
420
+ y = np.sin(x)
421
+
422
+ special_titles = [
423
+ "Signal/with/slashes",
424
+ "Signal:with:colons",
425
+ "Signal with spaces",
426
+ "Signal_with_underscores",
427
+ ]
428
+
429
+ for title in special_titles:
430
+ signal = create_signal(title=title, x=x, y=y)
431
+ signals.append(signal)
432
+
433
+ # Test filename generation with special characters
434
+ filenames = param.build_filenames(signals)
435
+ execenv.print(f" Generated filenames with special chars: {filenames}")
436
+
437
+ # All filenames should be valid (no slashes/colons in actual filenames)
438
+ for filename in filenames:
439
+ assert "/" not in filename or "\\" not in filename, (
440
+ f"Invalid filename: {filename}"
441
+ )
442
+
443
+ execenv.print(" ✓ Handled special characters in titles")
444
+
445
+ # Test with very long directory path
446
+ long_subdir = "a" * 50 # Create a long subdirectory name
447
+ long_dir = osp.join(tmpdir, long_subdir)
448
+ os.makedirs(long_dir, exist_ok=True)
449
+
450
+ param.directory = long_dir
451
+ param.basename = "test"
452
+ param.extension = ".h5sig"
453
+
454
+ # Should handle long paths
455
+ write_signals(param, signals[:1])
456
+ expected_path = osp.join(long_dir, "test.h5sig")
457
+ assert osp.exists(expected_path), "File should be created in long path"
458
+ execenv.print(" ✓ Handled long directory path")
459
+
460
+ execenv.print(f"{test_savetodir_param_edge_cases.__doc__}: OK")
461
+
462
+
463
+ if __name__ == "__main__":
464
+ test_write_signals_basic()
465
+ test_write_images_basic()
466
+ test_savetodir_param_formatting()
467
+ test_savetodir_param_collision_handling()
468
+ test_savetodir_param_metadata_access()
469
+ test_savetodir_param_units_formatting()
470
+ test_savetodir_param_edge_cases()