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,991 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Unit tests for scalar computation functions (GeometryResult transformations).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ import pytest
11
+
12
+ from sigima.objects import (
13
+ NO_ROI,
14
+ GeometryResult,
15
+ KindShape,
16
+ TableResult,
17
+ calc_table_from_data,
18
+ concat_geometries,
19
+ concat_tables,
20
+ filter_geometry_by_roi,
21
+ filter_table_by_roi,
22
+ )
23
+ from sigima.proc.image import transformer
24
+
25
+
26
+ def create_rectangle(x0=0.0, y0=0.0, w=1.0, h=1.0) -> GeometryResult:
27
+ """Create a simple rectangle GeometryResult."""
28
+ coords = np.array([[x0, y0, w, h]], dtype=float)
29
+ return GeometryResult("rect", "rectangle", coords)
30
+
31
+
32
+ class TestGeometryTransformations:
33
+ """Test class for geometry transformation functions."""
34
+
35
+ def test_rotate(self) -> None:
36
+ """Test rotation of a rectangle geometry result."""
37
+ rect = create_rectangle(0.0, 0.0, 1.0, 2.0)
38
+ rotated = transformer.rotate(rect, np.pi / 2, center=(0.5, 1.0))
39
+ expected_coords = np.array([[-0.5, 0.5, 2.0, 1.0]])
40
+ assert rotated.coords.shape == rect.coords.shape
41
+ assert np.allclose(rotated.coords, expected_coords)
42
+
43
+ def test_fliph(self) -> None:
44
+ """Test horizontal flip and its reversibility."""
45
+ rect = create_rectangle(1.0, 2.0, 2.0, 3.0)
46
+ flipped = transformer.fliph(rect, cx=2.0)
47
+ flipped_back = transformer.fliph(flipped, cx=2.0)
48
+ np.testing.assert_allclose(flipped_back.coords, rect.coords, rtol=1e-12)
49
+
50
+ def test_flipv(self) -> None:
51
+ """Test vertical flip and its reversibility."""
52
+ rect = create_rectangle(1.0, 2.0, 2.0, 3.0)
53
+ flipped = transformer.flipv(rect, cy=3.5)
54
+ flipped_back = transformer.flipv(flipped, cy=3.5)
55
+ np.testing.assert_allclose(flipped_back.coords, rect.coords, rtol=1e-12)
56
+
57
+ def test_translate(self) -> None:
58
+ """Test translation of a geometry result."""
59
+ rect = create_rectangle()
60
+ translated = transformer.translate(rect, 1.5, -2.0)
61
+ expected = rect.coords + np.array([1.5, -2.0, 0.0, 0.0])
62
+ np.testing.assert_allclose(translated.coords, expected, rtol=1e-12)
63
+
64
+ def test_scale(self) -> None:
65
+ """Test scaling and inverse scaling of a geometry result."""
66
+ rect = create_rectangle(1.0, 1.0, 2.0, 2.0)
67
+ scaled = transformer.scale(rect, 2.0, 0.5, center=(2.0, 2.0))
68
+ unscaled = transformer.scale(scaled, 0.5, 2.0, center=(2.0, 2.0))
69
+ np.testing.assert_allclose(unscaled.coords, rect.coords, rtol=1e-12)
70
+
71
+ def test_transpose(self) -> None:
72
+ """Test transpose and double-transpose (should restore original)."""
73
+ rect = create_rectangle(1.0, 2.0, 3.0, 4.0)
74
+ transposed = transformer.transpose(rect)
75
+ transposed_back = transformer.transpose(transposed)
76
+ np.testing.assert_allclose(transposed_back.coords, rect.coords, rtol=1e-12)
77
+
78
+
79
+ class TestTableResultInitialization:
80
+ """Test class for TableResult initialization and validation."""
81
+
82
+ def test_init_valid(self) -> None:
83
+ """Test TableResult initialization with valid data."""
84
+ data = [[1.0, 2.0], [3.0, 4.0]]
85
+ roi_indices = [0, 1]
86
+ table = TableResult(
87
+ title="Test Table",
88
+ headers=["col1", "col2"],
89
+ data=data,
90
+ roi_indices=roi_indices,
91
+ attrs={"method": "test"},
92
+ )
93
+ assert table.title == "Test Table"
94
+ assert list(table.headers) == ["col1", "col2"]
95
+ assert table.data == data
96
+ assert table.roi_indices == roi_indices
97
+ assert table.attrs == {"method": "test"}
98
+
99
+ def test_init_invalid_title(self) -> None:
100
+ """Test TableResult initialization with invalid title."""
101
+ with pytest.raises(ValueError, match="title must be a non-empty string"):
102
+ TableResult(title="", headers=["col"], data=[[1]])
103
+
104
+ def test_init_invalid_names(self) -> None:
105
+ """Test TableResult initialization with invalid headers."""
106
+ with pytest.raises(ValueError, match="names must be a sequence of strings"):
107
+ TableResult(title="Test", headers=[1, 2], data=[[1, 2]])
108
+
109
+ def test_init_invalid_data_shape(self) -> None:
110
+ """Test TableResult initialization with invalid data shape."""
111
+ with pytest.raises(ValueError, match="data must be a list of lists"):
112
+ TableResult(title="Test", headers=["col"], data=[1, 2, 3])
113
+
114
+ def test_init_invalid_data_columns(self) -> None:
115
+ """Test TableResult initialization with mismatched data columns."""
116
+ with pytest.raises(ValueError, match="data columns must match names length"):
117
+ TableResult(title="Test", headers=["col1", "col2"], data=[[1, 2, 3]])
118
+
119
+ def test_init_invalid_roi_indices(self) -> None:
120
+ """Test TableResult initialization with mismatched ROI indices length."""
121
+ with pytest.raises(
122
+ ValueError, match="roi_indices length must match number of data rows"
123
+ ):
124
+ TableResult(
125
+ title="Test",
126
+ headers=["col1", "col2"],
127
+ data=[[1, 2], [3, 4]],
128
+ roi_indices=[0],
129
+ )
130
+
131
+ def test_init_invalid_roi_indices_type(self) -> None:
132
+ """Test TableResult initialization with invalid roi_indices type."""
133
+ with pytest.raises(ValueError, match="roi_indices must be a list if provided"):
134
+ TableResult(
135
+ title="Test",
136
+ headers=["col1", "col2"],
137
+ data=[[1, 2], [3, 4]],
138
+ roi_indices=(0, 1),
139
+ )
140
+
141
+ def test_init_invalid_roi_indices_2d(self) -> None:
142
+ """Test TableResult initialization with 2D roi_indices."""
143
+ with pytest.raises(ValueError, match="roi_indices must be a list if provided"):
144
+ TableResult(
145
+ title="Test",
146
+ headers=["col1", "col2"],
147
+ data=[[1, 2], [3, 4]],
148
+ roi_indices=[[0], [1]],
149
+ )
150
+
151
+
152
+ class TestTableResultFactory:
153
+ """Test class for TableResult factory methods."""
154
+
155
+ def test_from_rows(self) -> None:
156
+ """Test TableResult.from_rows factory method."""
157
+ result = TableResult.from_rows(
158
+ title="Test",
159
+ headers=["col1", "col2"],
160
+ rows=[[1.0, 2.0], [3.0, 4.0]],
161
+ roi_indices=[0, 1],
162
+ attrs={"method": "test"},
163
+ )
164
+ assert result.title == "Test"
165
+ assert list(result.headers) == ["col1", "col2"]
166
+ assert result.data == [[1.0, 2.0], [3.0, 4.0]]
167
+ assert result.roi_indices == [0, 1]
168
+ assert result.attrs == {"method": "test"}
169
+
170
+
171
+ class TestTableResultSerialization:
172
+ """Test class for TableResult serialization methods."""
173
+
174
+ def test_to_dict(self) -> None:
175
+ """Test TableResult.to_dict serialization."""
176
+ table = TableResult(
177
+ title="Test Table",
178
+ headers=["col1", "col2"],
179
+ data=[[1.0, 2.0], [3.0, 4.0]],
180
+ roi_indices=[0, 1],
181
+ attrs={"method": "test"},
182
+ )
183
+ expected = {
184
+ "schema": 1,
185
+ "title": "Test Table",
186
+ "kind": "results",
187
+ "names": ["col1", "col2"],
188
+ "data": [[1.0, 2.0], [3.0, 4.0]],
189
+ "roi_indices": [0, 1],
190
+ "attrs": {"method": "test"},
191
+ }
192
+ assert table.to_dict() == expected
193
+
194
+ def test_from_dict(self) -> None:
195
+ """Test TableResult.from_dict deserialization."""
196
+ data = {
197
+ "title": "Test Table",
198
+ "kind": "results",
199
+ "names": ["col1", "col2"],
200
+ "data": [[1.0, 2.0], [3.0, 4.0]],
201
+ "roi_indices": [0, 1],
202
+ "attrs": {"method": "test"},
203
+ }
204
+ table = TableResult.from_dict(data)
205
+ assert table.title == "Test Table"
206
+ assert list(table.headers) == ["col1", "col2"]
207
+ assert table.data == [[1.0, 2.0], [3.0, 4.0]]
208
+ assert table.roi_indices == [0, 1]
209
+ assert table.attrs == {"method": "test"}
210
+
211
+
212
+ class TestTableResultDataAccess:
213
+ """Test class for TableResult data access methods."""
214
+
215
+ def setup_method(self) -> None:
216
+ """Set up test data for each test method."""
217
+ # pylint: disable=attribute-defined-outside-init
218
+ self.table = TableResult(
219
+ title="Test Table",
220
+ headers=["col1", "col2"],
221
+ data=[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
222
+ roi_indices=[NO_ROI, 0, 1],
223
+ )
224
+
225
+ def test_col(self) -> None:
226
+ """Test TableResult.col method."""
227
+ assert self.table.col("col1") == [1.0, 3.0, 5.0]
228
+ assert self.table.col("col2") == [2.0, 4.0, 6.0]
229
+
230
+ # Test missing column
231
+ with pytest.raises(KeyError, match="missing_col"):
232
+ self.table.col("missing_col")
233
+
234
+ def test_getitem(self) -> None:
235
+ """Test TableResult.__getitem__ method (shorthand for col)."""
236
+ assert self.table["col1"] == [1.0, 3.0, 5.0]
237
+ assert self.table["col2"] == [2.0, 4.0, 6.0]
238
+
239
+ def test_contains(self) -> None:
240
+ """Test TableResult.__contains__ method."""
241
+ assert "col1" in self.table
242
+ assert "col2" in self.table
243
+ assert "missing_col" not in self.table
244
+
245
+ def test_len(self) -> None:
246
+ """Test TableResult.__len__ method."""
247
+ assert len(self.table) == 2 # Number of columns
248
+
249
+ def test_value_single_row(self) -> None:
250
+ """Test TableResult.value with single row."""
251
+ single_row_table = TableResult(
252
+ title="Single", headers=["col1", "col2"], data=[[1.0, 2.0]]
253
+ )
254
+ assert single_row_table.value("col1") == 1.0
255
+ assert single_row_table.value("col2") == 2.0
256
+
257
+ def test_value_with_roi(self) -> None:
258
+ """Test TableResult.value with ROI filtering."""
259
+ # NO_ROI row
260
+ assert self.table.value("col1", roi=None) == 1.0
261
+ assert self.table.value("col2", roi=None) == 2.0
262
+
263
+ # ROI 0
264
+ assert self.table.value("col1", roi=0) == 3.0
265
+ assert self.table.value("col2", roi=0) == 4.0
266
+
267
+ # ROI 1
268
+ assert self.table.value("col1", roi=1) == 5.0
269
+ assert self.table.value("col2", roi=1) == 6.0
270
+
271
+ def test_value_ambiguous_selection(self) -> None:
272
+ """Test TableResult.value with ambiguous selection."""
273
+ multi_row_no_roi = TableResult(
274
+ title="Multi", headers=["col1"], data=[[1.0], [2.0]]
275
+ )
276
+ with pytest.raises(ValueError, match="Ambiguous selection"):
277
+ multi_row_no_roi.value("col1")
278
+
279
+ def test_value_duplicate_roi(self) -> None:
280
+ """Test TableResult.value with duplicate ROI indices."""
281
+ duplicate_roi_table = TableResult(
282
+ title="Dup",
283
+ headers=["col1"],
284
+ data=[[1.0], [2.0]],
285
+ roi_indices=[0, 0],
286
+ )
287
+ with pytest.raises(ValueError, match="Ambiguous selection"):
288
+ duplicate_roi_table.value("col1", roi=0)
289
+
290
+ def test_as_dict_single_row(self) -> None:
291
+ """Test TableResult.as_dict with single row."""
292
+ single_row_table = TableResult(
293
+ title="Single", headers=["col1", "col2"], data=[[1.0, 2.0]]
294
+ )
295
+ expected = {"col1": 1.0, "col2": 2.0}
296
+ assert single_row_table.as_dict() == expected
297
+
298
+ def test_as_dict_with_roi(self) -> None:
299
+ """Test TableResult.as_dict with ROI filtering."""
300
+ # NO_ROI row
301
+ expected_no_roi = {"col1": 1.0, "col2": 2.0}
302
+ assert self.table.as_dict(roi=None) == expected_no_roi
303
+
304
+ # ROI 0
305
+ expected_roi_0 = {"col1": 3.0, "col2": 4.0}
306
+ assert self.table.as_dict(roi=0) == expected_roi_0
307
+
308
+
309
+ class TestTableResultDisplayPreferences:
310
+ """Test class for TableResult display preferences functionality."""
311
+
312
+ def setup_method(self) -> None:
313
+ """Set up test data for each test method."""
314
+ # pylint: disable=attribute-defined-outside-init
315
+ self.table = TableResult(
316
+ title="Test Table",
317
+ headers=["col1", "col2", "col3"],
318
+ data=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
319
+ )
320
+
321
+ def test_display_preferences_default(self) -> None:
322
+ """Test default display preferences (all visible)."""
323
+ prefs = self.table.get_display_preferences()
324
+ expected = {"col1": True, "col2": True, "col3": True}
325
+ assert prefs == expected
326
+
327
+ visible = self.table.get_visible_headers()
328
+ assert visible == ["col1", "col2", "col3"]
329
+
330
+ def test_set_display_preferences(self) -> None:
331
+ """Test setting display preferences."""
332
+ self.table.set_display_preferences({"col1": True, "col2": False, "col3": True})
333
+
334
+ prefs = self.table.get_display_preferences()
335
+ expected = {"col1": True, "col2": False, "col3": True}
336
+ assert prefs == expected
337
+
338
+ visible = self.table.get_visible_headers()
339
+ assert visible == ["col1", "col3"]
340
+
341
+ def test_display_preferences_invalid_columns(self) -> None:
342
+ """Test setting display preferences with invalid column names."""
343
+ # Should ignore invalid columns
344
+ self.table.set_display_preferences(
345
+ {"col1": False, "invalid_col": False, "col3": True}
346
+ )
347
+
348
+ prefs = self.table.get_display_preferences()
349
+ expected = {"col1": False, "col2": True, "col3": True}
350
+ assert prefs == expected
351
+
352
+ def test_display_preferences_all_hidden(self) -> None:
353
+ """Test hiding all columns."""
354
+ self.table.set_display_preferences(
355
+ {"col1": False, "col2": False, "col3": False}
356
+ )
357
+
358
+ visible = self.table.get_visible_headers()
359
+ assert visible == []
360
+
361
+ def test_to_dataframe_visible_only_default(self) -> None:
362
+ """Test to_dataframe with visible_only=False (default)."""
363
+ df = self.table.to_dataframe(visible_only=False)
364
+ assert list(df.columns) == ["col1", "col2", "col3"]
365
+
366
+ def test_to_dataframe_visible_only_true(self) -> None:
367
+ """Test to_dataframe with visible_only=True."""
368
+ self.table.set_display_preferences({"col1": True, "col2": False, "col3": True})
369
+
370
+ df = self.table.to_dataframe(visible_only=True)
371
+ assert list(df.columns) == ["col1", "col3"]
372
+
373
+ def test_to_dataframe_visible_only_all_hidden(self) -> None:
374
+ """Test to_dataframe with all columns hidden."""
375
+ self.table.set_display_preferences(
376
+ {"col1": False, "col2": False, "col3": False}
377
+ )
378
+
379
+ df = self.table.to_dataframe(visible_only=True)
380
+ # When all columns are hidden, should return original dataframe
381
+ assert list(df.columns) == ["col1", "col2", "col3"]
382
+
383
+ def test_display_preferences_persistence(self) -> None:
384
+ """Test that display preferences persist through serialization."""
385
+ self.table.set_display_preferences({"col1": True, "col2": False, "col3": True})
386
+
387
+ # Serialize and deserialize
388
+ serialized = self.table.to_dict()
389
+ restored = TableResult.from_dict(serialized)
390
+
391
+ # Check preferences are preserved
392
+ prefs = restored.get_display_preferences()
393
+ expected = {"col1": True, "col2": False, "col3": True}
394
+ assert prefs == expected
395
+
396
+
397
+ class TestGeometryResultInitialization:
398
+ """Test class for GeometryResult initialization and validation."""
399
+
400
+ def test_init_valid_point(self) -> None:
401
+ """Test GeometryResult initialization with valid point data."""
402
+ coords = np.array([[1.0, 2.0], [3.0, 4.0]])
403
+ roi_indices = np.array([0, 1])
404
+ geom = GeometryResult(
405
+ title="Test Points",
406
+ kind=KindShape.POINT,
407
+ coords=coords,
408
+ roi_indices=roi_indices,
409
+ attrs={"method": "test"},
410
+ )
411
+ assert geom.title == "Test Points"
412
+ assert geom.kind == KindShape.POINT
413
+ np.testing.assert_array_equal(geom.coords, coords)
414
+ np.testing.assert_array_equal(geom.roi_indices, roi_indices)
415
+ assert geom.attrs == {"method": "test"}
416
+
417
+ def test_init_string_kind(self) -> None:
418
+ """Test GeometryResult initialization with string kind."""
419
+ coords = np.array([[1.0, 2.0]])
420
+ geom = GeometryResult("Test", "point", coords)
421
+ assert geom.kind == KindShape.POINT
422
+
423
+ def test_init_invalid_kind(self) -> None:
424
+ """Test GeometryResult initialization with invalid kind."""
425
+ coords = np.array([[1.0, 2.0]])
426
+ with pytest.raises(ValueError, match="Unsupported geometry kind"):
427
+ GeometryResult("Test", "invalid_shape", coords)
428
+
429
+ def test_init_invalid_title(self) -> None:
430
+ """Test GeometryResult initialization with invalid title."""
431
+ coords = np.array([[1.0, 2.0]])
432
+ with pytest.raises(ValueError, match="title must be a non-empty string"):
433
+ GeometryResult("", KindShape.POINT, coords)
434
+
435
+ def test_init_invalid_coords_shape(self) -> None:
436
+ """Test GeometryResult initialization with invalid coords shape."""
437
+ coords = np.array([1.0, 2.0]) # 1D instead of 2D
438
+ with pytest.raises(ValueError, match="coords must be a 2-D numpy array"):
439
+ GeometryResult("Test", KindShape.POINT, coords)
440
+
441
+ def test_init_point_wrong_columns(self) -> None:
442
+ """Test GeometryResult point initialization with wrong number of columns."""
443
+ coords = np.array([[1.0, 2.0, 3.0]]) # 3 columns instead of 2
444
+ with pytest.raises(ValueError, match="coords for 'point' must be \\(N,2\\)"):
445
+ GeometryResult("Test", KindShape.POINT, coords)
446
+
447
+ def test_init_segment_wrong_columns(self) -> None:
448
+ """Test GeometryResult segment initialization with wrong number of columns."""
449
+ coords = np.array([[1.0, 2.0, 3.0]]) # 3 columns instead of 4
450
+ with pytest.raises(ValueError, match="coords for 'segment' must be \\(N,4\\)"):
451
+ GeometryResult("Test", KindShape.SEGMENT, coords)
452
+
453
+ def test_init_circle_wrong_columns(self) -> None:
454
+ """Test GeometryResult circle initialization with wrong number of columns."""
455
+ coords = np.array([[1.0, 2.0]]) # 2 columns instead of 3
456
+ with pytest.raises(ValueError, match="coords for 'circle' must be \\(N,3\\)"):
457
+ GeometryResult("Test", KindShape.CIRCLE, coords)
458
+
459
+ def test_init_ellipse_wrong_columns(self) -> None:
460
+ """Test GeometryResult ellipse initialization with wrong number of columns."""
461
+ coords = np.array([[1.0, 2.0, 3.0, 4.0]]) # 4 columns instead of 5
462
+ with pytest.raises(ValueError, match="coords for 'ellipse' must be \\(N,5\\)"):
463
+ GeometryResult("Test", KindShape.ELLIPSE, coords)
464
+
465
+ def test_init_rectangle_wrong_columns(self) -> None:
466
+ """Test GeometryResult rectangle initialization with wrong columns."""
467
+ coords = np.array([[1.0, 2.0, 3.0]]) # 3 columns instead of 4
468
+ with pytest.raises(
469
+ ValueError, match="coords for 'rectangle' must be \\(N,4\\)"
470
+ ):
471
+ GeometryResult("Test", KindShape.RECTANGLE, coords)
472
+
473
+ def test_init_polygon_odd_columns(self) -> None:
474
+ """Test GeometryResult polygon initialization with odd number of columns."""
475
+ coords = np.array([[1.0, 2.0, 3.0]]) # 3 columns (odd)
476
+ with pytest.raises(
477
+ ValueError, match="coords for 'polygon' must be \\(N,2M\\) for M vertices"
478
+ ):
479
+ GeometryResult("Test", KindShape.POLYGON, coords)
480
+
481
+ def test_init_mismatched_roi_indices(self) -> None:
482
+ """Test GeometryResult initialization with mismatched ROI indices."""
483
+ coords = np.array([[1.0, 2.0], [3.0, 4.0]])
484
+ roi_indices = np.array([0]) # Only 1 element for 2 coords
485
+ with pytest.raises(
486
+ ValueError, match="roi_indices length must match number of coord rows"
487
+ ):
488
+ GeometryResult("Test", KindShape.POINT, coords, roi_indices)
489
+
490
+
491
+ class TestGeometryResultFactory:
492
+ """Test class for GeometryResult factory methods."""
493
+
494
+ def test_from_coords(self) -> None:
495
+ """Test GeometryResult.from_coords factory method."""
496
+ coords = [[1.0, 2.0], [3.0, 4.0]]
497
+ roi_indices = [0, 1]
498
+ geom = GeometryResult.from_coords(
499
+ title="Test Points",
500
+ kind=KindShape.POINT,
501
+ coords=coords,
502
+ roi_indices=roi_indices,
503
+ attrs={"method": "test"},
504
+ )
505
+ assert geom.title == "Test Points"
506
+ assert geom.kind == KindShape.POINT
507
+ np.testing.assert_array_equal(geom.coords, np.array(coords, dtype=float))
508
+ np.testing.assert_array_equal(
509
+ geom.roi_indices, np.array(roi_indices, dtype=int)
510
+ )
511
+ assert geom.attrs == {"method": "test"}
512
+
513
+
514
+ class TestGeometryResultSerialization:
515
+ """Test class for GeometryResult serialization methods."""
516
+
517
+ def test_to_dict(self) -> None:
518
+ """Test GeometryResult.to_dict serialization."""
519
+ coords = np.array([[1.0, 2.0], [3.0, 4.0]])
520
+ roi_indices = np.array([0, 1])
521
+ geom = GeometryResult(
522
+ title="Test Points",
523
+ kind=KindShape.POINT,
524
+ coords=coords,
525
+ roi_indices=roi_indices,
526
+ attrs={"method": "test"},
527
+ )
528
+ expected = {
529
+ "schema": 1,
530
+ "title": "Test Points",
531
+ "kind": "point",
532
+ "coords": [[1.0, 2.0], [3.0, 4.0]],
533
+ "roi_indices": [0, 1],
534
+ "attrs": {"method": "test"},
535
+ }
536
+ assert geom.to_dict() == expected
537
+
538
+ def test_from_dict(self) -> None:
539
+ """Test GeometryResult.from_dict deserialization."""
540
+ data = {
541
+ "title": "Test Points",
542
+ "kind": "point",
543
+ "coords": [[1.0, 2.0], [3.0, 4.0]],
544
+ "roi_indices": [0, 1],
545
+ "attrs": {"method": "test"},
546
+ }
547
+ geom = GeometryResult.from_dict(data)
548
+ assert geom.title == "Test Points"
549
+ assert geom.kind == KindShape.POINT
550
+ np.testing.assert_array_equal(geom.coords, np.array([[1.0, 2.0], [3.0, 4.0]]))
551
+ np.testing.assert_array_equal(geom.roi_indices, np.array([0, 1]))
552
+ assert geom.attrs == {"method": "test"}
553
+
554
+
555
+ class TestGeometryResultDataAccess:
556
+ """Test class for GeometryResult data access methods."""
557
+
558
+ def test_len(self) -> None:
559
+ """Test GeometryResult.__len__ method."""
560
+ coords = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
561
+ geom = GeometryResult("Test", KindShape.POINT, coords)
562
+ assert len(geom) == 3
563
+
564
+ def test_rows_no_roi(self) -> None:
565
+ """Test GeometryResult.rows with no ROI indices."""
566
+ coords = np.array([[1.0, 2.0], [3.0, 4.0]])
567
+ geom = GeometryResult("Test", KindShape.POINT, coords)
568
+ np.testing.assert_array_equal(geom.rows(), coords)
569
+ np.testing.assert_array_equal(geom.rows(roi=0), coords)
570
+
571
+ def test_rows_with_roi(self) -> None:
572
+ """Test GeometryResult.rows with ROI filtering."""
573
+ coords = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
574
+ roi_indices = np.array([NO_ROI, 0, 1])
575
+ geom = GeometryResult("Test", KindShape.POINT, coords, roi_indices)
576
+
577
+ # Test NO_ROI
578
+ expected_no_roi = np.array([[1.0, 2.0]])
579
+ np.testing.assert_array_equal(geom.rows(roi=None), expected_no_roi)
580
+
581
+ # Test ROI 0
582
+ expected_roi_0 = np.array([[3.0, 4.0]])
583
+ np.testing.assert_array_equal(geom.rows(roi=0), expected_roi_0)
584
+
585
+ # Test ROI 1
586
+ expected_roi_1 = np.array([[5.0, 6.0]])
587
+ np.testing.assert_array_equal(geom.rows(roi=1), expected_roi_1)
588
+
589
+
590
+ class TestGeometryResultShapeSpecific:
591
+ """Test class for shape-specific GeometryResult methods."""
592
+
593
+ def test_segments_lengths(self) -> None:
594
+ """Test GeometryResult.segments_lengths method."""
595
+ # Create segments: (0,0)-(3,4) and (1,1)-(4,5)
596
+ coords = np.array([[0.0, 0.0, 3.0, 4.0], [1.0, 1.0, 4.0, 5.0]])
597
+ geom = GeometryResult("Test", KindShape.SEGMENT, coords)
598
+ lengths = geom.segments_lengths()
599
+ expected = np.array([5.0, 5.0]) # Both segments have length 5
600
+ np.testing.assert_array_equal(lengths, expected)
601
+
602
+ def test_segments_lengths_wrong_kind(self) -> None:
603
+ """Test GeometryResult.segments_lengths with wrong kind."""
604
+ coords = np.array([[1.0, 2.0]])
605
+ geom = GeometryResult("Test", KindShape.POINT, coords)
606
+ with pytest.raises(
607
+ ValueError, match="segments_lengths requires kind='segment'"
608
+ ):
609
+ geom.segments_lengths()
610
+
611
+ def test_circles_radii(self) -> None:
612
+ """Test GeometryResult.circles_radii method."""
613
+ coords = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
614
+ geom = GeometryResult("Test", KindShape.CIRCLE, coords)
615
+ radii = geom.circles_radii()
616
+ expected = np.array([3.0, 6.0])
617
+ np.testing.assert_array_equal(radii, expected)
618
+
619
+ def test_circles_radii_wrong_kind(self) -> None:
620
+ """Test GeometryResult.circles_radii with wrong kind."""
621
+ coords = np.array([[1.0, 2.0]])
622
+ geom = GeometryResult("Test", KindShape.POINT, coords)
623
+ with pytest.raises(ValueError, match="circles_radii requires kind='circle'"):
624
+ geom.circles_radii()
625
+
626
+ def test_ellipse_axes_angles(self) -> None:
627
+ """Test GeometryResult.ellipse_axes_angles method."""
628
+ coords = np.array([[1.0, 2.0, 3.0, 4.0, 0.5], [5.0, 6.0, 7.0, 8.0, 1.0]])
629
+ geom = GeometryResult("Test", KindShape.ELLIPSE, coords)
630
+ a, b, theta = geom.ellipse_axes_angles()
631
+ np.testing.assert_array_equal(a, np.array([3.0, 7.0]))
632
+ np.testing.assert_array_equal(b, np.array([4.0, 8.0]))
633
+ np.testing.assert_array_equal(theta, np.array([0.5, 1.0]))
634
+
635
+ def test_ellipse_axes_angles_wrong_kind(self) -> None:
636
+ """Test GeometryResult.ellipse_axes_angles with wrong kind."""
637
+ coords = np.array([[1.0, 2.0]])
638
+ geom = GeometryResult("Test", KindShape.POINT, coords)
639
+ with pytest.raises(
640
+ ValueError, match="ellipse_axes_angles requires kind='ellipse'"
641
+ ):
642
+ geom.ellipse_axes_angles()
643
+
644
+
645
+ class TestGeometryResultDisplayPreferences:
646
+ """Test class for GeometryResult display preferences functionality."""
647
+
648
+ def test_display_preferences_default(self) -> None:
649
+ """Test default display preferences for rectangle (all visible)."""
650
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
651
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
652
+
653
+ prefs = geom.get_display_preferences()
654
+ expected = {"x": True, "y": True, "width": True, "height": True}
655
+ assert prefs == expected
656
+
657
+ visible = geom.get_visible_headers()
658
+ assert visible == ["x", "y", "width", "height"]
659
+
660
+ def test_set_display_preferences(self) -> None:
661
+ """Test setting display preferences for rectangle."""
662
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
663
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
664
+
665
+ geom.set_display_preferences(
666
+ {"x": True, "y": True, "width": False, "height": True}
667
+ )
668
+
669
+ prefs = geom.get_display_preferences()
670
+ expected = {"x": True, "y": True, "width": False, "height": True}
671
+ assert prefs == expected
672
+
673
+ visible = geom.get_visible_headers()
674
+ assert visible == ["x", "y", "height"]
675
+
676
+ def test_display_preferences_point(self) -> None:
677
+ """Test display preferences for point geometry."""
678
+ coords = np.array([[1.0, 2.0]])
679
+ geom = GeometryResult("Test Point", KindShape.POINT, coords)
680
+
681
+ # Test default
682
+ prefs = geom.get_display_preferences()
683
+ expected = {"x": True, "y": True}
684
+ assert prefs == expected
685
+
686
+ # Test setting preferences
687
+ geom.set_display_preferences({"x": False, "y": True})
688
+ prefs = geom.get_display_preferences()
689
+ expected = {"x": False, "y": True}
690
+ assert prefs == expected
691
+
692
+ def test_display_preferences_circle(self) -> None:
693
+ """Test display preferences for circle geometry."""
694
+ coords = np.array([[1.0, 2.0, 3.0]])
695
+ geom = GeometryResult("Test Circle", KindShape.CIRCLE, coords)
696
+
697
+ # Test default
698
+ prefs = geom.get_display_preferences()
699
+ expected = {"x": True, "y": True, "r": True}
700
+ assert prefs == expected
701
+
702
+ # Test setting preferences
703
+ geom.set_display_preferences({"x": True, "y": False, "r": True})
704
+ prefs = geom.get_display_preferences()
705
+ expected = {"x": True, "y": False, "r": True}
706
+ assert prefs == expected
707
+
708
+ def test_display_preferences_invalid_coords(self) -> None:
709
+ """Test setting display preferences with invalid coordinate names."""
710
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
711
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
712
+
713
+ # Should ignore invalid coordinates
714
+ geom.set_display_preferences(
715
+ {"x": False, "invalid_coord": False, "height": True}
716
+ )
717
+
718
+ prefs = geom.get_display_preferences()
719
+ expected = {"x": False, "y": True, "width": True, "height": True}
720
+ assert prefs == expected
721
+
722
+ def test_display_preferences_all_hidden(self) -> None:
723
+ """Test hiding all coordinates."""
724
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
725
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
726
+
727
+ geom.set_display_preferences(
728
+ {"x": False, "y": False, "width": False, "height": False}
729
+ )
730
+
731
+ visible = geom.get_visible_headers()
732
+ assert visible == []
733
+
734
+ def test_to_dataframe_visible_only_default(self) -> None:
735
+ """Test to_dataframe with visible_only=False (default)."""
736
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
737
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
738
+
739
+ df = geom.to_dataframe(visible_only=False)
740
+ assert list(df.columns) == ["x", "y", "width", "height"]
741
+
742
+ def test_to_dataframe_visible_only_true(self) -> None:
743
+ """Test to_dataframe with visible_only=True."""
744
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
745
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
746
+
747
+ geom.set_display_preferences(
748
+ {"x": True, "y": False, "width": True, "height": False}
749
+ )
750
+
751
+ df = geom.to_dataframe(visible_only=True)
752
+ assert list(df.columns) == ["x", "width"]
753
+
754
+ def test_to_dataframe_visible_only_all_hidden(self) -> None:
755
+ """Test to_dataframe with all coordinates hidden."""
756
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
757
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
758
+
759
+ geom.set_display_preferences(
760
+ {"x": False, "y": False, "width": False, "height": False}
761
+ )
762
+
763
+ df = geom.to_dataframe(visible_only=True)
764
+ # When all coordinates are hidden, should return original dataframe
765
+ assert list(df.columns) == ["x", "y", "width", "height"]
766
+
767
+ def test_display_preferences_persistence(self) -> None:
768
+ """Test that display preferences persist through serialization."""
769
+ coords = np.array([[0.0, 0.0, 10.0, 5.0]])
770
+ geom = GeometryResult("Test Rectangle", KindShape.RECTANGLE, coords)
771
+
772
+ geom.set_display_preferences(
773
+ {"x": True, "y": False, "width": True, "height": False}
774
+ )
775
+
776
+ # Serialize and deserialize
777
+ serialized = geom.to_dict()
778
+ restored = GeometryResult.from_dict(serialized)
779
+
780
+ # Check preferences are preserved
781
+ prefs = restored.get_display_preferences()
782
+ expected = {"x": True, "y": False, "width": True, "height": False}
783
+ assert prefs == expected
784
+
785
+
786
+ class TestUtilityFunctions:
787
+ """Test class for utility functions (calc_table_from_data, concat_*, filter_*)."""
788
+
789
+ def test_calc_table_from_data_no_roi(self) -> None:
790
+ """Test calc_table_from_data without ROI masks."""
791
+ data = np.array([1, 2, 3, 4, 5])
792
+ labeledfuncs = {"mean": np.mean, "std": np.std}
793
+ result = calc_table_from_data("Stats", data, labeledfuncs)
794
+
795
+ assert result.title == "Stats"
796
+ assert list(result.headers) == ["mean", "std"]
797
+ assert len(result.data) == 1
798
+ assert result.roi_indices == [NO_ROI]
799
+ assert result.data[0][0] == 3.0 # mean
800
+ assert abs(result.data[0][1] - np.std(data)) < 1e-10 # std
801
+
802
+ def test_calc_table_from_data_with_roi(self) -> None:
803
+ """Test calc_table_from_data with ROI masks."""
804
+ data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
805
+ roi_masks = [
806
+ np.array(
807
+ [[True, True, False], [False, False, False], [False, False, False]]
808
+ ),
809
+ np.array(
810
+ [[False, False, False], [False, True, True], [False, False, False]]
811
+ ),
812
+ ]
813
+ labeledfuncs = {"mean": np.mean, "sum": np.sum}
814
+ result = calc_table_from_data("ROI Stats", data, labeledfuncs, roi_masks)
815
+
816
+ assert result.title == "ROI Stats"
817
+ assert list(result.headers) == ["mean", "sum"]
818
+ assert len(result.data) == 2
819
+ assert result.roi_indices == [0, 1]
820
+ # ROI 0: mean of [1, 2] = 1.5, sum = 3
821
+ assert result.data[0][0] == 1.5
822
+ assert result.data[0][1] == 3.0
823
+ # ROI 1: mean of [5, 6] = 5.5, sum = 11
824
+ assert result.data[1][0] == 5.5
825
+ assert result.data[1][1] == 11.0
826
+
827
+ def test_concat_tables_empty(self) -> None:
828
+ """Test concat_tables with empty list."""
829
+ result = concat_tables("Empty", [])
830
+ assert result.title == "Empty"
831
+ assert result.headers == []
832
+ assert not result.data
833
+
834
+ def test_concat_tables_single(self) -> None:
835
+ """Test concat_tables with single table."""
836
+ table = TableResult("Single", headers=["col1"], data=[[1.0]])
837
+ result = concat_tables("Concat", [table])
838
+ assert result.title == "Concat"
839
+ assert list(result.headers) == ["col1"]
840
+ assert result.data == [[1.0]]
841
+
842
+ def test_concat_tables_multiple(self) -> None:
843
+ """Test concat_tables with multiple tables."""
844
+ table1 = TableResult(
845
+ "Table1",
846
+ headers=["col1", "col2"],
847
+ data=[[1.0, 2.0]],
848
+ roi_indices=[0],
849
+ )
850
+ table2 = TableResult(
851
+ "Table2",
852
+ headers=["col1", "col2"],
853
+ data=[[3.0, 4.0], [5.0, 6.0]],
854
+ roi_indices=[1, 2],
855
+ )
856
+ result = concat_tables("Combined", [table1, table2])
857
+
858
+ assert result.title == "Combined"
859
+ assert list(result.headers) == ["col1", "col2"]
860
+ assert result.data == [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]
861
+ assert result.roi_indices == [0, 1, 2]
862
+
863
+ def test_concat_tables_mismatched_names(self) -> None:
864
+ """Test concat_tables with mismatched column names."""
865
+ table1 = TableResult("Table1", headers=["col1"], data=[[1.0]])
866
+ table2 = TableResult("Table2", headers=["col2"], data=[[2.0]])
867
+ with pytest.raises(
868
+ ValueError, match="All TableResult objects must share the same names"
869
+ ):
870
+ concat_tables("Mismatched", [table1, table2])
871
+
872
+ def test_filter_table_by_roi_no_roi_indices(self) -> None:
873
+ """Test filter_table_by_roi with no ROI indices."""
874
+ table = TableResult("Test", headers=["col1"], data=[[1.0], [2.0]])
875
+
876
+ # Filter for None should keep all
877
+ result_none = filter_table_by_roi(table, None)
878
+ assert result_none.data == [[1.0], [2.0]]
879
+
880
+ # Filter for specific ROI should return empty
881
+ result_roi = filter_table_by_roi(table, 0)
882
+ assert not result_roi.data
883
+
884
+ def test_filter_table_by_roi_with_roi_indices(self) -> None:
885
+ """Test filter_table_by_roi with ROI indices."""
886
+ table = TableResult(
887
+ "Test",
888
+ headers=["col1"],
889
+ data=[[1.0], [2.0], [3.0]],
890
+ roi_indices=[NO_ROI, 0, 1],
891
+ )
892
+
893
+ # Filter for NO_ROI
894
+ result_none = filter_table_by_roi(table, None)
895
+ assert result_none.data == [[1.0]]
896
+ assert result_none.roi_indices == [NO_ROI]
897
+
898
+ # Filter for ROI 0
899
+ result_roi_0 = filter_table_by_roi(table, 0)
900
+ assert result_roi_0.data == [[2.0]]
901
+ assert result_roi_0.roi_indices == [0]
902
+
903
+ def test_concat_geometries_empty(self) -> None:
904
+ """Test concat_geometries with empty list."""
905
+ result = concat_geometries("Empty", [])
906
+ assert result.title == "Empty"
907
+ assert result.kind == KindShape.POINT
908
+ assert result.coords.shape == (0, 2)
909
+
910
+ def test_concat_geometries_single(self) -> None:
911
+ """Test concat_geometries with single geometry."""
912
+ coords = np.array([[1.0, 2.0]])
913
+ geom = GeometryResult("Single", KindShape.POINT, coords)
914
+ result = concat_geometries("Concat", [geom])
915
+ assert result.title == "Concat"
916
+ assert result.kind == KindShape.POINT
917
+ np.testing.assert_array_equal(result.coords, coords)
918
+
919
+ def test_concat_geometries_multiple(self) -> None:
920
+ """Test concat_geometries with multiple geometries."""
921
+ coords1 = np.array([[1.0, 2.0]])
922
+ coords2 = np.array([[3.0, 4.0], [5.0, 6.0]])
923
+ geom1 = GeometryResult("Geom1", KindShape.POINT, coords1, np.array([0]))
924
+ geom2 = GeometryResult("Geom2", KindShape.POINT, coords2, np.array([1, 2]))
925
+ result = concat_geometries("Combined", [geom1, geom2])
926
+
927
+ assert result.title == "Combined"
928
+ assert result.kind == KindShape.POINT
929
+ expected_coords = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
930
+ np.testing.assert_array_equal(result.coords, expected_coords)
931
+ np.testing.assert_array_equal(result.roi_indices, np.array([0, 1, 2]))
932
+
933
+ def test_concat_geometries_different_widths(self) -> None:
934
+ """Test concat_geometries with different coordinate widths."""
935
+ # Two polygons with different number of vertices
936
+ coords1 = np.array([[1.0, 2.0, 3.0, 4.0]]) # 2 vertices
937
+ coords2 = np.array([[5.0, 6.0, 7.0, 8.0, 9.0, 10.0]]) # 3 vertices
938
+ geom1 = GeometryResult("Poly1", KindShape.POLYGON, coords1)
939
+ geom2 = GeometryResult("Poly2", KindShape.POLYGON, coords2)
940
+
941
+ result = concat_geometries("Mixed", [geom1, geom2])
942
+ expected_coords = np.array(
943
+ [[1.0, 2.0, 3.0, 4.0, np.nan, np.nan], [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]]
944
+ )
945
+ np.testing.assert_array_equal(result.coords[:, :4], expected_coords[:, :4])
946
+ assert np.isnan(result.coords[0, 4])
947
+ assert np.isnan(result.coords[0, 5])
948
+ assert result.coords[1, 4] == 9.0
949
+ assert result.coords[1, 5] == 10.0
950
+
951
+ def test_concat_geometries_mismatched_kinds(self) -> None:
952
+ """Test concat_geometries with mismatched kinds."""
953
+ coords1 = np.array([[1.0, 2.0]])
954
+ coords2 = np.array([[3.0, 4.0, 5.0]])
955
+ geom1 = GeometryResult("Point", KindShape.POINT, coords1)
956
+ geom2 = GeometryResult("Circle", KindShape.CIRCLE, coords2)
957
+ with pytest.raises(
958
+ ValueError, match="All GeometryResult objects must share the same kind"
959
+ ):
960
+ concat_geometries("Mismatched", [geom1, geom2])
961
+
962
+ def test_filter_geometry_by_roi_no_roi_indices(self) -> None:
963
+ """Test filter_geometry_by_roi with no ROI indices."""
964
+ coords = np.array([[1.0, 2.0], [3.0, 4.0]])
965
+ geom = GeometryResult("Test", KindShape.POINT, coords)
966
+
967
+ # Filter for None should keep all
968
+ result_none = filter_geometry_by_roi(geom, None)
969
+ np.testing.assert_array_equal(result_none.coords, coords)
970
+
971
+ # Filter for specific ROI should return empty
972
+ result_roi = filter_geometry_by_roi(geom, 0)
973
+ assert result_roi.coords.shape == (0, 2)
974
+
975
+ def test_filter_geometry_by_roi_with_roi_indices(self) -> None:
976
+ """Test filter_geometry_by_roi with ROI indices."""
977
+ coords = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
978
+ roi_indices = np.array([NO_ROI, 0, 1])
979
+ geom = GeometryResult("Test", KindShape.POINT, coords, roi_indices)
980
+
981
+ # Filter for NO_ROI
982
+ result_none = filter_geometry_by_roi(geom, None)
983
+ expected_coords = np.array([[1.0, 2.0]])
984
+ np.testing.assert_array_equal(result_none.coords, expected_coords)
985
+ np.testing.assert_array_equal(result_none.roi_indices, np.array([NO_ROI]))
986
+
987
+ # Filter for ROI 0
988
+ result_roi_0 = filter_geometry_by_roi(geom, 0)
989
+ expected_coords = np.array([[3.0, 4.0]])
990
+ np.testing.assert_array_equal(result_roi_0.coords, expected_coords)
991
+ np.testing.assert_array_equal(result_roi_0.roi_indices, np.array([0]))