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