tracksdata 0.1.0rc0__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 (82) hide show
  1. tracksdata/__about__.py +34 -0
  2. tracksdata/__init__.py +41 -0
  3. tracksdata/_test/test_attrs.py +628 -0
  4. tracksdata/_test/test_options.py +149 -0
  5. tracksdata/array/__init__.py +9 -0
  6. tracksdata/array/_base_array.py +55 -0
  7. tracksdata/array/_graph_array.py +324 -0
  8. tracksdata/array/_nd_chunk_cache.py +158 -0
  9. tracksdata/array/_test/test_graph_array.py +349 -0
  10. tracksdata/array/_test/test_nd_chunk_cache.py +118 -0
  11. tracksdata/attrs.py +681 -0
  12. tracksdata/conftest.py +104 -0
  13. tracksdata/constants.py +71 -0
  14. tracksdata/edges/__init__.py +7 -0
  15. tracksdata/edges/_base_edge_attrs.py +83 -0
  16. tracksdata/edges/_base_edges.py +79 -0
  17. tracksdata/edges/_distance_edges.py +203 -0
  18. tracksdata/edges/_generic_edges.py +127 -0
  19. tracksdata/edges/_iou_edges.py +28 -0
  20. tracksdata/edges/_test/test_distance_edges.py +314 -0
  21. tracksdata/edges/_test/test_generic_edges.py +231 -0
  22. tracksdata/edges/_test/test_iou_edges.py +200 -0
  23. tracksdata/functional/__init__.py +6 -0
  24. tracksdata/functional/_edges.py +80 -0
  25. tracksdata/functional/_iou.py +100 -0
  26. tracksdata/functional/_napari.py +155 -0
  27. tracksdata/functional/_rx.py +314 -0
  28. tracksdata/functional/_test/test_functional_edges.py +72 -0
  29. tracksdata/functional/_test/test_iou.py +304 -0
  30. tracksdata/functional/_test/test_napari.py +64 -0
  31. tracksdata/functional/_test/test_rx.py +197 -0
  32. tracksdata/graph/__init__.py +10 -0
  33. tracksdata/graph/_base_graph.py +1395 -0
  34. tracksdata/graph/_graph_view.py +757 -0
  35. tracksdata/graph/_mapped_graph_mixin.py +238 -0
  36. tracksdata/graph/_rustworkx_graph.py +1728 -0
  37. tracksdata/graph/_sql_graph.py +1573 -0
  38. tracksdata/graph/_test/test_graph_backends.py +1798 -0
  39. tracksdata/graph/_test/test_index_graph.py +199 -0
  40. tracksdata/graph/_test/test_subgraph.py +1218 -0
  41. tracksdata/graph/filters/__init__.py +3 -0
  42. tracksdata/graph/filters/_base_filter.py +113 -0
  43. tracksdata/graph/filters/_indexed_filter.py +84 -0
  44. tracksdata/graph/filters/_spatial_filter.py +323 -0
  45. tracksdata/graph/filters/_test/test_spatial_filter.py +231 -0
  46. tracksdata/io/__init__.py +5 -0
  47. tracksdata/io/_ctc.py +293 -0
  48. tracksdata/io/_numpy_array.py +155 -0
  49. tracksdata/io/_test/test_ctc_io.py +77 -0
  50. tracksdata/metrics/__init__.py +11 -0
  51. tracksdata/metrics/_ctc_metrics.py +356 -0
  52. tracksdata/metrics/_test/test_ctc_metrics.py +127 -0
  53. tracksdata/metrics/_test/test_metrics_visualize.py +98 -0
  54. tracksdata/metrics/_visualize.py +257 -0
  55. tracksdata/nodes/__init__.py +8 -0
  56. tracksdata/nodes/_base_node_attrs.py +86 -0
  57. tracksdata/nodes/_base_nodes.py +32 -0
  58. tracksdata/nodes/_generic_nodes.py +183 -0
  59. tracksdata/nodes/_mask.py +333 -0
  60. tracksdata/nodes/_random.py +172 -0
  61. tracksdata/nodes/_regionprops.py +301 -0
  62. tracksdata/nodes/_test/test_generic_nodes.py +472 -0
  63. tracksdata/nodes/_test/test_mask.py +293 -0
  64. tracksdata/nodes/_test/test_random.py +264 -0
  65. tracksdata/nodes/_test/test_regionprops.py +280 -0
  66. tracksdata/options.py +165 -0
  67. tracksdata/solvers/__init__.py +6 -0
  68. tracksdata/solvers/_base_solver.py +48 -0
  69. tracksdata/solvers/_ilp_solver.py +410 -0
  70. tracksdata/solvers/_nearest_neighbors_solver.py +307 -0
  71. tracksdata/solvers/_test/test_ilp_solver.py +676 -0
  72. tracksdata/solvers/_test/test_nearest_neighbors_solver.py +384 -0
  73. tracksdata/utils/__init__.py +1 -0
  74. tracksdata/utils/_dataframe.py +52 -0
  75. tracksdata/utils/_dtypes.py +119 -0
  76. tracksdata/utils/_logging.py +6 -0
  77. tracksdata/utils/_multiprocessing.py +74 -0
  78. tracksdata/utils/_test/test_dataframe.py +33 -0
  79. tracksdata-0.1.0rc0.dist-info/METADATA +114 -0
  80. tracksdata-0.1.0rc0.dist-info/RECORD +82 -0
  81. tracksdata-0.1.0rc0.dist-info/WHEEL +4 -0
  82. tracksdata-0.1.0rc0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0rc0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0, 'rc0')
