sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,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"])
|