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.
- tracksdata/__about__.py +34 -0
- tracksdata/__init__.py +41 -0
- tracksdata/_test/test_attrs.py +628 -0
- tracksdata/_test/test_options.py +149 -0
- tracksdata/array/__init__.py +9 -0
- tracksdata/array/_base_array.py +55 -0
- tracksdata/array/_graph_array.py +324 -0
- tracksdata/array/_nd_chunk_cache.py +158 -0
- tracksdata/array/_test/test_graph_array.py +349 -0
- tracksdata/array/_test/test_nd_chunk_cache.py +118 -0
- tracksdata/attrs.py +681 -0
- tracksdata/conftest.py +104 -0
- tracksdata/constants.py +71 -0
- tracksdata/edges/__init__.py +7 -0
- tracksdata/edges/_base_edge_attrs.py +83 -0
- tracksdata/edges/_base_edges.py +79 -0
- tracksdata/edges/_distance_edges.py +203 -0
- tracksdata/edges/_generic_edges.py +127 -0
- tracksdata/edges/_iou_edges.py +28 -0
- tracksdata/edges/_test/test_distance_edges.py +314 -0
- tracksdata/edges/_test/test_generic_edges.py +231 -0
- tracksdata/edges/_test/test_iou_edges.py +200 -0
- tracksdata/functional/__init__.py +6 -0
- tracksdata/functional/_edges.py +80 -0
- tracksdata/functional/_iou.py +100 -0
- tracksdata/functional/_napari.py +155 -0
- tracksdata/functional/_rx.py +314 -0
- tracksdata/functional/_test/test_functional_edges.py +72 -0
- tracksdata/functional/_test/test_iou.py +304 -0
- tracksdata/functional/_test/test_napari.py +64 -0
- tracksdata/functional/_test/test_rx.py +197 -0
- tracksdata/graph/__init__.py +10 -0
- tracksdata/graph/_base_graph.py +1395 -0
- tracksdata/graph/_graph_view.py +757 -0
- tracksdata/graph/_mapped_graph_mixin.py +238 -0
- tracksdata/graph/_rustworkx_graph.py +1728 -0
- tracksdata/graph/_sql_graph.py +1573 -0
- tracksdata/graph/_test/test_graph_backends.py +1798 -0
- tracksdata/graph/_test/test_index_graph.py +199 -0
- tracksdata/graph/_test/test_subgraph.py +1218 -0
- tracksdata/graph/filters/__init__.py +3 -0
- tracksdata/graph/filters/_base_filter.py +113 -0
- tracksdata/graph/filters/_indexed_filter.py +84 -0
- tracksdata/graph/filters/_spatial_filter.py +323 -0
- tracksdata/graph/filters/_test/test_spatial_filter.py +231 -0
- tracksdata/io/__init__.py +5 -0
- tracksdata/io/_ctc.py +293 -0
- tracksdata/io/_numpy_array.py +155 -0
- tracksdata/io/_test/test_ctc_io.py +77 -0
- tracksdata/metrics/__init__.py +11 -0
- tracksdata/metrics/_ctc_metrics.py +356 -0
- tracksdata/metrics/_test/test_ctc_metrics.py +127 -0
- tracksdata/metrics/_test/test_metrics_visualize.py +98 -0
- tracksdata/metrics/_visualize.py +257 -0
- tracksdata/nodes/__init__.py +8 -0
- tracksdata/nodes/_base_node_attrs.py +86 -0
- tracksdata/nodes/_base_nodes.py +32 -0
- tracksdata/nodes/_generic_nodes.py +183 -0
- tracksdata/nodes/_mask.py +333 -0
- tracksdata/nodes/_random.py +172 -0
- tracksdata/nodes/_regionprops.py +301 -0
- tracksdata/nodes/_test/test_generic_nodes.py +472 -0
- tracksdata/nodes/_test/test_mask.py +293 -0
- tracksdata/nodes/_test/test_random.py +264 -0
- tracksdata/nodes/_test/test_regionprops.py +280 -0
- tracksdata/options.py +165 -0
- tracksdata/solvers/__init__.py +6 -0
- tracksdata/solvers/_base_solver.py +48 -0
- tracksdata/solvers/_ilp_solver.py +410 -0
- tracksdata/solvers/_nearest_neighbors_solver.py +307 -0
- tracksdata/solvers/_test/test_ilp_solver.py +676 -0
- tracksdata/solvers/_test/test_nearest_neighbors_solver.py +384 -0
- tracksdata/utils/__init__.py +1 -0
- tracksdata/utils/_dataframe.py +52 -0
- tracksdata/utils/_dtypes.py +119 -0
- tracksdata/utils/_logging.py +6 -0
- tracksdata/utils/_multiprocessing.py +74 -0
- tracksdata/utils/_test/test_dataframe.py +33 -0
- tracksdata-0.1.0rc0.dist-info/METADATA +114 -0
- tracksdata-0.1.0rc0.dist-info/RECORD +82 -0
- tracksdata-0.1.0rc0.dist-info/WHEEL +4 -0
- tracksdata-0.1.0rc0.dist-info/licenses/LICENSE +13 -0
tracksdata/__about__.py
ADDED
|
@@ -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"]
|