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,737 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests around the `ImageObj` class and its creation from parameters.
5
+ """
6
+
7
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
8
+ # pylint: disable=duplicate-code
9
+
10
+ from __future__ import annotations
11
+
12
+ import os.path as osp
13
+
14
+ import numpy as np
15
+ import pytest
16
+
17
+ import sigima.io
18
+ import sigima.objects
19
+ from sigima.io.image import ImageIORegistry
20
+ from sigima.objects.image import Gauss2DParam, Ramp2DParam
21
+ from sigima.tests import guiutils
22
+ from sigima.tests.data import (
23
+ create_annotated_image,
24
+ create_test_image_with_metadata,
25
+ iterate_image_creation,
26
+ )
27
+ from sigima.tests.env import execenv
28
+ from sigima.tests.helpers import (
29
+ WorkdirRestoringTempDir,
30
+ check_scalar_result,
31
+ compare_metadata,
32
+ read_test_objects,
33
+ )
34
+
35
+
36
+ def preprocess_image_parameters(param: sigima.objects.NewImageParam) -> None:
37
+ """Preprocess image parameters before creating the image.
38
+
39
+ Args:
40
+ param: The image parameters to preprocess.
41
+ """
42
+ if isinstance(param, Ramp2DParam):
43
+ param.a = 1.0
44
+ param.b = 2.0
45
+ param.c = 3.0
46
+ param.xmin = -1.0
47
+ param.xmax = 2.0
48
+ param.ymin = -5.0
49
+ param.ymax = 4.0
50
+ elif isinstance(param, Gauss2DParam):
51
+ param.x0 = param.y0 = 3.0
52
+ param.sigma = 5.0
53
+
54
+
55
+ def postprocess_image_object(
56
+ obj: sigima.objects.ImageObj, itype: sigima.objects.ImageTypes
57
+ ) -> None:
58
+ """Postprocess the image object after creation.
59
+
60
+ Args:
61
+ obj: The image object to postprocess.
62
+ itype: The type of the image.
63
+ """
64
+ if itype == sigima.objects.ImageTypes.ZEROS:
65
+ assert (obj.data == 0).all()
66
+ elif itype == sigima.objects.ImageTypes.RAMP:
67
+ assert obj.data is not None
68
+ check_scalar_result("Top-left corner", obj.data[0][0], -8.0)
69
+ check_scalar_result("Top-right corner", obj.data[0][-1], -5.0)
70
+ check_scalar_result("Bottom-left corner", obj.data[-1][0], 10.0)
71
+ check_scalar_result("Bottom-right", obj.data[-1][-1], 13.0)
72
+ else:
73
+ assert obj.data is not None
74
+
75
+
76
+ def test_all_image_types() -> None:
77
+ """Testing image creation from parameters"""
78
+ execenv.print(f"{test_all_image_types.__doc__}:")
79
+ for image in iterate_image_creation(
80
+ preproc=preprocess_image_parameters,
81
+ postproc=postprocess_image_object,
82
+ ):
83
+ assert image.data is not None
84
+ execenv.print(f"{test_all_image_types.__doc__}: OK")
85
+
86
+
87
+ def __get_filenames_and_images() -> list[tuple[str, sigima.objects.ImageObj]]:
88
+ """Get test filenames and images from the registry"""
89
+ fi_list = [
90
+ (fname, obj)
91
+ for fname, obj in read_test_objects(ImageIORegistry)
92
+ if obj is not None
93
+ ]
94
+ fi_list.append(("test_image_with_metadata", create_test_image_with_metadata()))
95
+ fi_list.append(("annotated_image", create_annotated_image()))
96
+ return fi_list
97
+
98
+
99
+ def test_hdf5_image_io() -> None:
100
+ """Test HDF5 I/O for image objects with uniform and non-uniform coordinates"""
101
+ execenv.print(f"{test_hdf5_image_io.__doc__}:")
102
+ with WorkdirRestoringTempDir() as tmpdir:
103
+ for fname, orig_image in __get_filenames_and_images():
104
+ if orig_image is None:
105
+ execenv.print(f" Skipping {fname} (not implemented)")
106
+ continue
107
+
108
+ # Test Case 1: Original image with uniform coordinates (default)
109
+ filename = osp.join(tmpdir, f"test_{osp.basename(fname)}_uniform.h5ima")
110
+ sigima.io.write_image(filename, orig_image)
111
+ execenv.print(f" Saved {filename} (uniform coords)")
112
+
113
+ # Read back
114
+ fetch_image = sigima.io.read_image(filename)
115
+ execenv.print(f" Read {filename}")
116
+
117
+ # Verify data
118
+ data = fetch_image.data
119
+ orig_data = orig_image.data
120
+ assert isinstance(data, np.ndarray)
121
+ assert isinstance(orig_data, np.ndarray)
122
+ assert data.shape == orig_data.shape
123
+ assert data.dtype == orig_data.dtype
124
+ assert fetch_image.annotations == orig_image.annotations
125
+ assert np.allclose(data, orig_data, atol=0.0, equal_nan=True)
126
+ compare_metadata(
127
+ fetch_image.metadata, orig_image.metadata.copy(), raise_on_diff=True
128
+ )
129
+
130
+ # Verify uniform coordinate attributes are preserved
131
+ if orig_image.is_uniform_coords:
132
+ assert fetch_image.is_uniform_coords
133
+ assert fetch_image.dx == orig_image.dx
134
+ assert fetch_image.dy == orig_image.dy
135
+ assert fetch_image.x0 == orig_image.x0
136
+ assert fetch_image.y0 == orig_image.y0
137
+ execenv.print(" ✓ Uniform coordinates preserved")
138
+
139
+ # Test Case 2: Same image with non-uniform coordinates
140
+ # Create a modified version with non-uniform coordinates
141
+ nonuniform_image = sigima.objects.create_image(
142
+ title=orig_image.title + " (non-uniform)",
143
+ data=orig_image.data.copy(),
144
+ metadata=orig_image.metadata.copy(),
145
+ units=(orig_image.xunit, orig_image.yunit, orig_image.zunit),
146
+ labels=(orig_image.xlabel, orig_image.ylabel, orig_image.zlabel),
147
+ )
148
+ # Set non-uniform coordinates
149
+ ny, nx = nonuniform_image.data.shape
150
+ xcoords = np.linspace(0, 1, nx)
151
+ ycoords = np.linspace(0, 1, ny) ** 2 # Quadratic spacing
152
+ nonuniform_image.set_coords(xcoords=xcoords, ycoords=ycoords)
153
+
154
+ # Save non-uniform version
155
+ filename_nu = osp.join(
156
+ tmpdir, f"test_{osp.basename(fname)}_nonuniform.h5ima"
157
+ )
158
+ sigima.io.write_image(filename_nu, nonuniform_image)
159
+ execenv.print(f" Saved {filename_nu} (non-uniform coords)")
160
+
161
+ # Read back
162
+ fetch_image_nu = sigima.io.read_image(filename_nu)
163
+ execenv.print(f" Read {filename_nu}")
164
+
165
+ # Verify data
166
+ assert np.allclose(
167
+ fetch_image_nu.data, nonuniform_image.data, atol=0.0, equal_nan=True
168
+ )
169
+
170
+ # Verify non-uniform coordinate attributes are preserved
171
+ assert not fetch_image_nu.is_uniform_coords
172
+ assert np.array_equal(fetch_image_nu.xcoords, xcoords)
173
+ assert np.array_equal(fetch_image_nu.ycoords, ycoords)
174
+ execenv.print(" ✓ Non-uniform coordinates preserved")
175
+
176
+ execenv.print(f"{test_hdf5_image_io.__doc__}: OK")
177
+
178
+
179
+ @pytest.mark.gui
180
+ def test_image_parameters_interactive() -> None:
181
+ """Test interactive creation of image parameters"""
182
+ execenv.print(f"{test_image_parameters_interactive.__doc__}:")
183
+ with guiutils.lazy_qt_app_context(force=True):
184
+ for itype in sigima.objects.ImageTypes:
185
+ param = sigima.objects.create_image_parameters(itype)
186
+ if param.edit():
187
+ execenv.print(f" Edited parameters for {itype.value}:")
188
+ execenv.print(f" {param}")
189
+ else:
190
+ execenv.print(f" Skipped editing parameters for {itype.value}")
191
+ execenv.print(f"{test_image_parameters_interactive.__doc__}: OK")
192
+
193
+
194
+ def test_create_image() -> None:
195
+ """Test creation of an image object using `create_image` function"""
196
+ execenv.print(f"{test_create_image.__doc__}:")
197
+ # pylint: disable=import-outside-toplevel
198
+
199
+ # Test all combinations of input parameters
200
+ title = "Some Image"
201
+ data = np.random.rand(10, 10)
202
+ metadata = {"key": "value"}
203
+ units = ("x unit", "y unit", "z unit")
204
+ labels = ("x label", "y label", "z label")
205
+
206
+ # 1. Create image with all parameters, and uniform coordinates
207
+ image = sigima.objects.create_image(
208
+ title=title,
209
+ data=data,
210
+ metadata=metadata,
211
+ units=units,
212
+ labels=labels,
213
+ )
214
+ assert isinstance(image, sigima.objects.ImageObj)
215
+ assert image.title == title
216
+ assert image.data is data # Data should be the same object (not a copy)
217
+ assert image.metadata == metadata
218
+ assert (image.xunit, image.yunit, image.zunit) == units
219
+ assert (image.xlabel, image.ylabel, image.zlabel) == labels
220
+ dx, dy, x0, y0 = 0.1, 0.2, 50.0, 100.0
221
+ image.set_uniform_coords(dx, dy, x0=x0, y0=y0)
222
+ assert image.is_uniform_coords
223
+ assert image.dx == dx
224
+ assert image.dy == dy
225
+ assert image.x0 == x0
226
+ assert image.y0 == y0
227
+
228
+ guiutils.view_images_if_gui(image, title=title)
229
+
230
+ # 2. Create image with non-uniform coordinates
231
+ xcoords = np.linspace(0, 1, 10)
232
+ ycoords = np.linspace(0, 1, 10) ** 2
233
+ image.set_coords(xcoords=xcoords, ycoords=ycoords)
234
+ assert not image.is_uniform_coords
235
+ assert np.array_equal(image.xcoords, xcoords)
236
+ assert np.array_equal(image.ycoords, ycoords)
237
+
238
+ guiutils.view_images_if_gui(image, title=title + " (non-uniform coords)")
239
+
240
+ # 3. Create image with only data
241
+ image = sigima.objects.create_image("", data=data)
242
+ assert isinstance(image, sigima.objects.ImageObj)
243
+ assert np.array_equal(image.data, data)
244
+ assert not image.metadata
245
+ assert (image.xunit, image.yunit, image.zunit) == ("", "", "")
246
+ assert (image.xlabel, image.ylabel, image.zlabel) == ("", "", "")
247
+
248
+ execenv.print(f"{test_create_image.__doc__}: OK")
249
+
250
+
251
+ def test_create_image_from_param() -> None:
252
+ """Test creation of an image object using `create_image_from_param` function"""
253
+ execenv.print(f"{test_create_image_from_param.__doc__}:")
254
+
255
+ # Test 1: Basic parameter with defaults
256
+ param = sigima.objects.NewImageParam()
257
+ param.title = "Test Image"
258
+ param.height = 100
259
+ param.width = 200
260
+ param.dtype = sigima.objects.ImageDatatypes.UINT16
261
+
262
+ image = sigima.objects.create_image_from_param(param)
263
+ assert isinstance(image, sigima.objects.ImageObj)
264
+ assert image.title == "Test Image"
265
+ assert image.data is not None
266
+ assert image.data.shape == (100, 200)
267
+ assert image.data.dtype == np.uint16
268
+ assert (image.data == 0).all() # NewImageParam generates zeros by default
269
+
270
+ # Test 2: Parameter with default values (no explicit setting)
271
+ param_defaults = sigima.objects.NewImageParam()
272
+ # Don't set any values, use defaults
273
+
274
+ image_defaults = sigima.objects.create_image_from_param(param_defaults)
275
+ assert isinstance(image_defaults, sigima.objects.ImageObj)
276
+ assert image_defaults.data is not None
277
+ assert image_defaults.data.shape == (1024, 1024) # Default dimensions
278
+ assert image_defaults.data.dtype == np.float64 # Default dtype from NewImageParam
279
+
280
+ # Test 3: Different image types using create_image_parameters
281
+ test_cases = [
282
+ (sigima.objects.ImageTypes.ZEROS, sigima.objects.ImageDatatypes.UINT8),
283
+ (
284
+ sigima.objects.ImageTypes.UNIFORM_DISTRIBUTION,
285
+ sigima.objects.ImageDatatypes.FLOAT32,
286
+ ),
287
+ (
288
+ sigima.objects.ImageTypes.NORMAL_DISTRIBUTION,
289
+ sigima.objects.ImageDatatypes.FLOAT64,
290
+ ),
291
+ (sigima.objects.ImageTypes.GAUSS, sigima.objects.ImageDatatypes.UINT16),
292
+ (sigima.objects.ImageTypes.RAMP, sigima.objects.ImageDatatypes.FLOAT64),
293
+ ]
294
+
295
+ for img_type, dtype in test_cases:
296
+ param_type = sigima.objects.create_image_parameters(
297
+ img_type,
298
+ title=f"Test {img_type.value}",
299
+ height=50,
300
+ width=60,
301
+ idtype=dtype,
302
+ )
303
+
304
+ # Preprocess parameters for specific types
305
+ preprocess_image_parameters(param_type)
306
+
307
+ image_type = sigima.objects.create_image_from_param(param_type)
308
+ assert isinstance(image_type, sigima.objects.ImageObj)
309
+ assert image_type.data is not None
310
+ assert image_type.data.shape == (50, 60)
311
+ assert image_type.data.dtype == dtype.value
312
+
313
+ # Validate image type-specific properties
314
+ if img_type == sigima.objects.ImageTypes.ZEROS:
315
+ assert (image_type.data == 0).all()
316
+ elif img_type == sigima.objects.ImageTypes.UNIFORM_DISTRIBUTION:
317
+ # Uniform distribution should have varying values
318
+ assert not (image_type.data == image_type.data[0, 0]).all()
319
+ assert np.isfinite(image_type.data).all()
320
+ elif img_type == sigima.objects.ImageTypes.NORMAL_DISTRIBUTION:
321
+ # Normal distribution should have reasonable values
322
+ assert not (image_type.data == 0).all()
323
+ assert np.isfinite(image_type.data).all()
324
+ elif img_type == sigima.objects.ImageTypes.GAUSS:
325
+ # 2D Gaussian should have non-zero values
326
+ assert not (image_type.data == 0).all()
327
+ assert np.isfinite(image_type.data).all()
328
+ elif img_type == sigima.objects.ImageTypes.RAMP:
329
+ # Ramp should have varying values
330
+ assert not (image_type.data == image_type.data[0, 0]).all()
331
+ assert np.isfinite(image_type.data).all()
332
+
333
+ # Test automatic title generation for distribution types
334
+ if "DISTRIBUTION" in img_type.name:
335
+ param_autotitle = sigima.objects.create_image_parameters(
336
+ img_type, title="", height=50, width=60, idtype=dtype
337
+ )
338
+ image_autotitle = sigima.objects.create_image_from_param(param_autotitle)
339
+ assert "Random" in image_autotitle.title, (
340
+ f"Auto-generated title should contain 'Random' for {img_type.value}"
341
+ )
342
+
343
+ # Test 4: Gaussian parameters with specific values
344
+ gauss_param = sigima.objects.Gauss2DParam()
345
+ gauss_param.title = "Custom Gauss"
346
+ gauss_param.height = 80
347
+ gauss_param.width = 80
348
+ gauss_param.dtype = sigima.objects.ImageDatatypes.FLOAT32
349
+
350
+ gauss_image = sigima.objects.create_image_from_param(gauss_param)
351
+ assert isinstance(gauss_image, sigima.objects.ImageObj)
352
+ assert gauss_image.title == "Custom Gauss"
353
+ assert gauss_image.data.shape == (80, 80)
354
+ assert gauss_image.data.dtype == np.float32
355
+ # Center should have highest value for Gaussian
356
+ center_val = gauss_image.data[40, 40]
357
+ corner_val = gauss_image.data[0, 0]
358
+ assert center_val > corner_val
359
+
360
+ # Test 5: Ramp parameters with specific values
361
+ ramp_param = sigima.objects.Ramp2DParam()
362
+ ramp_param.title = "Custom Ramp"
363
+ ramp_param.height = 60
364
+ ramp_param.width = 40
365
+ ramp_param.dtype = sigima.objects.ImageDatatypes.FLOAT64
366
+
367
+ ramp_image = sigima.objects.create_image_from_param(ramp_param)
368
+ assert isinstance(ramp_image, sigima.objects.ImageObj)
369
+ assert ramp_image.title == "Custom Ramp"
370
+ assert ramp_image.data.shape == (60, 40)
371
+ assert ramp_image.data.dtype == np.float64
372
+ # Ramp should have different values at different positions
373
+ assert ramp_image.data[0, 0] != ramp_image.data[-1, -1]
374
+
375
+ execenv.print(f"{test_create_image_from_param.__doc__}: OK")
376
+
377
+
378
+ def test_image_copy() -> None:
379
+ """Test copying image objects with uniform and non-uniform coordinates"""
380
+ execenv.print(f"{test_image_copy.__doc__}:")
381
+
382
+ # Create a base image with some data
383
+ data = np.random.rand(50, 60)
384
+ title = "Original Image"
385
+ metadata = {"key1": "value1", "key2": 42}
386
+ units = ("mm", "mm", "intensity")
387
+ labels = ("X axis", "Y axis", "Intensity")
388
+
389
+ # Test 1: Copy image with uniform coordinates
390
+ execenv.print(" Test 1: Copy image with uniform coordinates")
391
+ image_uniform = sigima.objects.create_image(
392
+ title=title,
393
+ data=data.copy(),
394
+ metadata=metadata.copy(),
395
+ units=units,
396
+ labels=labels,
397
+ )
398
+ dx, dy, x0, y0 = 0.5, 0.8, 10.0, 20.0
399
+ image_uniform.set_uniform_coords(dx, dy, x0=x0, y0=y0)
400
+ # Set some scale attributes
401
+ image_uniform.autoscale = False
402
+ image_uniform.xscalelog = True
403
+ image_uniform.xscalemin = 5.0
404
+ image_uniform.xscalemax = 25.0
405
+ image_uniform.yscalelog = False
406
+ image_uniform.yscalemin = 15.0
407
+ image_uniform.yscalemax = 35.0
408
+ image_uniform.zscalemin = 0.0
409
+ image_uniform.zscalemax = 1.0
410
+
411
+ # Copy the image
412
+ copied_uniform = image_uniform.copy()
413
+
414
+ # Verify the copy
415
+ assert copied_uniform is not image_uniform
416
+ assert copied_uniform.title == image_uniform.title
417
+ assert np.array_equal(copied_uniform.data, image_uniform.data)
418
+ assert copied_uniform.data is not image_uniform.data # Different array objects
419
+ assert copied_uniform.metadata == image_uniform.metadata
420
+ assert copied_uniform.metadata is not image_uniform.metadata
421
+ assert (copied_uniform.xunit, copied_uniform.yunit, copied_uniform.zunit) == units
422
+ assert (
423
+ copied_uniform.xlabel,
424
+ copied_uniform.ylabel,
425
+ copied_uniform.zlabel,
426
+ ) == labels
427
+
428
+ # Verify uniform coordinates are preserved
429
+ assert copied_uniform.is_uniform_coords == image_uniform.is_uniform_coords
430
+ assert copied_uniform.is_uniform_coords is True
431
+ assert copied_uniform.dx == dx
432
+ assert copied_uniform.dy == dy
433
+ assert copied_uniform.x0 == x0
434
+ assert copied_uniform.y0 == y0
435
+ execenv.print(" ✓ Uniform coordinates correctly copied")
436
+
437
+ # Verify scale attributes are preserved
438
+ assert copied_uniform.autoscale == image_uniform.autoscale
439
+ assert copied_uniform.xscalelog == image_uniform.xscalelog
440
+ assert copied_uniform.xscalemin == image_uniform.xscalemin
441
+ assert copied_uniform.xscalemax == image_uniform.xscalemax
442
+ assert copied_uniform.yscalelog == image_uniform.yscalelog
443
+ assert copied_uniform.yscalemin == image_uniform.yscalemin
444
+ assert copied_uniform.yscalemax == image_uniform.yscalemax
445
+ assert copied_uniform.zscalemin == image_uniform.zscalemin
446
+ assert copied_uniform.zscalemax == image_uniform.zscalemax
447
+ execenv.print(" ✓ Scale attributes correctly copied")
448
+
449
+ # Test 2: Copy image with non-uniform coordinates
450
+ execenv.print(" Test 2: Copy image with non-uniform coordinates")
451
+ image_nonuniform = sigima.objects.create_image(
452
+ title=title + " (non-uniform)",
453
+ data=data.copy(),
454
+ metadata=metadata.copy(),
455
+ units=units,
456
+ labels=labels,
457
+ )
458
+ # Create non-uniform coordinates (quadratic spacing)
459
+ ny, nx = data.shape
460
+ xcoords = np.linspace(0, 10, nx) ** 1.5
461
+ ycoords = np.linspace(0, 20, ny) ** 2
462
+ image_nonuniform.set_coords(xcoords=xcoords, ycoords=ycoords)
463
+
464
+ # Copy the image
465
+ copied_nonuniform = image_nonuniform.copy()
466
+
467
+ # Verify the copy
468
+ assert copied_nonuniform is not image_nonuniform
469
+ assert copied_nonuniform.title == image_nonuniform.title
470
+ assert np.array_equal(copied_nonuniform.data, image_nonuniform.data)
471
+ assert copied_nonuniform.data is not image_nonuniform.data
472
+ assert copied_nonuniform.metadata == image_nonuniform.metadata
473
+ assert copied_nonuniform.metadata is not image_nonuniform.metadata
474
+
475
+ # Verify non-uniform coordinates are preserved
476
+ assert copied_nonuniform.is_uniform_coords == image_nonuniform.is_uniform_coords
477
+ assert copied_nonuniform.is_uniform_coords is False
478
+ assert np.array_equal(copied_nonuniform.xcoords, xcoords)
479
+ assert np.array_equal(copied_nonuniform.ycoords, ycoords)
480
+ assert copied_nonuniform.xcoords is not image_nonuniform.xcoords
481
+ assert copied_nonuniform.ycoords is not image_nonuniform.ycoords
482
+ execenv.print(" ✓ Non-uniform coordinates correctly copied")
483
+
484
+ # Test 3: Copy with title override
485
+ execenv.print(" Test 3: Copy with custom title")
486
+ new_title = "Copied Image"
487
+ copied_with_title = image_uniform.copy(title=new_title)
488
+ assert copied_with_title.title == new_title
489
+ assert copied_with_title.is_uniform_coords is True
490
+ assert copied_with_title.dx == dx
491
+ execenv.print(" ✓ Title override works correctly")
492
+
493
+ # Test 4: Copy with dtype conversion
494
+ execenv.print(" Test 4: Copy with dtype conversion")
495
+ copied_uint16 = image_uniform.copy(dtype=np.uint16)
496
+ assert copied_uint16.data.dtype == np.uint16
497
+ assert copied_uint16.is_uniform_coords is True
498
+ assert copied_uint16.dx == dx
499
+ execenv.print(" ✓ Dtype conversion works correctly")
500
+
501
+ # Test 5: Copy with metadata filtering
502
+ execenv.print(" Test 5: Copy with metadata filtering")
503
+ copied_basic_meta = image_uniform.copy(all_metadata=False)
504
+ assert copied_basic_meta.is_uniform_coords is True
505
+ assert copied_basic_meta.dx == dx
506
+ execenv.print(" ✓ Metadata filtering works correctly")
507
+
508
+ execenv.print(f"{test_image_copy.__doc__}: OK")
509
+
510
+
511
+ def test_coordinate_conversion() -> None:
512
+ """Test physical_to_indices and indices_to_physical methods"""
513
+ execenv.print(f"{test_coordinate_conversion.__doc__}:")
514
+
515
+ # Create a test image
516
+ data = np.random.rand(100, 150)
517
+
518
+ # ==================== Test 1: Uniform coordinates ====================
519
+ execenv.print(" Test 1: Uniform coordinates - basic conversion")
520
+ image_uniform = sigima.objects.create_image(
521
+ title="Uniform Coordinates Test", data=data.copy()
522
+ )
523
+ dx, dy, x0, y0 = 0.5, 0.8, 10.0, 20.0
524
+ image_uniform.set_uniform_coords(dx, dy, x0=x0, y0=y0)
525
+
526
+ # Test basic forward conversion (physical → indices)
527
+ physical_coords = [10.0, 20.0, 15.0, 30.0] # Two points
528
+ indices = image_uniform.physical_to_indices(physical_coords)
529
+ assert len(indices) == 4
530
+ assert indices[0] == 0 # (10.0 - 10.0) / 0.5 = 0
531
+ assert indices[1] == 0 # (20.0 - 20.0) / 0.8 = 0
532
+ assert indices[2] == 10 # (15.0 - 10.0) / 0.5 = 10
533
+ assert indices[3] == 13 # (30.0 - 20.0) / 0.8 = 12.5 → 13 (floor(12.5 + 0.5))
534
+ execenv.print(" ✓ Forward conversion (physical → indices) correct")
535
+
536
+ # Test basic backward conversion (indices → physical)
537
+ indices_input = [0, 0, 10, 12]
538
+ coords = image_uniform.indices_to_physical(indices_input)
539
+ assert len(coords) == 4
540
+ assert coords[0] == 10.0 # 0 * 0.5 + 10.0 = 10.0
541
+ assert coords[1] == 20.0 # 0 * 0.8 + 20.0 = 20.0
542
+ assert coords[2] == 15.0 # 10 * 0.5 + 10.0 = 15.0
543
+ assert coords[3] == 29.6 # 12 * 0.8 + 20.0 = 29.6
544
+ execenv.print(" ✓ Backward conversion (indices → physical) correct")
545
+
546
+ # Test round-trip accuracy
547
+ execenv.print(" Test 2: Uniform coordinates - round-trip accuracy")
548
+ original_physical = [12.5, 25.6, 18.3, 35.2]
549
+ indices_rt = image_uniform.physical_to_indices(
550
+ original_physical, as_float=True
551
+ ) # Use float to preserve precision
552
+ recovered_physical = image_uniform.indices_to_physical(indices_rt)
553
+ np.testing.assert_allclose(recovered_physical, original_physical, rtol=1e-10)
554
+ execenv.print(" ✓ Round-trip (physical → indices → physical) preserves values")
555
+
556
+ # Test with origin offset and different pixel spacing
557
+ execenv.print(" Test 3: Uniform coordinates - with non-zero origin")
558
+ image_offset = sigima.objects.create_image(
559
+ title="Offset Origin Test", data=data.copy()
560
+ )
561
+ image_offset.set_uniform_coords(dx=2.0, dy=3.0, x0=-5.0, y0=-10.0)
562
+ phys = [-5.0, -10.0, 5.0, 20.0]
563
+ idx = image_offset.physical_to_indices(phys)
564
+ assert idx[0] == 0 # (-5.0 - (-5.0)) / 2.0 = 0
565
+ assert idx[1] == 0 # (-10.0 - (-10.0)) / 3.0 = 0
566
+ assert idx[2] == 5 # (5.0 - (-5.0)) / 2.0 = 5
567
+ assert idx[3] == 10 # (20.0 - (-10.0)) / 3.0 = 10
568
+ execenv.print(" ✓ Non-zero origin handled correctly")
569
+
570
+ # Test clipping to image boundaries
571
+ execenv.print(" Test 4: Uniform coordinates - clipping to boundaries")
572
+ out_of_bounds = [-100.0, -100.0, 1000.0, 1000.0]
573
+ clipped = image_uniform.physical_to_indices(out_of_bounds, clip=True)
574
+ assert clipped[0] == 0 # Clipped to minimum X index
575
+ assert clipped[1] == 0 # Clipped to minimum Y index
576
+ assert clipped[2] == data.shape[1] - 1 # Clipped to maximum X index (149)
577
+ assert clipped[3] == data.shape[0] - 1 # Clipped to maximum Y index (99)
578
+ execenv.print(" ✓ Clipping to image boundaries works correctly")
579
+
580
+ # Test as_float option
581
+ execenv.print(" Test 5: Uniform coordinates - float indices")
582
+ float_coords = [10.25, 20.4]
583
+ float_indices = image_uniform.physical_to_indices(float_coords, as_float=True)
584
+ int_indices = image_uniform.physical_to_indices(float_coords, as_float=False)
585
+ assert isinstance(float_indices[0], float)
586
+ assert isinstance(int_indices[0], (int, np.integer))
587
+ assert float_indices[0] == 0.5 # (10.25 - 10.0) / 0.5 = 0.5
588
+ assert int_indices[0] == 1 # floor(0.5 + 0.5) = 1
589
+ execenv.print(" ✓ as_float option works correctly")
590
+
591
+ # ==================== Test 6: Uniform to non-uniform conversion ==========
592
+ execenv.print(" Test 6: Converting uniform to non-uniform coordinates")
593
+ # Create a uniform image and test conversions
594
+ image_to_convert = sigima.objects.create_image(
595
+ title="Uniform to Non-uniform Test", data=data.copy()
596
+ )
597
+ dx_conv, dy_conv, x0_conv, y0_conv = 0.5, 0.8, 10.0, 20.0
598
+ image_to_convert.set_uniform_coords(dx_conv, dy_conv, x0=x0_conv, y0=y0_conv)
599
+
600
+ # Test conversions with uniform coordinates
601
+ test_phys = [12.5, 25.6, 18.3, 35.2]
602
+ indices_before = image_to_convert.physical_to_indices(test_phys, as_float=True)
603
+ physical_before = image_to_convert.indices_to_physical([10.0, 20.0, 50.0, 60.0])
604
+
605
+ # Convert to non-uniform coordinates
606
+ image_to_convert.switch_coords_to("non-uniform")
607
+ assert not image_to_convert.is_uniform_coords
608
+ assert len(image_to_convert.xcoords) == data.shape[1]
609
+ assert len(image_to_convert.ycoords) == data.shape[0]
610
+
611
+ # Verify the generated xcoords and ycoords match the uniform grid
612
+ expected_xcoords = np.linspace(
613
+ x0_conv, x0_conv + dx_conv * (data.shape[1] - 1), data.shape[1]
614
+ )
615
+ expected_ycoords = np.linspace(
616
+ y0_conv, y0_conv + dy_conv * (data.shape[0] - 1), data.shape[0]
617
+ )
618
+ np.testing.assert_allclose(image_to_convert.xcoords, expected_xcoords, rtol=1e-10)
619
+ np.testing.assert_allclose(image_to_convert.ycoords, expected_ycoords, rtol=1e-10)
620
+ execenv.print(" ✓ Generated non-uniform coords match uniform grid")
621
+
622
+ # Test that conversions give the same results after switching to non-uniform
623
+ indices_after = image_to_convert.physical_to_indices(test_phys, as_float=True)
624
+ physical_after = image_to_convert.indices_to_physical([10.0, 20.0, 50.0, 60.0])
625
+ np.testing.assert_allclose(indices_after, indices_before, rtol=1e-10)
626
+ np.testing.assert_allclose(physical_after, physical_before, rtol=1e-10)
627
+ execenv.print(" ✓ Coordinate conversions consistent after switch to non-uniform")
628
+
629
+ # ==================== Test 7: Non-uniform coordinates ====================
630
+ execenv.print(" Test 7: Non-uniform coordinates - basic conversion")
631
+ image_nonuniform = sigima.objects.create_image(
632
+ title="Non-Uniform Coordinates Test", data=data.copy()
633
+ )
634
+
635
+ # Create non-uniform coordinates with logarithmic spacing
636
+ ny, nx = data.shape
637
+ xcoords = np.logspace(0, 2, nx) # 1 to 100, logarithmic spacing
638
+ ycoords = np.linspace(0, 50, ny) ** 2 # 0 to 2500, quadratic spacing
639
+ image_nonuniform.set_coords(xcoords=xcoords, ycoords=ycoords)
640
+
641
+ # Test forward conversion with interpolation
642
+ phys_nu = [xcoords[0], ycoords[0], xcoords[10], ycoords[20]]
643
+ idx_nu = image_nonuniform.physical_to_indices(phys_nu, as_float=True)
644
+ assert abs(idx_nu[0] - 0.0) < 1e-10 # First X coord → index 0
645
+ assert abs(idx_nu[1] - 0.0) < 1e-10 # First Y coord → index 0
646
+ assert abs(idx_nu[2] - 10.0) < 1e-10 # 10th X coord → index 10
647
+ assert abs(idx_nu[3] - 20.0) < 1e-10 # 20th Y coord → index 20
648
+ execenv.print(" ✓ Non-uniform forward conversion correct")
649
+
650
+ # Test backward conversion with interpolation
651
+ idx_back = [0.0, 0.0, 10.0, 20.0]
652
+ coords_back = image_nonuniform.indices_to_physical(idx_back)
653
+ assert abs(coords_back[0] - xcoords[0]) < 1e-10
654
+ assert abs(coords_back[1] - ycoords[0]) < 1e-10
655
+ assert abs(coords_back[2] - xcoords[10]) < 1e-10
656
+ assert abs(coords_back[3] - ycoords[20]) < 1e-10
657
+ execenv.print(" ✓ Non-uniform backward conversion correct")
658
+
659
+ # Test round-trip for non-uniform coordinates
660
+ execenv.print(" Test 8: Non-uniform coordinates - round-trip accuracy")
661
+ original_nu = [xcoords[5], ycoords[15], xcoords[50], ycoords[75]]
662
+ indices_nu_rt = image_nonuniform.physical_to_indices(original_nu, as_float=True)
663
+ recovered_nu = image_nonuniform.indices_to_physical(indices_nu_rt)
664
+ np.testing.assert_allclose(recovered_nu, original_nu, rtol=1e-10)
665
+ execenv.print(" ✓ Round-trip for non-uniform coordinates preserves values")
666
+
667
+ # Test interpolation between grid points
668
+ execenv.print(" Test 9: Non-uniform coordinates - interpolation")
669
+ # Test a coordinate between grid points
670
+ mid_x = (xcoords[5] + xcoords[6]) / 2
671
+ mid_y = (ycoords[10] + ycoords[11]) / 2
672
+ mid_coords = [mid_x, mid_y]
673
+ mid_indices = image_nonuniform.physical_to_indices(mid_coords, as_float=True)
674
+ # Should be close to 5.5 and 10.5
675
+ assert 5.4 < mid_indices[0] < 5.6
676
+ assert 10.4 < mid_indices[1] < 10.6
677
+ execenv.print(" ✓ Interpolation between grid points works")
678
+
679
+ # ==================== Test 10: Edge cases ====================
680
+ execenv.print(" Test 10: Edge cases")
681
+
682
+ # Empty coordinate list
683
+ empty_coords = []
684
+ empty_indices = image_uniform.physical_to_indices(empty_coords)
685
+ assert len(empty_indices) == 0
686
+ execenv.print(" ✓ Empty coordinate list handled")
687
+
688
+ # Single point
689
+ single_point = [12.0, 25.0]
690
+ single_idx = image_uniform.physical_to_indices(single_point)
691
+ assert len(single_idx) == 2
692
+ execenv.print(" ✓ Single point conversion works")
693
+
694
+ # Multiple points
695
+ multi_points = [10.0, 20.0, 15.0, 30.0, 20.0, 40.0, 25.0, 50.0]
696
+ multi_idx = image_uniform.physical_to_indices(multi_points)
697
+ assert len(multi_idx) == 8
698
+ execenv.print(" ✓ Multiple points conversion works")
699
+
700
+ # Odd number of coordinates should raise ValueError
701
+ execenv.print(" Test 11: Error handling")
702
+ try:
703
+ image_uniform.physical_to_indices([10.0, 20.0, 15.0]) # Odd number
704
+ assert False, "Should have raised ValueError for odd number of coords"
705
+ except ValueError as e:
706
+ assert "even number" in str(e)
707
+ execenv.print(" ✓ ValueError raised for odd number of coordinates")
708
+
709
+ try:
710
+ image_uniform.indices_to_physical([0, 0, 5]) # Odd number
711
+ assert False, "Should have raised ValueError for odd number of indices"
712
+ except ValueError as e:
713
+ assert "even number" in str(e)
714
+ execenv.print(" ✓ ValueError raised for odd number of indices")
715
+
716
+ # Test clipping with non-uniform coordinates
717
+ execenv.print(" Test 12: Non-uniform coordinates - clipping")
718
+ out_of_bounds_nu = [-1000.0, -1000.0, 10000.0, 10000.0]
719
+ clipped_nu = image_nonuniform.physical_to_indices(out_of_bounds_nu, clip=True)
720
+ assert clipped_nu[0] == 0
721
+ assert clipped_nu[1] == 0
722
+ assert clipped_nu[2] == data.shape[1] - 1
723
+ assert clipped_nu[3] == data.shape[0] - 1
724
+ execenv.print(" ✓ Clipping works for non-uniform coordinates")
725
+
726
+ execenv.print(f"{test_coordinate_conversion.__doc__}: OK")
727
+
728
+
729
+ if __name__ == "__main__":
730
+ guiutils.enable_gui()
731
+ test_create_image()
732
+ test_image_parameters_interactive()
733
+ test_all_image_types()
734
+ test_hdf5_image_io()
735
+ test_create_image_from_param()
736
+ test_image_copy()
737
+ test_coordinate_conversion()