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,495 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for reading and writing coordinated text format files.
5
+ """
6
+
7
+ import os
8
+ import os.path as osp
9
+ import tempfile
10
+
11
+ import numpy as np
12
+
13
+ import sigima.io
14
+ import sigima.objects
15
+ import sigima.params
16
+ import sigima.proc.image
17
+ from sigima.io.image.formats import CoordinatedTextFileReader
18
+ from sigima.tests.env import execenv
19
+ from sigima.tests.helpers import (
20
+ WorkdirRestoringTempDir,
21
+ check_array_result,
22
+ get_test_fnames,
23
+ )
24
+
25
+
26
+ def test_read_image_basic():
27
+ """Basic test to read a simple coordinated text image file"""
28
+ path = get_test_fnames("coordinated_text/image.txt")[0]
29
+ imgs = CoordinatedTextFileReader.read_images(path)
30
+ assert len(imgs) == 1, f"Expected 1 image, got {len(imgs)}"
31
+ arr = np.asarray(imgs[0].data)
32
+ expected = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)
33
+ check_array_result("test read image.txt", arr, expected)
34
+
35
+
36
+ def test_read_image_with_unit():
37
+ """Test to read a coordinated text image file with units in metadata"""
38
+ path = get_test_fnames("coordinated_text/image_with_unit.txt")[0]
39
+ imgs = CoordinatedTextFileReader.read_images(path)
40
+ assert len(imgs) == 1, f"Expected 1 image, got {len(imgs)}"
41
+ img = imgs[0]
42
+ # units should come from metadata (X, Y, Z)
43
+ check_array_result(
44
+ "test read image_with_unit.txt",
45
+ np.asarray(img.data),
46
+ np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]),
47
+ )
48
+
49
+ assert img.xunit == "mm", (
50
+ f"X unit not read correctly: {img.xunit} given but mm expected"
51
+ )
52
+ assert img.yunit == "nm", (
53
+ f"Y unit not read correctly: {img.yunit} given but nm expected"
54
+ )
55
+ assert img.zunit == "A", (
56
+ f"Z unit not read correctly: {img.zunit} given but A expected"
57
+ )
58
+
59
+
60
+ def test_read_image_with_nan():
61
+ """Test to read a coordinated text image file with NaN values"""
62
+ path = get_test_fnames("coordinated_text/image_with_nan.txt")[0]
63
+ imgs = CoordinatedTextFileReader.read_images(path)
64
+ assert len(imgs) == 1, f"Expected 1 image, got {len(imgs)}"
65
+ arr = np.asarray(imgs[0].data)
66
+ # expected NaN positions from the test file
67
+ assert np.isnan(arr[0, 2]), "expected NaN at position (0,2), got {arr[0,2]}"
68
+ assert np.isnan(arr[1, 0]), "expected NaN at position (1,0), got {arr[1,0]}"
69
+ assert np.isnan(arr[1, 1]), "expected NaN at position (1,1), got {arr[1,1]}"
70
+ # and a valid value
71
+ assert arr[0, 0] == 1, "expected 1 at position (0,0), got {arr[0,0]}"
72
+ assert arr[1, 2] == 6, "expected 6 at position (1,2), got {arr[1,2]}"
73
+
74
+
75
+ def test_read_complex_image_and_error():
76
+ """Test to read a coordinated text complex image file with error image"""
77
+ path = get_test_fnames("coordinated_text/complex_image.txt")[0]
78
+ imgs = CoordinatedTextFileReader.read_images(path)
79
+ # should return main image and error image
80
+ assert len(imgs) == 2, f"Expected 2 images, got {len(imgs)}"
81
+ img, img_err = imgs[0], imgs[1]
82
+ # data should be complex
83
+ assert np.iscomplexobj(np.asarray(img.data)), (
84
+ f"expected complex data, got {np.asarray(img.data).dtype}"
85
+ )
86
+ assert np.iscomplexobj(np.asarray(img_err.data)), (
87
+ f"expected complex data, got {np.asarray(img_err.data).dtype}"
88
+ )
89
+ # check first element values (from first data line)
90
+ first_val = img.data[0, 0]
91
+ expected = complex(3.678795e-01, 3.678795e-01)
92
+ np.testing.assert_allclose(first_val, expected, rtol=1e-7, atol=1e-12)
93
+ first_err = img_err.data[0, 0]
94
+ expected_err = complex(1.839397e-01, -3.678795e-01)
95
+ np.testing.assert_allclose(first_err, expected_err, rtol=1e-7, atol=1e-12)
96
+
97
+
98
+ def test_read_nonuniform_coordinates():
99
+ """Test reading coordinated text file with non-uniform coordinates"""
100
+ # Create a temporary test file with non-uniform coordinates
101
+ test_content = """# Created on 2024-10-10 12:00:00.000000
102
+ # By Test Script
103
+ # Using matrislib 3.0.0test
104
+ # nx : 3
105
+ # ny : 2
106
+ # X : X-axis (mm)
107
+ # Y : Y-axis (mm)
108
+ # Z : Z-value (units)
109
+ 0.000000 0.000000 1.000000
110
+ 1.500000 0.000000 2.000000
111
+ 4.000000 0.000000 3.000000
112
+ 0.000000 3.000000 4.000000
113
+ 1.500000 3.000000 5.000000
114
+ 4.000000 3.000000 6.000000
115
+ """
116
+
117
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
118
+ f.write(test_content)
119
+ temp_filename = f.name
120
+
121
+ try:
122
+ imgs = CoordinatedTextFileReader.read_images(temp_filename)
123
+ assert len(imgs) == 1, f"Expected 1 image, got {len(imgs)}"
124
+
125
+ img = imgs[0]
126
+
127
+ # Should detect non-uniform coordinates
128
+ assert not img.is_uniform_coords, "Should detect non-uniform coordinates"
129
+
130
+ # Check coordinate arrays
131
+ expected_x = np.array([0.0, 1.5, 4.0])
132
+ expected_y = np.array([0.0, 3.0])
133
+
134
+ np.testing.assert_allclose(img.xcoords, expected_x, rtol=1e-10)
135
+ np.testing.assert_allclose(img.ycoords, expected_y, rtol=1e-10)
136
+
137
+ # Check data shape and values
138
+ assert img.data.shape == (2, 3), f"Expected shape (2, 3), got {img.data.shape}"
139
+ expected_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
140
+ np.testing.assert_allclose(img.data, expected_data)
141
+
142
+ # Check coordinate conversion functionality
143
+ img.switch_coords_to("uniform")
144
+ assert img.is_uniform_coords, "Should convert to uniform coordinates"
145
+
146
+ # After conversion to uniform, switching back creates uniform grid
147
+ img.switch_coords_to("non-uniform")
148
+ assert not img.is_uniform_coords, "Should convert back to non-uniform"
149
+
150
+ # The new non-uniform coordinates will be a uniform grid, not the original
151
+ # This is expected behavior since uniform conversion loses original spacing
152
+ assert len(img.xcoords) == len(expected_x), (
153
+ "Should have same number of X coordinates"
154
+ )
155
+ assert len(img.ycoords) == len(expected_y), (
156
+ "Should have same number of Y coordinates"
157
+ )
158
+
159
+ finally:
160
+ os.unlink(temp_filename)
161
+
162
+
163
+ def test_nonuniform_coordinates_io() -> None:
164
+ """Test I/O (read and write) for coordinated text format
165
+ with non-uniform coordinates
166
+ """
167
+ execenv.print(f"{test_nonuniform_coordinates_io.__doc__}:")
168
+
169
+ # Create a test image with non-uniform coordinates
170
+ title = "Non-uniform Coordinates Test"
171
+ data = np.random.rand(10, 10)
172
+ metadata = {"test_key": "test_value", "coordinate_type": "non-uniform"}
173
+ units = ("μm", "μm", "counts")
174
+ labels = ("X position", "Y position", "Intensity")
175
+
176
+ # Create the image object
177
+ orig_image = sigima.objects.create_image(
178
+ title=title,
179
+ data=data,
180
+ metadata=metadata,
181
+ units=units,
182
+ labels=labels,
183
+ )
184
+
185
+ # Set non-uniform coordinates
186
+ xcoords = np.linspace(0, 1, 10)
187
+ ycoords = np.linspace(0, 1, 10) ** 2 # Quadratic spacing
188
+ orig_image.set_coords(xcoords=xcoords, ycoords=ycoords)
189
+
190
+ # Verify the original image has non-uniform coordinates
191
+ assert not orig_image.is_uniform_coords
192
+ assert np.array_equal(orig_image.xcoords, xcoords)
193
+ assert np.array_equal(orig_image.ycoords, ycoords)
194
+
195
+ execenv.print(" ✓ Created non-uniform coordinate image")
196
+
197
+ with WorkdirRestoringTempDir() as tmpdir:
198
+ # Test coordinated text CSV format writing (the main focus of this test)
199
+ csv_filename = osp.join(tmpdir, "test_nonuniform_coords.csv")
200
+
201
+ # Save to coordinated text CSV format
202
+ sigima.io.write_image(csv_filename, orig_image)
203
+ execenv.print(f" ✓ Saved to coordinated text CSV: {csv_filename}")
204
+
205
+ # Read back from coordinated text CSV
206
+ loaded_csv_image = sigima.io.read_image(csv_filename)
207
+ execenv.print(f" ✓ Loaded from coordinated text CSV: {csv_filename}")
208
+
209
+ # Verify the loaded CSV image
210
+ assert isinstance(loaded_csv_image, sigima.objects.ImageObj)
211
+ assert loaded_csv_image.title == osp.basename(csv_filename)
212
+
213
+ # For CSV files, use allclose instead of array_equal due to
214
+ # floating-point precision loss during text serialization
215
+ assert np.allclose(loaded_csv_image.data, orig_image.data, atol=1e-10)
216
+ csv_units = (
217
+ loaded_csv_image.xunit,
218
+ loaded_csv_image.yunit,
219
+ loaded_csv_image.zunit,
220
+ )
221
+ assert csv_units == units
222
+ csv_labels = (
223
+ loaded_csv_image.xlabel,
224
+ loaded_csv_image.ylabel,
225
+ loaded_csv_image.zlabel,
226
+ )
227
+ assert csv_labels == labels
228
+
229
+ # Most importantly: verify coordinate system is preserved
230
+ # Use allclose for coordinates too due to text serialization precision
231
+ assert not loaded_csv_image.is_uniform_coords
232
+ assert np.allclose(loaded_csv_image.xcoords, xcoords, atol=1e-10)
233
+ assert np.allclose(loaded_csv_image.ycoords, ycoords, atol=1e-10)
234
+
235
+ execenv.print(" ✓ Coordinated text CSV round-trip verification successful")
236
+
237
+ execenv.print(f"{test_nonuniform_coordinates_io.__doc__}: OK")
238
+
239
+
240
+ def test_uniform_coordinates_io() -> None:
241
+ """Test I/O (read and write) for coordinated text format
242
+ with uniform coordinates
243
+ """
244
+ execenv.print(f"{test_uniform_coordinates_io.__doc__}:")
245
+
246
+ # Create a test image with uniform coordinates
247
+ title = "Uniform Coordinates Test"
248
+ data = np.random.rand(10, 10)
249
+ metadata = {"test_key": "test_value", "coordinate_type": "uniform"}
250
+ units = ("mm", "mm", "intensity")
251
+ labels = ("X position", "Y position", "Signal")
252
+
253
+ # Create the image object
254
+ orig_image = sigima.objects.create_image(
255
+ title=title,
256
+ data=data,
257
+ metadata=metadata,
258
+ units=units,
259
+ labels=labels,
260
+ )
261
+
262
+ # Set uniform coordinates (dx, dy, x0, y0)
263
+ dx, dy, x0, y0 = 0.5, 0.3, 10.0, 20.0
264
+ orig_image.set_uniform_coords(dx, dy, x0=x0, y0=y0)
265
+
266
+ # Verify the original image has uniform coordinates
267
+ assert orig_image.is_uniform_coords
268
+ assert orig_image.dx == dx
269
+ assert orig_image.dy == dy
270
+ assert orig_image.x0 == x0
271
+ assert orig_image.y0 == y0
272
+
273
+ execenv.print(" ✓ Created uniform coordinate image")
274
+
275
+ with WorkdirRestoringTempDir() as tmpdir:
276
+ # Test text CSV format writing with uniform coordinates
277
+ # Note: uniform coordinates written as plain CSV (not coordinated text)
278
+ csv_filename = osp.join(tmpdir, "test_uniform_coords.csv")
279
+
280
+ # Save to CSV format
281
+ sigima.io.write_image(csv_filename, orig_image)
282
+ execenv.print(f" ✓ Saved to CSV: {csv_filename}")
283
+
284
+ # Read back from CSV
285
+ loaded_csv_image = sigima.io.read_image(csv_filename)
286
+ execenv.print(f" ✓ Loaded from CSV: {csv_filename}")
287
+
288
+ # Verify the loaded CSV image
289
+ assert isinstance(loaded_csv_image, sigima.objects.ImageObj)
290
+ assert loaded_csv_image.title == osp.basename(csv_filename)
291
+
292
+ # For CSV files, use allclose instead of array_equal due to
293
+ # floating-point precision loss during text serialization
294
+ assert np.allclose(loaded_csv_image.data, orig_image.data, atol=1e-10)
295
+
296
+ # Note: plain CSV format does NOT preserve units and labels
297
+ # So we don't check those here
298
+
299
+ # Important: verify coordinate system is NOT preserved for plain CSV
300
+ # Plain CSV files lose coordinate info, revert to default uniform coords
301
+ assert loaded_csv_image.is_uniform_coords
302
+ # Default uniform coordinates (dx=1, dy=1, x0=0, y0=0)
303
+ assert loaded_csv_image.dx == 1.0
304
+ assert loaded_csv_image.dy == 1.0
305
+ assert loaded_csv_image.x0 == 0.0
306
+ assert loaded_csv_image.y0 == 0.0
307
+
308
+ execenv.print(" ✓ CSV round-trip verification successful")
309
+ execenv.print(" ⚠ Note: Plain CSV does not preserve coordinate information")
310
+
311
+ execenv.print(f"{test_uniform_coordinates_io.__doc__}: OK")
312
+
313
+
314
+ def test_write_with_nan_values() -> None:
315
+ """Test writing coordinated text format with NaN values in data"""
316
+ execenv.print(f"{test_write_with_nan_values.__doc__}:")
317
+
318
+ # Create test image with NaN values
319
+ data = np.array(
320
+ [[1.0, 2.0, np.nan], [4.0, np.nan, 6.0], [np.nan, 8.0, 9.0]], dtype=float
321
+ )
322
+ title = "NaN Test"
323
+ units = ("mm", "mm", "V")
324
+ labels = ("X", "Y", "Voltage")
325
+
326
+ orig_image = sigima.objects.create_image(
327
+ title=title, data=data, units=units, labels=labels
328
+ )
329
+
330
+ # Set non-uniform coordinates to trigger coordinated text format
331
+ xcoords = np.array([0.0, 1.5, 4.0])
332
+ ycoords = np.array([0.0, 3.0, 7.0])
333
+ orig_image.set_coords(xcoords=xcoords, ycoords=ycoords)
334
+
335
+ execenv.print(" ✓ Created image with NaN values")
336
+
337
+ with WorkdirRestoringTempDir() as tmpdir:
338
+ csv_filename = osp.join(tmpdir, "test_nan.csv")
339
+
340
+ # Write with NaN values
341
+ sigima.io.write_image(csv_filename, orig_image)
342
+ execenv.print(f" ✓ Saved to CSV with NaN values: {csv_filename}")
343
+
344
+ # Read back
345
+ loaded_image = sigima.io.read_image(csv_filename)
346
+ execenv.print(f" ✓ Loaded from CSV: {csv_filename}")
347
+
348
+ # Verify NaN values are preserved
349
+ assert np.allclose(
350
+ loaded_image.data, orig_image.data, atol=1e-10, equal_nan=True
351
+ )
352
+ # Count NaN values
353
+ orig_nan_count = np.isnan(orig_image.data).sum()
354
+ loaded_nan_count = np.isnan(loaded_image.data).sum()
355
+ assert orig_nan_count == loaded_nan_count == 3
356
+ execenv.print(f" ✓ NaN values preserved ({loaded_nan_count} NaNs)")
357
+
358
+ # Verify coordinates
359
+ assert np.allclose(loaded_image.xcoords, xcoords, atol=1e-10)
360
+ assert np.allclose(loaded_image.ycoords, ycoords, atol=1e-10)
361
+
362
+ execenv.print(f"{test_write_with_nan_values.__doc__}: OK")
363
+
364
+
365
+ def test_polynomial_calibration_txt_io() -> None:
366
+ """Test I/O for images with polynomial calibration saved as TXT files"""
367
+ execenv.print(f"{test_polynomial_calibration_txt_io.__doc__}:")
368
+
369
+ # Create test image
370
+ data = np.random.rand(10, 10) * 100
371
+ orig_image = sigima.objects.create_image("Test", data)
372
+ orig_image.set_uniform_coords(dx=1.0, dy=1.0, x0=0.0, y0=0.0)
373
+
374
+ # Apply polynomial calibration on X axis (a0=0, a1=1, a2=0.001)
375
+ p = sigima.params.XYZCalibrateParam.create(axis="x", a0=0.0, a1=1.0, a2=0.001)
376
+ calibrated_image = sigima.proc.image.calibration(orig_image, p)
377
+
378
+ # Verify calibrated image has non-uniform coordinates
379
+ assert not calibrated_image.is_uniform_coords
380
+ execenv.print(" ✓ Created image with polynomial calibration")
381
+
382
+ with WorkdirRestoringTempDir() as tmpdir:
383
+ # Test TXT format
384
+ txt_filename = osp.join(tmpdir, "test_polynomial.txt")
385
+ sigima.io.write_image(txt_filename, calibrated_image)
386
+ execenv.print(f" ✓ Saved to TXT: {txt_filename}")
387
+
388
+ loaded_txt = sigima.io.read_image(txt_filename)
389
+ execenv.print(f" ✓ Loaded from TXT: {txt_filename}")
390
+
391
+ # Verify non-uniform coordinates are preserved
392
+ assert not loaded_txt.is_uniform_coords, (
393
+ "TXT file should preserve non-uniform coordinates"
394
+ )
395
+ assert np.allclose(loaded_txt.xcoords, calibrated_image.xcoords, atol=1e-10)
396
+ assert np.allclose(loaded_txt.ycoords, calibrated_image.ycoords, atol=1e-10)
397
+ assert np.allclose(loaded_txt.data, calibrated_image.data, atol=1e-10)
398
+ execenv.print(" ✓ Non-uniform coordinates preserved in TXT format")
399
+
400
+ # Test CSV format for comparison
401
+ csv_filename = osp.join(tmpdir, "test_polynomial.csv")
402
+ sigima.io.write_image(csv_filename, calibrated_image)
403
+ execenv.print(f" ✓ Saved to CSV: {csv_filename}")
404
+
405
+ loaded_csv = sigima.io.read_image(csv_filename)
406
+ execenv.print(f" ✓ Loaded from CSV: {csv_filename}")
407
+
408
+ # Verify both formats produce identical results
409
+ assert not loaded_csv.is_uniform_coords
410
+ assert np.allclose(loaded_csv.xcoords, loaded_txt.xcoords, atol=1e-10)
411
+ assert np.allclose(loaded_csv.ycoords, loaded_txt.ycoords, atol=1e-10)
412
+ assert np.allclose(loaded_csv.data, loaded_txt.data, atol=1e-10)
413
+ execenv.print(" ✓ TXT and CSV formats produce identical results")
414
+
415
+ execenv.print(f"{test_polynomial_calibration_txt_io.__doc__}: OK")
416
+
417
+
418
+ def test_metadata_type_restoration() -> None:
419
+ """Test that metadata types are correctly restored when reading text files"""
420
+ execenv.print(f"{test_metadata_type_restoration.__doc__}:")
421
+
422
+ # Create test image with various metadata types
423
+ data = np.random.rand(5, 5) * 100
424
+ orig_image = sigima.objects.create_image("TypeTest", data)
425
+
426
+ # Add metadata with different types
427
+ orig_image.metadata["int_value"] = 42
428
+ orig_image.metadata["negative_int"] = -123
429
+ orig_image.metadata["float_value"] = 3.14159
430
+ orig_image.metadata["negative_float"] = -2.71828
431
+ orig_image.metadata["scientific_float"] = 1.23e-5
432
+ orig_image.metadata["bool_true"] = True
433
+ orig_image.metadata["bool_false"] = False
434
+ orig_image.metadata["string_value"] = "hello world"
435
+
436
+ execenv.print(" ✓ Created image with mixed metadata types")
437
+
438
+ # Set non-uniform coordinates to trigger coordinated text format
439
+ xcoords = np.array([0.0, 1.0, 2.5, 4.0, 6.0])
440
+ ycoords = np.array([0.0, 1.0, 2.0, 3.5, 5.5])
441
+ orig_image.set_coords(xcoords=xcoords, ycoords=ycoords)
442
+
443
+ with WorkdirRestoringTempDir() as tmpdir:
444
+ filename = osp.join(tmpdir, "test_metadata_types.txt")
445
+
446
+ # Save to text file
447
+ sigima.io.write_image(filename, orig_image)
448
+ execenv.print(f" ✓ Saved to TXT: {filename}")
449
+
450
+ # Load it back
451
+ loaded_image = sigima.io.read_image(filename)
452
+ execenv.print(f" ✓ Loaded from TXT: {filename}")
453
+
454
+ # Verify integer types are restored
455
+ assert isinstance(loaded_image.metadata["int_value"], int)
456
+ assert loaded_image.metadata["int_value"] == 42
457
+ assert isinstance(loaded_image.metadata["negative_int"], int)
458
+ assert loaded_image.metadata["negative_int"] == -123
459
+ execenv.print(" ✓ Integer types restored correctly")
460
+
461
+ # Verify float types are restored
462
+ assert isinstance(loaded_image.metadata["float_value"], float)
463
+ assert abs(loaded_image.metadata["float_value"] - 3.14159) < 1e-10
464
+ assert isinstance(loaded_image.metadata["negative_float"], float)
465
+ assert abs(loaded_image.metadata["negative_float"] - (-2.71828)) < 1e-10
466
+ assert isinstance(loaded_image.metadata["scientific_float"], float)
467
+ assert abs(loaded_image.metadata["scientific_float"] - 1.23e-5) < 1e-15
468
+ execenv.print(" ✓ Float types restored correctly")
469
+
470
+ # Verify boolean types are restored
471
+ assert isinstance(loaded_image.metadata["bool_true"], bool)
472
+ assert loaded_image.metadata["bool_true"] is True
473
+ assert isinstance(loaded_image.metadata["bool_false"], bool)
474
+ assert loaded_image.metadata["bool_false"] is False
475
+ execenv.print(" ✓ Boolean types restored correctly")
476
+
477
+ # Verify string types are preserved
478
+ assert isinstance(loaded_image.metadata["string_value"], str)
479
+ assert loaded_image.metadata["string_value"] == "hello world"
480
+ execenv.print(" ✓ String types preserved correctly")
481
+
482
+ execenv.print(f"{test_metadata_type_restoration.__doc__}: OK")
483
+
484
+
485
+ if __name__ == "__main__":
486
+ test_read_image_basic()
487
+ test_read_image_with_unit()
488
+ test_read_image_with_nan()
489
+ test_read_complex_image_and_error()
490
+ test_read_nonuniform_coordinates()
491
+ test_nonuniform_coordinates_io()
492
+ test_uniform_coordinates_io()
493
+ test_write_with_nan_values()
494
+ test_polynomial_calibration_txt_io()
495
+ test_metadata_type_restoration()
@@ -0,0 +1,198 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ DateTime CSV I/O Unit Test
5
+ ==========================
6
+
7
+ Unit tests for reading CSV files with datetime columns.
8
+ """
9
+
10
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import os.path as osp
16
+ import tempfile
17
+ from datetime import datetime, timedelta
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+ import pytest
23
+
24
+ from sigima.io import read_signal, read_signals, write_signal
25
+ from sigima.io.signal.formats import CSVSignalFormat
26
+ from sigima.objects import SignalObj, create_signal
27
+ from sigima.tests.env import execenv
28
+ from sigima.tests.helpers import get_test_fnames
29
+
30
+
31
+ def test_datetime_csv_io() -> None:
32
+ """Test reading CSV file with datetime X column."""
33
+ execenv.print("Testing datetime CSV I/O...")
34
+
35
+ # Get path to the datetime test file
36
+ filenames = get_test_fnames("datetime.txt", in_folder="curve_formats")
37
+ assert len(filenames) > 0, "datetime.txt test file not found"
38
+ filename = filenames[0]
39
+ assert osp.exists(filename), f"Test file not found: {filename}"
40
+
41
+ # Read signals from file (read_signals returns a list, read_signal returns first)
42
+ signals = read_signals(filename)
43
+
44
+ # Should create multiple signals (one per Y column)
45
+ assert len(signals) > 0, "No signals were read from datetime.txt"
46
+ execenv.print(f" Read {len(signals)} signals from file")
47
+
48
+ # Test first signal (Temperature)
49
+ signal = signals[0]
50
+ execenv.print(f" First signal: {signal.title}")
51
+
52
+ # Check that datetime metadata was detected
53
+ assert signal.metadata.get("x_datetime", False), (
54
+ "DateTime metadata not detected in X column"
55
+ )
56
+ assert signal.xunit == "s", "DateTime unit should be 's' (seconds)"
57
+
58
+ # Check X data is float (timestamps)
59
+ assert isinstance(signal.x, np.ndarray), "X data should be numpy array"
60
+ assert signal.x.dtype in (np.float32, np.float64), "X data should be float"
61
+ execenv.print(f" X data type: {signal.x.dtype}")
62
+ execenv.print(f" X data shape: {signal.x.shape}")
63
+ execenv.print(f" First 5 X values: {signal.x[:5]}")
64
+
65
+ # Remove NaN values before checking monotonicity
66
+ x_clean = signal.x[~np.isnan(signal.x)]
67
+ execenv.print(f" Clean X shape (no NaNs): {x_clean.shape}")
68
+ execenv.print(f" First clean X value (timestamp): {x_clean[0]:.2f}")
69
+
70
+ # Check X values are monotonically increasing
71
+ assert np.all(np.diff(x_clean) >= 0), "X values should be monotonic"
72
+
73
+ # Check Y data exists and has correct length
74
+ assert isinstance(signal.y, np.ndarray), "Y data should be numpy array"
75
+ assert len(signal.x) == len(signal.y), "X and Y should have same length"
76
+ execenv.print(f" Y data shape: {signal.y.shape}")
77
+ execenv.print(f" First Y value: {signal.y[0]}")
78
+
79
+ # Test datetime conversion back
80
+ dt_values = signal.get_x_as_datetime()
81
+ assert dt_values is not None, "Should be able to get datetime values"
82
+ assert len(dt_values) == len(signal.x), (
83
+ "Datetime array should have same length as X"
84
+ )
85
+ execenv.print(f" First datetime value: {dt_values[0]}")
86
+
87
+ # Check that the datetime is reasonable (should be 2025-06-19 10:00:00)
88
+ # Convert to string to check
89
+ dt_str = pd.to_datetime(dt_values[0]).strftime("%Y-%m-%d %H:%M:%S")
90
+ expected_start = "2025-06-19 10:00:00"
91
+ assert dt_str == expected_start, (
92
+ f"Expected first datetime to be {expected_start}, got {dt_str}"
93
+ )
94
+
95
+ # Check labels were extracted correctly
96
+ assert signal.xlabel, "X label should be set"
97
+ assert signal.ylabel, "Y label should be set"
98
+ execenv.print(f" X label: {signal.xlabel}")
99
+ execenv.print(f" Y label: {signal.ylabel}")
100
+
101
+ # Check we have multiple signals (Temperature, Humidity, Dew Point)
102
+ assert len(signals) >= 3, "Should have at least 3 signals"
103
+ signal_titles = [s.ylabel for s in signals]
104
+ execenv.print(f" Signal Y labels: {signal_titles}")
105
+
106
+ # All signals should have the same datetime metadata
107
+ for sig in signals:
108
+ assert sig.is_x_datetime(), "All signals should have datetime X"
109
+ assert sig.xunit == "s"
110
+
111
+ # Check that all signals have the same X data (timestamps)
112
+ for sig in signals[1:]:
113
+ assert np.array_equal(sig.x, signals[0].x), (
114
+ "All signals should share the same X data"
115
+ )
116
+
117
+ execenv.print(" ✓ DateTime CSV I/O test passed")
118
+
119
+
120
+ def test_datetime_csv_write_with_datetime(tmp_path: Path):
121
+ """Test writing CSV file with datetime X values"""
122
+ # Create signal with datetime X
123
+ timestamps = [
124
+ datetime(2025, 1, 1, 10, 0, 0),
125
+ datetime(2025, 1, 1, 10, 0, 1),
126
+ datetime(2025, 1, 1, 10, 0, 2),
127
+ ]
128
+ signal = SignalObj()
129
+ signal.set_x_from_datetime(timestamps, unit="s")
130
+ signal.y = np.array([1.0, 2.0, 3.0])
131
+ signal.ylabel = "Temperature"
132
+ signal.xlabel = "Time"
133
+
134
+ # Write to CSV
135
+ csv_file = tmp_path / "datetime_signal.csv"
136
+ fmt = CSVSignalFormat()
137
+ fmt.write(str(csv_file), signal)
138
+
139
+ # Read file back and check contents
140
+ with open(csv_file, "r", encoding="utf-8") as f:
141
+ lines = f.readlines()
142
+
143
+ # Should have header + 3 data lines
144
+ assert len(lines) == 4
145
+ # Header should be "Time,Temperature"
146
+ assert "Time" in lines[0]
147
+ assert "Temperature" in lines[0]
148
+ # First data line should contain datetime string
149
+ assert "2025-01-01" in lines[1]
150
+ assert "10:00:00" in lines[1]
151
+
152
+
153
+ def test_datetime_csv_roundtrip() -> None:
154
+ """Test that datetime signals can be written and read back."""
155
+ execenv.print("Testing datetime CSV roundtrip...")
156
+
157
+ # Create a signal with datetime X
158
+ base_time = datetime(2025, 10, 6, 10, 0, 0)
159
+ timestamps = [base_time + timedelta(minutes=i * 5) for i in range(20)]
160
+ values = 20 + np.random.randn(20) * 2
161
+
162
+ signal = create_signal("Test Temperature")
163
+ signal.set_x_from_datetime(timestamps, unit="s")
164
+ signal.y = values
165
+ signal.ylabel = "Temperature"
166
+ signal.yunit = "°C"
167
+
168
+ # Write to temporary file
169
+ with tempfile.NamedTemporaryFile(
170
+ mode="w", suffix=".csv", delete=False, newline=""
171
+ ) as tmp:
172
+ tmp_path = tmp.name
173
+
174
+ try:
175
+ write_signal(tmp_path, signal)
176
+ execenv.print(f" Wrote signal to: {tmp_path}")
177
+
178
+ # Read it back
179
+ signal_read = read_signal(tmp_path)
180
+ execenv.print(" Signal read back successfully")
181
+
182
+ # Check datetime metadata is preserved
183
+ assert signal_read.is_x_datetime(), "DateTime metadata should be preserved"
184
+
185
+ # Check Y values match (X will be timestamps now, not exact datetime strings)
186
+ assert len(signal_read.y) == len(values), "Y length should match"
187
+ assert np.allclose(signal_read.y, values, atol=0.01), "Y values should match"
188
+
189
+ execenv.print(" ✓ DateTime CSV roundtrip test passed")
190
+
191
+ finally:
192
+ # Clean up temporary file
193
+ if osp.exists(tmp_path):
194
+ os.unlink(tmp_path)
195
+
196
+
197
+ if __name__ == "__main__":
198
+ pytest.main([__file__, "-v"])