33
+
34
+ __commit_id__ = commit_id = None
tracksdata/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """A common data structure and basic tools for multi-object tracking."""
2
+
3
+ try:
4
+ from tracksdata.__about__ import __version__
5
+ except ImportError:
6
+ # Fallback for development installs without proper build
7
+ __version__ = "unknown"
8
+
9
+ import tracksdata.array as array
10
+ import tracksdata.attrs as attrs
11
+ import tracksdata.constants as constants
12
+ import tracksdata.edges as edges
13
+ import tracksdata.functional as functional
14
+ import tracksdata.graph as graph
15
+ import tracksdata.metrics as metrics
16
+ import tracksdata.nodes as nodes
17
+ import tracksdata.options as options
18
+ import tracksdata.solvers as solvers
19
+ import tracksdata.utils._logging as logging
20
+ from tracksdata.attrs import EdgeAttr, NodeAttr
21
+ from tracksdata.constants import DEFAULT_ATTR_KEYS
22
+
23
+ # import tracksdata.io as io # not included as other interfaces are preferred
24
+
25
+
26
+ __all__ = [
27
+ "DEFAULT_ATTR_KEYS",
28
+ "EdgeAttr",
29
+ "NodeAttr",
30
+ "array",
31
+ "attrs",
32
+ "constants",
33
+ "edges",
34
+ "functional",
35
+ "graph",
36
+ "logging",
37
+ "metrics",
38
+ "nodes",
39
+ "options",
40
+ "solvers",
41
+ ]
@@ -0,0 +1,628 @@
1
+ import math
2
+ import operator
3
+ from collections.abc import Callable
4
+
5
+ import numpy as np
6
+ import polars as pl
7
+ import pytest
8
+
9
+ from tracksdata.attrs import (
10
+ Attr,
11
+ AttrComparison,
12
+ EdgeAttr,
13
+ NodeAttr,
14
+ attr_comps_to_strs,
15
+ polars_reduce_attr_comps,
16
+ split_attr_comps,
17
+ )
18
+
19
+
20
+ def test_attr_expr_init_with_string() -> None:
21
+ expr = Attr("test")
22
+ assert isinstance(expr.expr, pl.Expr)
23
+ assert expr.expr.meta.root_names() == ["test"]
24
+
25
+
26
+ def test_attr_expr_init_with_scalar() -> None:
27
+ expr = Attr(1.0)
28
+ assert isinstance(expr.expr, pl.Expr)
29
+ # Literal expressions don't have root names
30
+ assert expr.expr.meta.root_names() == []
31
+
32
+
33
+ def test_attr_expr_init_with_attr_expr() -> None:
34
+ expr1 = Attr("test")
35
+ expr2 = Attr(expr1).sqrt()
36
+ assert isinstance(expr2.expr, pl.Expr)
37
+ assert expr2.expr.meta.root_names() == ["test"]
38
+
39
+
40
+ def test_attr_expr_init_with_polars_expr() -> None:
41
+ pl_expr = pl.col("test")
42
+ expr = Attr(pl_expr)
43
+ assert isinstance(expr.expr, pl.Expr)
44
+ assert expr.expr.meta.root_names() == ["test"]
45
+
46
+
47
+ def test_attr_expr_evaluate() -> None:
48
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
49
+ expr = Attr("a") + Attr("b")
50
+ result = expr.evaluate(df)
51
+ assert isinstance(result, pl.Series)
52
+ assert result.to_list() == [5, 7, 9]
53
+
54
+
55
+ def test_attr_expr_column_names() -> None:
56
+ expr = Attr("test")
57
+ assert expr.columns == ["test"]
58
+
59
+
60
+ @pytest.mark.parametrize(
61
+ "op,func",
62
+ [
63
+ (operator.neg, lambda x: -x),
64
+ (operator.pos, lambda x: +x),
65
+ (operator.abs, abs),
66
+ (operator.invert, lambda x: ~x),
67
+ ],
68
+ )
69
+ def test_attr_expr_unary_operators(op: Callable, func: Callable) -> None:
70
+ df = pl.DataFrame({"a": [-1, 2, -3]})
71
+ expr = op(Attr("a"))
72
+ result = expr.evaluate(df)
73
+ expected = pl.Series([func(x) for x in df["a"]])
74
+ assert result.to_list() == expected.to_list()
75
+
76
+
77
+ @pytest.mark.parametrize(
78
+ "op,func",
79
+ [
80
+ (operator.add, lambda x, y: x + y),
81
+ (operator.sub, lambda x, y: x - y),
82
+ (operator.mul, lambda x, y: x * y),
83
+ (operator.truediv, lambda x, y: x / y),
84
+ (operator.floordiv, lambda x, y: x // y),
85
+ (operator.mod, lambda x, y: x % y),
86
+ (operator.pow, lambda x, y: x**y),
87
+ (operator.eq, lambda x, y: x == y),
88
+ (operator.ne, lambda x, y: x != y),
89
+ (operator.lt, lambda x, y: x < y),
90
+ (operator.le, lambda x, y: x <= y),
91
+ (operator.gt, lambda x, y: x > y),
92
+ (operator.ge, lambda x, y: x >= y),
93
+ ],
94
+ )
95
+ def test_attr_expr_binary_operators(op: Callable, func: Callable) -> None:
96
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
97
+ expr = op(Attr("a"), Attr("b"))
98
+ result = expr.evaluate(df)
99
+ expected = pl.Series([func(x, y) for x, y in zip(df["a"], df["b"], strict=False)])
100
+ assert result.to_list() == expected.to_list()
101
+
102
+
103
+ def test_attr_expr_alias() -> None:
104
+ expr = Attr("test").alias("new_name")
105
+ assert isinstance(expr, Attr)
106
+ # Note: alias doesn't change root names
107
+ assert expr.columns == ["test"]
108
+
109
+
110
+ def test_attr_expr_method_delegation() -> None:
111
+ df = pl.DataFrame({"a": [1, 2, 3]})
112
+ expr = Attr("a").log(2)
113
+ result = expr.evaluate(df)
114
+ expected = df.select(pl.col("a").log(2)).to_series()
115
+ assert result.to_list() == expected.to_list()
116
+
117
+
118
+ def test_attr_expr_complex_expression() -> None:
119
+ df = pl.DataFrame({"iou": [0.5, 0.7, 0.9], "distance": [10, 20, 30]})
120
+ expr = (1 - Attr("iou")) * Attr("distance")
121
+ result = expr.evaluate(df)
122
+ expected = [(1 - iou) * dist for iou, dist in zip(df["iou"], df["distance"], strict=False)]
123
+ assert result.to_list() == expected
124
+
125
+
126
+ def test_attr_expr_with_infinity() -> None:
127
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
128
+ expr = (Attr("a") == 1) * math.inf - math.inf * (Attr("b") > 4) + Attr("c")
129
+
130
+ result = expr.evaluate(df)
131
+ assert result.to_list() == [7, 8, 9]
132
+ assert expr.expr_columns == ["c"]
133
+
134
+ assert len(expr.inf_exprs) == 1
135
+ assert expr.inf_exprs[0].expr_columns == ["a"]
136
+ assert expr.inf_exprs[0].evaluate(df).to_list() == [True, False, False]
137
+
138
+ assert len(expr.neg_inf_exprs) == 1
139
+ assert expr.neg_inf_exprs[0].expr_columns == ["b"]
140
+ assert expr.neg_inf_exprs[0].evaluate(df).to_list() == [False, True, True]
141
+
142
+
143
+ def test_attr_expr_multiple_positive_infinity() -> None:
144
+ """Test expression with multiple positive infinity terms."""
145
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
146
+ expr = Attr("a") * math.inf + math.inf * Attr("b") + Attr("c")
147
+
148
+ result = expr.evaluate(df)
149
+ assert result.to_list() == [7, 8, 9] # Only finite term remains
150
+ assert expr.expr_columns == ["c"]
151
+
152
+ assert len(expr.inf_exprs) == 2
153
+ assert len(expr.neg_inf_exprs) == 0
154
+
155
+ # Check both expressions are tracked
156
+ inf_columns = set()
157
+ for inf_expr in expr.inf_exprs:
158
+ inf_columns.update(inf_expr.columns)
159
+ assert inf_columns == {"a", "b"}
160
+
161
+
162
+ def test_attr_expr_multiple_negative_infinity() -> None:
163
+ """Test expression with multiple negative infinity terms."""
164
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
165
+ expr = Attr("a") * (-math.inf) - math.inf * Attr("b") + Attr("c")
166
+
167
+ result = expr.evaluate(df)
168
+ assert result.to_list() == [7, 8, 9] # Only finite term remains
169
+ assert expr.expr_columns == ["c"]
170
+
171
+ assert len(expr.inf_exprs) == 0
172
+ assert len(expr.neg_inf_exprs) == 2
173
+
174
+ # Check both expressions are tracked
175
+ neg_inf_columns = set()
176
+ for neg_inf_expr in expr.neg_inf_exprs:
177
+ neg_inf_columns.update(neg_inf_expr.columns)
178
+ assert neg_inf_columns == {"a", "b"}
179
+
180
+
181
+ def test_attr_expr_only_infinity_terms() -> None:
182
+ """Test expression with only infinity terms (no finite terms)."""
183
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
184
+ expr = Attr("a") * math.inf - math.inf * Attr("b")
185
+
186
+ result = expr.evaluate(df)
187
+ assert result.to_list() == [0] # All infinity terms become literal zero
188
+ assert expr.expr_columns == [] # No finite columns
189
+
190
+ assert len(expr.inf_exprs) == 1
191
+ assert len(expr.neg_inf_exprs) == 1
192
+ assert expr.inf_exprs[0].columns == ["a"]
193
+ assert expr.neg_inf_exprs[0].columns == ["b"]
194
+
195
+
196
+ def test_attr_expr_complex_infinity_expressions() -> None:
197
+ """Test infinity with more complex expressions (not just boolean comparisons)."""
198
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
199
+ expr = (Attr("a") + Attr("b")) * math.inf - math.inf * (Attr("c") / 2) + Attr("a")
200
+
201
+ result = expr.evaluate(df)
202
+ assert result.to_list() == [1, 2, 3] # Only AttrExpr("a") remains
203
+ assert set(expr.columns) == {"a", "b", "c"}
204
+ assert expr.expr_columns == ["a"]
205
+
206
+ assert len(expr.inf_exprs) == 1
207
+ assert len(expr.neg_inf_exprs) == 1
208
+
209
+ # Test that complex expressions are properly tracked
210
+ inf_expr_result = expr.inf_exprs[0].evaluate(df)
211
+ assert inf_expr_result.to_list() == [5, 7, 9] # a + b
212
+ assert set(expr.inf_exprs[0].columns) == {"a", "b"}
213
+
214
+ neg_inf_expr_result = expr.neg_inf_exprs[0].evaluate(df)
215
+ assert neg_inf_expr_result.to_list() == [3.5, 4.0, 4.5] # c / 2
216
+ assert expr.neg_inf_exprs[0].columns == ["c"]
217
+
218
+
219
+ def test_attr_expr_nested_infinity_operations() -> None:
220
+ """Test nested operations involving infinity."""
221
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
222
+
223
+ # Create intermediate expressions with infinity
224
+ inf_expr = Attr("a") * math.inf
225
+ finite_expr = Attr("b") + 1
226
+
227
+ # Combine them
228
+ combined = inf_expr + finite_expr
229
+
230
+ result = combined.evaluate(df)
231
+ assert result.to_list() == [5, 6, 7] # Only (b + 1) remains
232
+ assert combined.expr_columns == ["b"]
233
+ assert set(combined.columns) == {"a", "b"}
234
+
235
+ assert len(combined.inf_exprs) == 1
236
+ assert len(combined.neg_inf_exprs) == 0
237
+ assert combined.inf_exprs[0].columns == ["a"]
238
+
239
+
240
+ def test_attr_expr_infinity_column_name_tracking() -> None:
241
+ """Test that inf_columns and neg_inf_columns properties work correctly."""
242
+ df = pl.DataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
243
+
244
+ # Simpler test: separate infinity operations that are easier to track
245
+ expr = (
246
+ Attr("x") * math.inf # positive infinity
247
+ + Attr("y") * math.inf # positive infinity
248
+ - math.inf * Attr("z") # negative infinity
249
+ + Attr("x") # finite term
250
+ )
251
+
252
+ result = expr.evaluate(df)
253
+ assert result.to_list() == [1, 2] # Only finite AttrExpr("x") remains
254
+
255
+ # Test the convenience properties
256
+ assert set(expr.inf_columns) == {"x", "y"} # x and y in positive infinity
257
+ assert set(expr.neg_inf_columns) == {"z"} # z in negative infinity
258
+
259
+ assert expr.has_inf() is True
260
+
261
+ # Check individual expressions
262
+ assert len(expr.inf_exprs) == 2 # x and y
263
+ assert len(expr.neg_inf_exprs) == 1 # z
264
+
265
+
266
+ def test_attr_expr_no_infinity_terms() -> None:
267
+ """Test that expressions without infinity work normally."""
268
+ df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
269
+ expr = Attr("a") * 2 + Attr("b") - 1
270
+
271
+ result = expr.evaluate(df)
272
+ assert result.to_list() == [5, 8, 11] # 2*a + b - 1 = 2*[1,2,3] + [4,5,6] - 1 = [2,4,6] + [4,5,6] - 1 = [5,8,11]
273
+
274
+ # No infinity terms should be tracked
275
+ assert len(expr.inf_exprs) == 0
276
+ assert len(expr.neg_inf_exprs) == 0
277
+ assert expr.inf_columns == []
278
+ assert expr.neg_inf_columns == []
279
+ assert expr.has_inf() is False
280
+
281
+
282
+ def test_attr_expr_scalar_operations() -> None:
283
+ df = pl.DataFrame({"a": [1, 2, 3]})
284
+ expr = Attr("a") * 2
285
+ result = expr.evaluate(df)
286
+ assert result.to_list() == [2, 4, 6]
287
+
288
+ expr = 2 * Attr("a") # Test reverse operation
289
+ result = expr.evaluate(df)
290
+ assert result.to_list() == [2, 4, 6]
291
+
292
+
293
+ def test_attr_expr_boolean_operations() -> None:
294
+ df = pl.DataFrame({"a": [True, False, True], "b": [False, True, True]})
295
+
296
+ expr = Attr("a") & Attr("b") # and
297
+ result = expr.evaluate(df)
298
+ assert result.to_list() == [False, False, True]
299
+
300
+ expr = Attr("a") | Attr("b") # or
301
+ result = expr.evaluate(df)
302
+ assert result.to_list() == [True, True, True]
303
+
304
+
305
+ def test_duplicated_columns() -> None:
306
+ df = pl.DataFrame({"a": [1, 2, 3]})
307
+ expr = (Attr("a") == 1) * 10 - 5 * (Attr("a") > 2)
308
+ result = expr.evaluate(df)
309
+ assert result.to_list() == [10, 0, -5]
310
+ assert expr.columns == ["a"]
311
+
312
+
313
+ def test_attr_reverse_comparison() -> None:
314
+ """Test basic initialization of AttrComparison."""
315
+ attr = Attr("test_column")
316
+ comp = 5 == attr # reversed on purpose
317
+
318
+ assert comp.attr == attr
319
+ assert comp.column == "test_column"
320
+ assert comp.op == operator.eq
321
+ assert comp.other == 5
322
+
323
+
324
+ def test_attr_numpy_comparison() -> None:
325
+ """Test basic initialization of AttrComparison."""
326
+ attr = Attr("test_column")
327
+ comp = attr == np.asarray(5)
328
+
329
+ assert comp.attr == attr
330
+ assert comp.column == "test_column"
331
+ assert comp.op == operator.eq
332
+ assert comp.other == 5
333
+
334
+
335
+ def test_attr_comparison_repr() -> None:
336
+ """Test string representation of AttrComparison."""
337
+ attr = Attr("test_column")
338
+ comp = attr > 10
339
+
340
+ assert repr(comp) == "Attr(test_column) > 10"
341
+
342
+
343
+ def test_attr_comparison_to_attr() -> None:
344
+ """Test converting AttrComparison back to Attr."""
345
+ df = pl.DataFrame({"test_column": [1, 2, 3, 4, 5]})
346
+ attr = Attr("test_column")
347
+ comp = attr > 3
348
+
349
+ converted_attr = comp.to_attr()
350
+ result = converted_attr.evaluate(df)
351
+
352
+ assert result.to_list() == [False, False, False, True, True]
353
+
354
+
355
+ def test_attr_comparison_getattr_delegation() -> None:
356
+ """Test that AttrComparison delegates attribute access to its Attr representation."""
357
+ attr = Attr("test_column")
358
+ comp = attr == 5
359
+
360
+ # Test that we can access Attr methods through AttrComparison
361
+ assert comp.columns == ["test_column"]
362
+ assert comp.expr_columns == ["test_column"]
363
+
364
+
365
+ def test_attr_comparison_operator_delegation() -> None:
366
+ """Test that AttrComparison delegates operators to its Attr representation."""
367
+ df = pl.DataFrame({"test_column": [1, 2, 3, 4, 5]})
368
+ attr = Attr("test_column")
369
+ comp = attr > 3
370
+
371
+ # Test that we can use operators on AttrComparison
372
+ result_attr = comp + 10
373
+ result = result_attr.evaluate(df)
374
+
375
+ # Should be (test_column > 3) + 10
376
+ expected = [(x > 3) + 10 for x in df["test_column"]]
377
+ assert result.to_list() == expected
378
+
379
+
380
+ def test_attr_comparison_init_with_infinity_attr() -> None:
381
+ """Test that AttrComparison raises error when attr has infinity."""
382
+ # Create an attr with infinity
383
+ attr_with_inf = Attr("test") * math.inf
384
+
385
+ with pytest.raises(ValueError, match="Comparison operators are not supported for expressions with infinity"):
386
+ AttrComparison(attr_with_inf, operator.eq, 5)
387
+
388
+
389
+ def test_attr_comparison_init_with_attr_other() -> None:
390
+ """Test that AttrComparison raises error when comparing two Attr objects."""
391
+ attr1 = Attr("col1")
392
+ attr2 = Attr("col2")
393
+
394
+ with pytest.raises(ValueError, match="Does not support comparison between expressions"):
395
+ AttrComparison(attr1, operator.eq, attr2)
396
+
397
+
398
+ def test_attr_comparison_init_with_empty_columns() -> None:
399
+ """Test that AttrComparison raises error for empty expressions."""
400
+ # Create an attr with no columns (literal)
401
+ attr_no_cols = Attr(5)
402
+
403
+ with pytest.raises(ValueError, match="Comparison operators are not supported for empty expressions"):
404
+ AttrComparison(attr_no_cols, operator.eq, 10)
405
+
406
+
407
+ def test_attr_comparison_init_with_multiple_columns() -> None:
408
+ """Test that AttrComparison raises error for multiple columns."""
409
+ # Create an attr with multiple columns
410
+ attr_multi_cols = Attr("col1") + Attr("col2")
411
+
412
+ with pytest.raises(ValueError, match="Comparison operators are not supported for multiple columns"):
413
+ AttrComparison(attr_multi_cols, operator.eq, 10)
414
+
415
+
416
+ def test_attr_comparison_comparison_operators() -> None:
417
+ """Test all comparison operators with AttrComparison."""
418
+ df = pl.DataFrame({"test_column": [1, 2, 3, 4, 5]})
419
+ attr = Attr("test_column")
420
+
421
+ test_cases = [
422
+ (operator.eq, 3, [False, False, True, False, False]),
423
+ (operator.ne, 3, [True, True, False, True, True]),
424
+ (operator.lt, 3, [True, True, False, False, False]),
425
+ (operator.le, 3, [True, True, True, False, False]),
426
+ (operator.gt, 3, [False, False, False, True, True]),
427
+ (operator.ge, 3, [False, False, True, True, True]),
428
+ ]
429
+
430
+ for op, other, expected in test_cases:
431
+ comp = op(attr, other)
432
+ result = comp.to_attr().evaluate(df)
433
+ assert result.to_list() == expected
434
+
435
+
436
+ def test_attr_comparison_binary_operators() -> None:
437
+ """Test binary operators with AttrComparison."""
438
+ df = pl.DataFrame({"test_column": [1, 2, 3, 4, 5]})
439
+ attr = Attr("test_column")
440
+ comp = AttrComparison(attr, operator.gt, 3)
441
+
442
+ # Test addition
443
+ result = comp + 10
444
+ result_series = result.evaluate(df)
445
+ expected = [(x > 3) + 10 for x in df["test_column"]]
446
+ assert result_series.to_list() == expected
447
+
448
+ # Test multiplication
449
+ result = comp * 2
450
+ result_series = result.evaluate(df)
451
+ expected = [(x > 3) * 2 for x in df["test_column"]]
452
+ assert result_series.to_list() == expected
453
+
454
+
455
+ def test_attr_comparison_reverse_operators() -> None:
456
+ """Test reverse operators with AttrComparison."""
457
+ df = pl.DataFrame({"test_column": [1, 2, 3, 4, 5]})
458
+ attr = Attr("test_column")
459
+ comp = AttrComparison(attr, operator.gt, 3)
460
+
461
+ # Test reverse addition
462
+ result = 10 + comp
463
+ result_series = result.evaluate(df)
464
+ expected = [10 + (x > 3) for x in df["test_column"]]
465
+ assert result_series.to_list() == expected
466
+
467
+ # Test reverse multiplication
468
+ result = 2 * comp
469
+ result_series = result.evaluate(df)
470
+ expected = [2 * (x > 3) for x in df["test_column"]]
471
+ assert result_series.to_list() == expected
472
+
473
+
474
+ def test_split_attr_comps() -> None:
475
+ """Test splitting attribute comparisons into node and edge comparisons."""
476
+ node_attr1 = NodeAttr("node_col1")
477
+ node_attr2 = NodeAttr("node_col2")
478
+ edge_attr1 = EdgeAttr("edge_col1")
479
+ edge_attr2 = EdgeAttr("edge_col2")
480
+
481
+ node_comp1 = node_attr1 == 1
482
+ node_comp2 = node_attr2 > 5
483
+ edge_comp1 = edge_attr1 < 10
484
+ edge_comp2 = edge_attr2 != 0
485
+
486
+ all_comps = [node_comp1, edge_comp1, node_comp2, edge_comp2]
487
+ node_comps, edge_comps = split_attr_comps(all_comps)
488
+
489
+ assert len(node_comps) == 2
490
+ assert len(edge_comps) == 2
491
+ assert node_comps[0].column == "node_col1"
492
+ assert node_comps[1].column == "node_col2"
493
+ assert edge_comps[0].column == "edge_col1"
494
+ assert edge_comps[1].column == "edge_col2"
495
+
496
+
497
+ def test_split_attr_comps_empty() -> None:
498
+ """Test splitting empty list of attribute comparisons."""
499
+ node_comps, edge_comps = split_attr_comps([])
500
+ assert node_comps == []
501
+ assert edge_comps == []
502
+
503
+
504
+ def test_split_attr_comps_only_node() -> None:
505
+ """Test splitting only node attribute comparisons."""
506
+ node_attr = NodeAttr("node_col")
507
+ node_comp = AttrComparison(node_attr, operator.eq, 1)
508
+
509
+ node_comps, edge_comps = split_attr_comps([node_comp])
510
+ assert len(node_comps) == 1
511
+ assert len(edge_comps) == 0
512
+ assert node_comps[0].column == "node_col"
513
+
514
+
515
+ def test_split_attr_comps_only_edge() -> None:
516
+ """Test splitting only edge attribute comparisons."""
517
+ edge_attr = EdgeAttr("edge_col")
518
+ edge_comp = AttrComparison(edge_attr, operator.gt, 5)
519
+
520
+ node_comps, edge_comps = split_attr_comps([edge_comp])
521
+ assert len(node_comps) == 0
522
+ assert len(edge_comps) == 1
523
+ assert edge_comps[0].column == "edge_col"
524
+
525
+
526
+ def test_split_attr_comps_invalid_type() -> None:
527
+ """Test splitting with invalid attribute type."""
528
+ # Create a comparison with regular Attr instead of NodeAttr or EdgeAttr
529
+ regular_attr = Attr("regular_col")
530
+ regular_comp = regular_attr == 1
531
+
532
+ with pytest.raises(ValueError, match="Expected comparisons of 'NodeAttr' or 'EdgeAttr' objects"):
533
+ split_attr_comps([regular_comp])
534
+
535
+
536
+ def test_attr_comps_to_strs() -> None:
537
+ """Test converting attribute comparisons to strings."""
538
+ node_attr = NodeAttr("node_col")
539
+ edge_attr = EdgeAttr("edge_col")
540
+
541
+ node_comp = node_attr == 1
542
+ edge_comp = edge_attr > 5
543
+
544
+ result = attr_comps_to_strs([node_comp, edge_comp])
545
+ assert result == ["node_col", "edge_col"]
546
+
547
+
548
+ def test_attr_comps_to_strs_empty() -> None:
549
+ """Test converting empty list of attribute comparisons to strings."""
550
+ result = attr_comps_to_strs([])
551
+ assert result == []
552
+
553
+
554
+ def test_polars_reduce_attr_comps() -> None:
555
+ """Test reducing attribute comparisons to a single polars expression."""
556
+ df = pl.DataFrame({"col1": [1, 2, 3, 4, 5], "col2": [10, 20, 30, 40, 50], "col3": [True, False, True, False, True]})
557
+
558
+ attr1 = Attr("col1")
559
+ attr2 = Attr("col2")
560
+ attr3 = Attr("col3")
561
+
562
+ comp1 = attr1 > 2
563
+ comp2 = attr2 < 35
564
+ comp3 = attr3 == True
565
+
566
+ result_expr = polars_reduce_attr_comps(df, [comp1, comp2, comp3], operator.and_)
567
+ result = df.select(result_expr).to_series()
568
+
569
+ # Expected: (col1 > 2) & (col2 < 35) & (col3 == True)
570
+ expected = [
571
+ False, # 1 > 2 = False, so False
572
+ False, # 2 > 2 = False, so False
573
+ True, # 3 > 2 = True, 30 < 35 = True, True == True = True, so True
574
+ False, # 4 > 2 = True, 40 < 35 = False, so False
575
+ False, # 5 > 2 = True, 50 < 35 = False, so False
576
+ ]
577
+ assert result.to_list() == expected
578
+
579
+
580
+ def test_polars_reduce_attr_comps_empty() -> None:
581
+ """Test reducing empty list of attribute comparisons raises ValueError."""
582
+ df = pl.DataFrame({"col1": [1, 2, 3]})
583
+
584
+ with pytest.raises(ValueError, match="No attribute comparisons provided"):
585
+ polars_reduce_attr_comps(df, [], operator.and_)
586
+
587
+
588
+ def test_polars_reduce_attr_comps_single() -> None:
589
+ """Test reducing single attribute comparison."""
590
+ df = pl.DataFrame({"col1": [1, 2, 3, 4, 5]})
591
+
592
+ attr = Attr("col1")
593
+ comp = attr > 3
594
+
595
+ result_expr = polars_reduce_attr_comps(df, [comp], operator.and_)
596
+ result = df.select(result_expr).to_series()
597
+
598
+ expected = [False, False, False, True, True]
599
+ assert result.to_list() == expected
600
+
601
+
602
+ def test_attr_comparison_complex_operations() -> None:
603
+ """Test complex operations involving AttrComparison."""
604
+ df = pl.DataFrame({"col1": [1, 2, 3, 4, 5], "col2": [10, 20, 30, 40, 50]})
605
+
606
+ attr1 = Attr("col1")
607
+ attr2 = Attr("col2")
608
+
609
+ comp1 = attr1 > 2
610
+ comp2 = attr2 < 35
611
+
612
+ # Test combining comparisons with arithmetic
613
+ result = comp1 * 10 + comp2 * 5
614
+ result_series = result.evaluate(df)
615
+
616
+ expected = [(x > 2) * 10 + (y < 35) * 5 for x, y in zip(df["col1"], df["col2"], strict=False)]
617
+ assert result_series.to_list() == expected
618
+
619
+
620
+ def test_attr_comparison_method_delegation() -> None:
621
+ """Test that AttrComparison properly delegates method calls."""
622
+ attr = Attr("test_column")
623
+ comp = attr > 3
624
+
625
+ # Test that we can call methods on the comparison
626
+ result = comp.alias("new_name")
627
+ assert isinstance(result, Attr)
628
+ assert result.columns == ["test_column"]