From 9900feee6cccd490be57a1e2cc4facda74c41871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sat, 7 Mar 2026 20:24:04 +0000 Subject: [PATCH 01/20] feat: tensor aggregators (mean, std) --- pyproject.toml | 2 +- ratiopath/ray/aggregate/__init__.py | 5 + ratiopath/ray/aggregate/tensor_mean.py | 125 +++++++++++++++++++ ratiopath/ray/aggregate/tensor_std.py | 143 +++++++++++++++++++++ tests/test_ray_aggregations.py | 127 +++++++++++++++++++ uv.lock | 164 ++----------------------- 6 files changed, 409 insertions(+), 157 deletions(-) create mode 100644 ratiopath/ray/aggregate/__init__.py create mode 100644 ratiopath/ray/aggregate/tensor_mean.py create mode 100644 ratiopath/ray/aggregate/tensor_std.py create mode 100644 tests/test_ray_aggregations.py diff --git a/pyproject.toml b/pyproject.toml index 6c26195..7392eb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ readme = "README.md" license = "MIT" license-files = ["LICENSE"] -requires-python = ">=3.12" +requires-python = ">=3.12,<3.14" dependencies = [ "albumentations>=2.0.8", "imagecodecs>=2025.8.2", diff --git a/ratiopath/ray/aggregate/__init__.py b/ratiopath/ray/aggregate/__init__.py new file mode 100644 index 0000000..3b8c34f --- /dev/null +++ b/ratiopath/ray/aggregate/__init__.py @@ -0,0 +1,5 @@ +from ratiopath.ray.aggregate.tensor_mean import TensorMean +from ratiopath.ray.aggregate.tensor_std import TensorStd + + +__all__ = ["TensorMean", "TensorStd"] diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py new file mode 100644 index 0000000..b56e755 --- /dev/null +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -0,0 +1,125 @@ +from typing import cast + +import numpy as np + +from ray.data.aggregate import AggregateFnV2 +from ray.data.block import Block, BlockAccessor + + +class TensorMean(AggregateFnV2[dict, np.ndarray | float]): + """Calculates the mean (average) of a column containing Tensors. + + This aggregator treats the data column as a high-dimensional array where + **axis 0 represents the batch dimension**. To satisfy the requirements + of a reduction, axis 0 must be included in the aggregation. This ensures + that the data is collapsed across rows, preventing the internal state + from growing linearly with the dataset size. + + The `axis` parameter defines which dimensions of the 2D+ block (Batch, ...) + should be collapsed. + + Args: + on: The name of the column containing tensors or numbers. + axis: The axis or axes along which the reduction is computed. + - `None`: Global reduction. Collapses all dimensions (including batch) + to a single scalar. + - `int`: Aggregates over both the batch (axis 0) AND the specified + tensor dimension. For example, `axis=1` collapses the batch and + the first dimension of the tensors. + - `tuple`: A sequence of axes that **must** explicitly include `0`. + ignore_nulls: Whether to ignore null values. Defaults to True. + alias_name: Optional name for the resulting column. Defaults to "mean()". + + Raises: + ValueError: If `axis` is provided but does not include `0`. + + Note: + If you wish to perform operations on tensors independently without + collapsing the batch dimension (axis 0), use `.map()` instead. + + Example: + >>> import ray + >>> import numpy as np + >>> from ratiopath.ray.aggregate import TensorMean + >>> # Dataset with 2x2 matrices: total shape (Batch=2, Dim1=2, Dim2=2) + >>> ds = ray.data.from_items( + ... [ + ... {"m": np.array([[1, 1], [1, 1]])}, + ... {"m": np.array([[3, 3], [3, 3]])}, + ... ] + ... ) + >>> # 1. Global Mean (axis=None) -> Result: 2.0 + >>> ds.aggregate(TensorMean(on="m", axis=None)) + >>> + >>> # 2. Batch Mean (axis=0) -> Result: np.array([[2, 2], [2, 2]]) + >>> ds.aggregate(TensorMean(on="m", axis=0)) + >>> + >>> # 3. Mean across Batch and Rows (axis=(0, 1)) -> Result: np.array([2, 2]) + >>> ds.aggregate(TensorMean(on="m", axis=(0, 1))) + """ + + aggregate_axis: tuple[int, ...] | None = None + + def __init__( + self, + on: str | None = None, + axis: int | tuple[int, ...] | None = None, + ignore_nulls: bool = True, + alias_name: str | None = None, + ): + super().__init__( + name=alias_name if alias_name else f"mean({on!s})", + on=on, + ignore_nulls=ignore_nulls, + # Initialize with identity values for summation + zero_factory=lambda: {"sum": None, "shape": None, "count": 0}, + ) + + if axis is not None: + axes = {0, axis} if isinstance(axis, int) else set(axis) + + if 0 not in axes: + raise ValueError( + f"Invalid axis configuration: {axis}. Axis 0 (the batch dimension) " + "must be included to perform a reduction. To process rows " + "independently without collapsing the batch, use .map() instead." + ) + + self.aggregate_axis = tuple(axes) + + def aggregate_block(self, block: Block) -> dict: + block_acc = BlockAccessor.for_block(block) + # Access the raw numpy data for the column + col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) + + # Perform the partial sum and calculate how many elements contributed + block_sum = np.sum(col_np, axis=self.aggregate_axis) + block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) + + return { + "sum": block_sum.flatten(), + "shape": block_sum.shape, + "count": block_count, + } + + def combine(self, current_accumulator: dict, new: dict) -> dict: + if new["count"] == 0: + return current_accumulator + + if current_accumulator["count"] == 0: + return new + + return { + "sum": np.asarray(current_accumulator["sum"]) + np.asarray(new["sum"]), + "shape": new["shape"], + "count": current_accumulator["count"] + new["count"], + } + + def finalize(self, accumulator: dict) -> np.ndarray | float: + count = accumulator["count"] + + if count == 0: + return np.nan + + # Reshape the flattened sum back to original aggregated dimensions + return np.asarray(accumulator["sum"]).reshape(accumulator["shape"]) / count diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py new file mode 100644 index 0000000..2feeecd --- /dev/null +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -0,0 +1,143 @@ +from typing import cast + +import numpy as np + +from ray.data.aggregate import AggregateFnV2 +from ray.data.block import Block, BlockAccessor + + +class TensorStd(AggregateFnV2[dict, np.ndarray | float]): + """Calculates the standard deviation of a column containing Tensors. + + This aggregator treats the data column as a high-dimensional array where + **axis 0 represents the batch dimension**. To satisfy the requirements + of a reduction, axis 0 must be included in the aggregation. + + It uses a parallel variance accumulation algorithm (Chan's method) to maintain + numerical stability while processing data across multiple Ray blocks. + + Args: + on: The name of the column containing tensors or numbers. + axis: The axis or axes along which the reduction is computed. + - `None`: Global reduction. Collapses all dimensions (including batch) + to a single scalar. + - `int`: Aggregates over both the batch (axis 0) AND the specified + tensor dimension. For example, `axis=1` collapses the batch and + the first dimension of the tensors. + - `tuple`: A sequence of axes that **must** explicitly include `0`. + ignore_nulls: Whether to ignore null values. Defaults to True. + alias_name: Optional name for the resulting column. Defaults to "std()". + + Raises: + ValueError: If `axis` is provided but does not include `0`. + + Example: + >>> import ray + >>> import numpy as np + >>> from ratiopath.ray.aggregate import TensorStd + >>> ds = ray.data.from_items( + ... [ + ... {"m": np.array([[1, 2], [1, 2]])}, + ... {"m": np.array([[5, 6], [5, 6]])}, + ... ] + ... ) + >>> # 1. Global Std (axis=None) -> ~2.06 + >>> ds.aggregate(TensorStd(on="m", axis=None)) + >>> # 2. Batch Std (axis=0) -> np.array([[2, 2], [2, 2]]) + >>> ds.aggregate(TensorStd(on="m", axis=0)) + """ + + aggregate_axis: tuple[int, ...] | None = None + + def __init__( + self, + on: str | None = None, + axis: int | tuple[int, ...] | None = None, + ignore_nulls: bool = True, + alias_name: str | None = None, + ): + super().__init__( + name=alias_name if alias_name else f"std({on!s})", + on=on, + ignore_nulls=ignore_nulls, + # sum: partial sum, ssd: sum of squared differences, count: elements reduced + zero_factory=lambda: {"sum": None, "ssd": None, "shape": None, "count": 0}, + ) + + if axis is not None: + axes = {0, axis} if isinstance(axis, int) else set(axis) + + if 0 not in axes: + raise ValueError( + f"Invalid axis configuration: {axis}. Axis 0 (the batch dimension) " + "must be included to perform a reduction. To process rows " + "independently without collapsing the batch, use .map() instead." + ) + + self.aggregate_axis = tuple(sorted(axes)) + + def aggregate_block(self, block: Block) -> dict: + block_acc = BlockAccessor.for_block(block) + col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) + + # Partial sum and element count + block_sum = np.sum(col_np, axis=self.aggregate_axis) + block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) + + # SSD calculation: sum((x - mean)^2) + # Note: We need to expand block_sum to be broadcastable against col_np + # or calculate ssd using the identity: sum(x^2) - (sum(x)^2 / n) + # The (x - mean)^2 method is usually more numerically stable. + if self.aggregate_axis is None: + block_ssd = np.sum((col_np - (block_sum / block_count)) ** 2) + else: + # Re-expand sum for broadcasting + expanded_sum = block_sum + for ax in sorted(self.aggregate_axis): + expanded_sum = np.expand_dims(expanded_sum, ax) + block_ssd = np.sum( + (col_np - (expanded_sum / block_count)) ** 2, axis=self.aggregate_axis + ) + + return { + "sum": block_sum.flatten(), + "ssd": block_ssd.flatten(), + "shape": block_sum.shape, + "count": block_count, + } + + def combine(self, current_accumulator: dict, new: dict) -> dict: + if new["count"] == 0: + return current_accumulator + + if current_accumulator["count"] == 0: + return new + + mean_a = np.asarray(current_accumulator["sum"]) / current_accumulator["count"] + mean_b = np.asarray(new["sum"]) / new["count"] + delta = mean_b - mean_a + + combined_sum = np.asarray(current_accumulator["sum"]) + np.asarray(new["sum"]) + combined_count = current_accumulator["count"] + new["count"] + combined_ssd = ( + np.asarray(current_accumulator["ssd"]) + + np.asarray(new["ssd"]) + + (delta**2 * current_accumulator["count"] * new["count"] / combined_count) + ) + + return { + "sum": combined_sum, + "ssd": combined_ssd, + "shape": new["shape"], + "count": combined_count, + } + + def finalize(self, accumulator: dict) -> np.ndarray | float: + count = accumulator["count"] + + if count == 0: + return np.nan + + return np.sqrt( + np.asarray(accumulator["ssd"]).reshape(accumulator["shape"]) / count + ) diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py new file mode 100644 index 0000000..893adf0 --- /dev/null +++ b/tests/test_ray_aggregations.py @@ -0,0 +1,127 @@ +import os + +import numpy as np +import pytest +import ray + + +# Set environment variables before ray.init +os.environ["RAY_ENABLE_METRICS_EXPORT"] = "0" +os.environ["RAY_IGNORE_VENV_MISMATCH"] = "1" +os.environ["RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO"] = "0" + +# Adjust imports based on your file structure +from ratiopath.ray.aggregate import TensorMean, TensorStd + + +@pytest.fixture(scope="module") +def ray_start(): + if not ray.is_initialized(): + ray.init(ignore_reinit_error=True) + yield + ray.shutdown() + + +## --- TensorMean Tests --- + + +def test_tensor_mean_global(ray_start): + """Tests axis=None: Global reduction to a single scalar.""" + data = [ + {"m": np.array([[2, 4], [6, 8]])}, + {"m": np.array([[0, 0], [0, 0]])}, + ] + ds = ray.data.from_items(data) + result = ds.aggregate(TensorMean(on="m", axis=None)) + # (2+4+6+8) / 8 = 2.5 + assert result["mean(m)"] == 2.5 + + +def test_tensor_mean_int_shorthand(ray_start): + """Tests axis=1: Should aggregate over batch (0) AND dim 1.""" + data = [ + {"m": np.array([[10, 20], [30, 40]])}, # Row sums: 30, 70 + {"m": np.array([[0, 0], [0, 0]])}, # Row sums: 0, 0 + ] + ds = ray.data.from_items(data) + # Aggregating over axis 1 (internal becomes (0, 1)) + result = ds.aggregate(TensorMean(on="m", axis=1)) + + expected = np.array([10.0, 15.0]) # [(10+30+0+0)/2, (20+40+0+0)/2] + np.testing.assert_array_equal(result["mean(m)"], expected) + + +def test_tensor_mean_batch_only(ray_start): + """Tests axis=0: Should collapse only the batch dimension.""" + data = [ + {"m": np.array([[10, 10], [10, 10]])}, + {"m": np.array([[20, 20], [20, 20]])}, + ] + ds = ray.data.from_items(data) + result = ds.aggregate(TensorMean(on="m", axis=0)) + + expected = np.array([[15.0, 15.0], [15.0, 15.0]]) + np.testing.assert_array_equal(result["mean(m)"], expected) + + +## --- TensorStd Tests --- + + +def test_tensor_std_global(ray_start): + """Tests global standard deviation.""" + vals = np.array([1, 2, 3, 4, 5, 6, 7, 8]) + data = [{"m": vals[:4].reshape(2, 2)}, {"m": vals[4:].reshape(2, 2)}] + ds = ray.data.from_items(data) + + result = ds.aggregate(TensorStd(on="m", axis=None)) + expected = np.std(vals) + assert pytest.approx(result["std(m)"], 0.0001) == expected + + +def test_tensor_std_batch_only(ray_start): + """Tests STD across the batch dimension only.""" + # Two identical matrices with different offsets + data = [ + {"m": np.array([10, 20])}, # Sample 1 + {"m": np.array([30, 40])}, # Sample 2 + ] + ds = ray.data.from_items(data) + result = ds.aggregate(TensorStd(on="m", axis=0)) + + # Std of [10, 30] is 10; Std of [20, 40] is 10 + expected = np.array([10.0, 10.0]) + np.testing.assert_array_equal(result["std(m)"], expected) + + +## --- Validation & Logic Tests --- + + +def test_invalid_axis_tuple(ray_start): + """Verifies that providing a tuple without axis 0 raises ValueError.""" + with pytest.raises( + ValueError, match=r"Axis 0 \(the batch dimension\) must be included" + ): + TensorMean(on="m", axis=(1, 2)) + + +def test_tensor_aggregate_groupby(ray_start): + """Verifies Mean and Std work within groupby operations.""" + data = [ + {"id": "A", "m": np.array([1, 1])}, + {"id": "A", "m": np.array([3, 3])}, + {"id": "B", "m": np.array([10, 10])}, + ] + ds = ray.data.from_items(data) + + # Test Mean Groupby + res_mean = ds.groupby("id").aggregate(TensorMean(on="m", axis=0)).take_all() + res_mean = sorted(res_mean, key=lambda x: x["id"]) + + np.testing.assert_array_equal(res_mean[0]["mean(m)"], [2.0, 2.0]) # Mean of [1,3] + np.testing.assert_array_equal(res_mean[1]["mean(m)"], [10.0, 10.0]) + + # Test Std Groupby + res_std = ds.groupby("id").aggregate(TensorStd(on="m", axis=0)).take_all() + res_std = sorted(res_std, key=lambda x: x["id"]) + + np.testing.assert_array_equal(res_std[0]["std(m)"], [1.0, 1.0]) # Std of [1,3] diff --git a/uv.lock b/uv.lock index a80eb20..1a106e3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.12, <3.14" resolution-markers = [ "sys_platform == 'darwin'", "platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -85,7 +85,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] @@ -159,17 +158,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] @@ -352,23 +340,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/36/b1d3117c34f5eebfd7a92314042338dce608654173c71b1fb707e416aef5/imagecodecs-2025.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b7cd0c41ea83f249db2f4ee04c7c3f23bd6fbef89a58bdaa7d2bf11421ffc15", size = 10127356, upload-time = "2025-08-03T06:07:04.771Z" }, { url = "https://files.pythonhosted.org/packages/e6/41/9ee8f13c4e3425c1aed0b9c5bca5547b968da20cb60af094fafcb52be05f/imagecodecs-2025.8.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10f421048206ffb6763b1eb28f2205535801794e9f2c612bbcc219d14b9e44bc", size = 25765528, upload-time = "2025-08-03T06:07:08.044Z" }, { url = "https://files.pythonhosted.org/packages/d6/1a/f12736807a179a4bd5a26387f2edf00cc3f972139b61f2833f6fbe9b9685/imagecodecs-2025.8.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f14a5eb8151e32316028c09212eed1d14334460e8adbbef819c1d8ec6ab7e633", size = 26718026, upload-time = "2025-08-03T06:07:13.54Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cd/b0367017443af901279952817900e7f496998fb11de24d91cccdb4fa3dc3/imagecodecs-2025.8.2-cp312-cp312-win32.whl", hash = "sha256:e5cb491fa698d055643d3519bab36a3da8d4144ac759430464e2f75f945f3325", size = 18546766, upload-time = "2025-08-03T06:07:16.836Z" }, { url = "https://files.pythonhosted.org/packages/c4/97/5eb48564f5fdbe3ef83a07ba45d301d45bf85af382a051de34e0a3d6af85/imagecodecs-2025.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:7371169cff1b98f07a983b28fffd2d334cf40a61fce3c876dda930149081bdeb", size = 22674367, upload-time = "2025-08-03T06:07:20.208Z" }, { url = "https://files.pythonhosted.org/packages/2b/b9/a1a70b5d2250937cd747ceff94ddb9cb70d6f1b9fef9f4164d88c63dd3cb/imagecodecs-2025.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:bf0a97cd58810d5d7ecd983332f1f85a0df991080a774f7caadb1add172165d0", size = 17981844, upload-time = "2025-08-03T06:07:23.219Z" }, { url = "https://files.pythonhosted.org/packages/f2/69/77a5c020030dbba1a566264b9201f091534b4b10e9ac5725b7bd40895a8b/imagecodecs-2025.8.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:06a1b54847cbf595ed58879936eabdab990f1780b80d03a8d4edb57e61c768b0", size = 12454348, upload-time = "2025-08-03T06:07:26.135Z" }, { url = "https://files.pythonhosted.org/packages/f9/5d/c5dd7f0706dc48d38deefe4cba05ac78236b662a8ef86f9c0cc569b9db96/imagecodecs-2025.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbf3abd281034e29034b773407c27eff35dd94832a9b6e3c97490db079e3c0a8", size = 10103114, upload-time = "2025-08-03T06:07:28.657Z" }, { url = "https://files.pythonhosted.org/packages/60/40/8b656577120f758ce4b177917b57c76f15e695ff0e63584f641db2063bbe/imagecodecs-2025.8.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:248bb2ea3e43690196acdb1dae5aa40213dbd00c47255295d57da80770ceeaa7", size = 25602557, upload-time = "2025-08-03T06:07:32.399Z" }, { url = "https://files.pythonhosted.org/packages/8a/de/6c1cf78cc0ecc45d98a0eb0d8920df7b90719f8643c7ed9b1bb700f95890/imagecodecs-2025.8.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52230fcd0c331b0167ccd14d7ce764bb780006b65bf69761d8bde6863419fdbf", size = 26544468, upload-time = "2025-08-03T06:07:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/51/14/bda4974256e31eca65e33446026c91d54d1124aa59ce042fc326836597ec/imagecodecs-2025.8.2-cp313-cp313-win32.whl", hash = "sha256:151f9e0879ed8a025b19fcffbf4785dacf29002d5fec94d318e84ba154ddd54c", size = 18534480, upload-time = "2025-08-03T06:07:39.901Z" }, { url = "https://files.pythonhosted.org/packages/a6/bb/ada8851ee56ab835562fb96f764f055e16a5d43a81ebc95207f6b2f3c1d4/imagecodecs-2025.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:d167c265caaf36f9f090e55b68a7c0ec4e5b436923cdc047358743f450534154", size = 22662134, upload-time = "2025-08-03T06:07:43.845Z" }, { url = "https://files.pythonhosted.org/packages/7c/b6/687ad4e137fe637213540cf71bf6a3957cc48667ce6d96d6d9dcb8409305/imagecodecs-2025.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:37199e19d61c7a6c0cf901859d7a81c4449d18047a15ca9d2ebe17176c8d1b69", size = 17968181, upload-time = "2025-08-03T06:07:47.254Z" }, - { url = "https://files.pythonhosted.org/packages/63/5f/2be51d6ea6e6cae13d8d4ce77d5076ef72e492f670368bb193db35e146ce/imagecodecs-2025.8.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:7a3c89b5f5c946d5649892e15b5c89aeca357d048331c3a4ae89009320d2704a", size = 12447596, upload-time = "2025-08-03T06:07:49.804Z" }, - { url = "https://files.pythonhosted.org/packages/6e/75/d9cd579b1fd5fdef5742567236adb49df15beaf8f66219412f21fb86a64c/imagecodecs-2025.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a4d2249e4d25a57da7da336c68efc72a7e15f9271dc6c13c322947f30383b8e", size = 10107274, upload-time = "2025-08-03T06:07:52.787Z" }, - { url = "https://files.pythonhosted.org/packages/57/46/011e41f99f2301091f902afd917a4a8079056510dfcb8b8529d96e4232c8/imagecodecs-2025.8.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31ebfca1bd01c6e4db25c0a91301ddc9aeca1dc63642db689543a9700a5869e8", size = 25579702, upload-time = "2025-08-03T06:07:56.392Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9b/b2d38a3902b2a61cfdbe1bca7aa939b77e32349d00aba7a4014d1dfd8cb9/imagecodecs-2025.8.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be84bd8f3cf1552efc92f2465af5ab40a14a788d48482ef3a702e9dae5f4cd0b", size = 26372160, upload-time = "2025-08-03T06:08:00.079Z" }, - { url = "https://files.pythonhosted.org/packages/c2/96/a22ce5b43a93d12b21f2ec9e374cd4a7edf9347630c7ed90bef7c4ca9f5d/imagecodecs-2025.8.2-cp314-cp314-win32.whl", hash = "sha256:ce57af27547c42dfb888562a1a22dc51a6103c20b3fb69ac4c26121acc741ade", size = 18802212, upload-time = "2025-08-03T06:08:03.341Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0c/d364bff2ffb8f2864e9f7fa6dd05b57c25a4c341ba1e2f08b3dea1dc8a6d/imagecodecs-2025.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:b93d77293c0aa9e661d42f3203b13ea135d5bf9f0936fbbe90780ed1c67322d3", size = 23052776, upload-time = "2025-08-03T06:08:06.975Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4b/54d1e3a2b9d11c8a875c98b543dc5527c259c174b05987b1e79ccfc107b9/imagecodecs-2025.8.2-cp314-cp314-win_arm64.whl", hash = "sha256:d7a53983d4df035761dc652e44c912092bddc5b115d6f5f612df301ce94fcd55", size = 18374009, upload-time = "2025-08-03T06:08:10.074Z" }, ] [[package]] @@ -689,12 +668,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] @@ -782,28 +755,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, - { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, - { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, - { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, - { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, - { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, - { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, - { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, - { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, - { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, - { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, - { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, - { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, ] [[package]] @@ -843,7 +794,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -854,7 +805,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -881,9 +832,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -894,7 +845,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -1092,28 +1043,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, ] [[package]] @@ -1347,24 +1276,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, - { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, - { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, - { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, - { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, - { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, - { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, - { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, - { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, ] [[package]] @@ -1663,35 +1574,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, - { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, - { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, - { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, - { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, - { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, - { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, - { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, ] [[package]] @@ -1779,18 +1661,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, - { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, - { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, ] [[package]] @@ -1829,24 +1699,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665, upload-time = "2025-07-27T16:30:05.916Z" }, { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210, upload-time = "2025-07-27T16:30:11.655Z" }, { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661, upload-time = "2025-07-27T16:30:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921, upload-time = "2025-07-27T16:30:30.081Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152, upload-time = "2025-07-27T16:30:35.336Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028, upload-time = "2025-07-27T16:30:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666, upload-time = "2025-07-27T16:30:43.663Z" }, - { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318, upload-time = "2025-07-27T16:30:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724, upload-time = "2025-07-27T16:30:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335, upload-time = "2025-07-27T16:30:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310, upload-time = "2025-07-27T16:31:06.151Z" }, - { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239, upload-time = "2025-07-27T16:31:59.942Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460, upload-time = "2025-07-27T16:31:11.865Z" }, - { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322, upload-time = "2025-07-27T16:31:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329, upload-time = "2025-07-27T16:31:21.188Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544, upload-time = "2025-07-27T16:31:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112, upload-time = "2025-07-27T16:31:30.62Z" }, - { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594, upload-time = "2025-07-27T16:31:36.112Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080, upload-time = "2025-07-27T16:31:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306, upload-time = "2025-07-27T16:31:48.109Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705, upload-time = "2025-07-27T16:31:53.96Z" }, ] [[package]] @@ -2075,7 +1927,7 @@ name = "triton" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "setuptools", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "setuptools" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, From f2176f4fb95a1a8899e00f7a4b6f99dcbce87f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sat, 7 Mar 2026 21:07:06 +0000 Subject: [PATCH 02/20] feat: fixes --- ratiopath/ray/aggregate/tensor_mean.py | 17 +++++++++++++---- ratiopath/ray/aggregate/tensor_std.py | 22 ++++++++++++++-------- tests/test_ray_aggregations.py | 3 ++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index b56e755..33f290f 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -62,17 +62,17 @@ class TensorMean(AggregateFnV2[dict, np.ndarray | float]): def __init__( self, - on: str | None = None, + on: str, axis: int | tuple[int, ...] | None = None, ignore_nulls: bool = True, alias_name: str | None = None, ): super().__init__( - name=alias_name if alias_name else f"mean({on!s})", + name=alias_name if alias_name else f"mean({on})", on=on, ignore_nulls=ignore_nulls, # Initialize with identity values for summation - zero_factory=lambda: {"sum": None, "shape": None, "count": 0}, + zero_factory=self.zero_factory, ) if axis is not None: @@ -87,8 +87,17 @@ def __init__( self.aggregate_axis = tuple(axes) + @staticmethod + def zero_factory() -> dict: + return {"sum": None, "shape": None, "count": 0} + def aggregate_block(self, block: Block) -> dict: block_acc = BlockAccessor.for_block(block) + + # If there are no valid (non-null) entries, return the zero value + if block_acc.count(self._target_col_name, self._ignore_nulls) == 0: # type: ignore [arg-type] + return self.zero_factory() + # Access the raw numpy data for the column col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) @@ -115,7 +124,7 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: "count": current_accumulator["count"] + new["count"], } - def finalize(self, accumulator: dict) -> np.ndarray | float: + def finalize(self, accumulator: dict) -> np.ndarray | float: # type: ignore [override] count = accumulator["count"] if count == 0: diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index 2feeecd..45b763d 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -51,17 +51,17 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): def __init__( self, - on: str | None = None, + on: str, axis: int | tuple[int, ...] | None = None, ignore_nulls: bool = True, alias_name: str | None = None, ): super().__init__( - name=alias_name if alias_name else f"std({on!s})", + name=alias_name if alias_name else f"std({on})", on=on, ignore_nulls=ignore_nulls, # sum: partial sum, ssd: sum of squared differences, count: elements reduced - zero_factory=lambda: {"sum": None, "ssd": None, "shape": None, "count": 0}, + zero_factory=self.zero_factory, ) if axis is not None: @@ -76,8 +76,17 @@ def __init__( self.aggregate_axis = tuple(sorted(axes)) + @staticmethod + def zero_factory() -> dict: + return {"sum": None, "ssd": None, "shape": None, "count": 0} + def aggregate_block(self, block: Block) -> dict: block_acc = BlockAccessor.for_block(block) + + # If there are no valid (non-null) entries, return the zero value + if block_acc.count(self._target_col_name, self._ignore_nulls) == 0: # type: ignore [arg-type] + return self.zero_factory() + col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) # Partial sum and element count @@ -85,15 +94,12 @@ def aggregate_block(self, block: Block) -> dict: block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) # SSD calculation: sum((x - mean)^2) - # Note: We need to expand block_sum to be broadcastable against col_np - # or calculate ssd using the identity: sum(x^2) - (sum(x)^2 / n) - # The (x - mean)^2 method is usually more numerically stable. if self.aggregate_axis is None: block_ssd = np.sum((col_np - (block_sum / block_count)) ** 2) else: # Re-expand sum for broadcasting expanded_sum = block_sum - for ax in sorted(self.aggregate_axis): + for ax in self.aggregate_axis: expanded_sum = np.expand_dims(expanded_sum, ax) block_ssd = np.sum( (col_np - (expanded_sum / block_count)) ** 2, axis=self.aggregate_axis @@ -132,7 +138,7 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: "count": combined_count, } - def finalize(self, accumulator: dict) -> np.ndarray | float: + def finalize(self, accumulator: dict) -> np.ndarray | float: # type: ignore [override] count = accumulator["count"] if count == 0: diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index 893adf0..20cb840 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -47,7 +47,7 @@ def test_tensor_mean_int_shorthand(ray_start): # Aggregating over axis 1 (internal becomes (0, 1)) result = ds.aggregate(TensorMean(on="m", axis=1)) - expected = np.array([10.0, 15.0]) # [(10+30+0+0)/2, (20+40+0+0)/2] + expected = np.array([10.0, 15.0]) # [(10+30+0+0)/4, (20+40+0+0)/4] np.testing.assert_array_equal(result["mean(m)"], expected) @@ -125,3 +125,4 @@ def test_tensor_aggregate_groupby(ray_start): res_std = sorted(res_std, key=lambda x: x["id"]) np.testing.assert_array_equal(res_std[0]["std(m)"], [1.0, 1.0]) # Std of [1,3] + np.testing.assert_array_equal(res_std[1]["std(m)"], [0.0, 0.0]) # Std of [10 From eb4b53c9d522644ccf016170573294bf7933e754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sat, 7 Mar 2026 21:15:31 +0000 Subject: [PATCH 03/20] feat: format --- ratiopath/ray/aggregate/tensor_std.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index 45b763d..a0f9acf 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -138,7 +138,7 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: "count": combined_count, } - def finalize(self, accumulator: dict) -> np.ndarray | float: # type: ignore [override] + def finalize(self, accumulator: dict) -> np.ndarray | float: # type: ignore [override] count = accumulator["count"] if count == 0: From 131e9f70d945d39c157e553c4aebaf8423e1fc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sun, 8 Mar 2026 12:20:29 +0000 Subject: [PATCH 04/20] feat: std ddof + docs --- ratiopath/ray/aggregate/tensor_mean.py | 4 +-- ratiopath/ray/aggregate/tensor_std.py | 40 +++++++++++++++++++------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index 33f290f..aba46f6 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -85,7 +85,7 @@ def __init__( "independently without collapsing the batch, use .map() instead." ) - self.aggregate_axis = tuple(axes) + self._aggregate_axis = tuple(axes) @staticmethod def zero_factory() -> dict: @@ -102,7 +102,7 @@ def aggregate_block(self, block: Block) -> dict: col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) # Perform the partial sum and calculate how many elements contributed - block_sum = np.sum(col_np, axis=self.aggregate_axis) + block_sum = np.sum(col_np, axis=self._aggregate_axis) block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) return { diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index a0f9acf..e9d5c64 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -11,11 +11,13 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): This aggregator treats the data column as a high-dimensional array where **axis 0 represents the batch dimension**. To satisfy the requirements - of a reduction, axis 0 must be included in the aggregation. + of a reduction and prevent memory growth proportional to the number of rows, + axis 0 must be included in the aggregation. It uses a parallel variance accumulation algorithm (Chan's method) to maintain numerical stability while processing data across multiple Ray blocks. + Args: on: The name of the column containing tensors or numbers. axis: The axis or axes along which the reduction is computed. @@ -25,11 +27,19 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): tensor dimension. For example, `axis=1` collapses the batch and the first dimension of the tensors. - `tuple`: A sequence of axes that **must** explicitly include `0`. + ddof: Delta Degrees of Freedom. The divisor used in calculations + is $N - ddof$, where $N$ represents the number of elements. + Defaults to 1.0 (sample standard deviation). ignore_nulls: Whether to ignore null values. Defaults to True. alias_name: Optional name for the resulting column. Defaults to "std()". Raises: - ValueError: If `axis` is provided but does not include `0`. + ValueError: If `axis` is provided as a tuple but does not include `0`. + + Note: + This aggregator is designed for "reduction" operations. If you wish to + calculate statistics per-row without collapsing the batch dimension, + use `.map()` instead. Example: >>> import ray @@ -41,10 +51,14 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): ... {"m": np.array([[5, 6], [5, 6]])}, ... ] ... ) - >>> # 1. Global Std (axis=None) -> ~2.06 + >>> # 1. Global Std (axis=None) -> All elements reduced to one scalar >>> ds.aggregate(TensorStd(on="m", axis=None)) - >>> # 2. Batch Std (axis=0) -> np.array([[2, 2], [2, 2]]) + >>> # 2. Batch Std (axis=0) -> Result is a 2x2 matrix of std values + >>> # calculated across the dataset rows. >>> ds.aggregate(TensorStd(on="m", axis=0)) + >>> # 3. Int shorthand (axis=1) -> Internally uses axis=(0, 1) + >>> # Collapses batch and the first dimension of the tensor. + >>> ds.aggregate(TensorStd(on="m", axis=1)) """ aggregate_axis: tuple[int, ...] | None = None @@ -53,6 +67,7 @@ def __init__( self, on: str, axis: int | tuple[int, ...] | None = None, + ddof: float = 1.0, ignore_nulls: bool = True, alias_name: str | None = None, ): @@ -64,6 +79,8 @@ def __init__( zero_factory=self.zero_factory, ) + self._ddof = ddof + if axis is not None: axes = {0, axis} if isinstance(axis, int) else set(axis) @@ -74,7 +91,7 @@ def __init__( "independently without collapsing the batch, use .map() instead." ) - self.aggregate_axis = tuple(sorted(axes)) + self._aggregate_axis = tuple(sorted(axes)) @staticmethod def zero_factory() -> dict: @@ -90,19 +107,19 @@ def aggregate_block(self, block: Block) -> dict: col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) # Partial sum and element count - block_sum = np.sum(col_np, axis=self.aggregate_axis) + block_sum = np.sum(col_np, axis=self._aggregate_axis) block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) # SSD calculation: sum((x - mean)^2) - if self.aggregate_axis is None: + if self._aggregate_axis is None: block_ssd = np.sum((col_np - (block_sum / block_count)) ** 2) else: # Re-expand sum for broadcasting expanded_sum = block_sum - for ax in self.aggregate_axis: + for ax in self._aggregate_axis: expanded_sum = np.expand_dims(expanded_sum, ax) block_ssd = np.sum( - (col_np - (expanded_sum / block_count)) ** 2, axis=self.aggregate_axis + (col_np - (expanded_sum / block_count)) ** 2, axis=self._aggregate_axis ) return { @@ -141,9 +158,10 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: def finalize(self, accumulator: dict) -> np.ndarray | float: # type: ignore [override] count = accumulator["count"] - if count == 0: + if count - self._ddof <= 0: return np.nan return np.sqrt( - np.asarray(accumulator["ssd"]).reshape(accumulator["shape"]) / count + np.asarray(accumulator["ssd"]).reshape(accumulator["shape"]) + / (count - self._ddof) ) From 73eaa0dbc62e954307ab1c94515f276362c5c897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sun, 8 Mar 2026 12:21:00 +0000 Subject: [PATCH 05/20] feat: docs update lint --- ratiopath/ray/aggregate/tensor_std.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index e9d5c64..afd8071 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -17,7 +17,6 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): It uses a parallel variance accumulation algorithm (Chan's method) to maintain numerical stability while processing data across multiple Ray blocks. - Args: on: The name of the column containing tensors or numbers. axis: The axis or axes along which the reduction is computed. From f06fdf87703f2dcfbdc63afa6b6deb92b4c65218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sun, 8 Mar 2026 12:25:44 +0000 Subject: [PATCH 06/20] feat: docs --- ratiopath/ray/aggregate/tensor_mean.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index aba46f6..00d3e42 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -11,12 +11,9 @@ class TensorMean(AggregateFnV2[dict, np.ndarray | float]): This aggregator treats the data column as a high-dimensional array where **axis 0 represents the batch dimension**. To satisfy the requirements - of a reduction, axis 0 must be included in the aggregation. This ensures - that the data is collapsed across rows, preventing the internal state - from growing linearly with the dataset size. + of a reduction and prevent memory growth proportional to the number of rows, + axis 0 must be included in the aggregation. - The `axis` parameter defines which dimensions of the 2D+ block (Batch, ...) - should be collapsed. Args: on: The name of the column containing tensors or numbers. @@ -31,11 +28,12 @@ class TensorMean(AggregateFnV2[dict, np.ndarray | float]): alias_name: Optional name for the resulting column. Defaults to "mean()". Raises: - ValueError: If `axis` is provided but does not include `0`. + ValueError: If `axis` is provided as a tuple but does not include `0`. Note: - If you wish to perform operations on tensors independently without - collapsing the batch dimension (axis 0), use `.map()` instead. + This aggregator is designed for "reduction" operations. If you wish to + calculate statistics per-row without collapsing the batch dimension, + use `.map()` instead. Example: >>> import ray From 0dab47aa1f1be0d9cb743f9d2871bf023b368ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sun, 8 Mar 2026 17:06:59 +0000 Subject: [PATCH 07/20] feat: param fix --- ratiopath/ray/aggregate/tensor_mean.py | 2 +- ratiopath/ray/aggregate/tensor_std.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index 00d3e42..e1cc339 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -56,7 +56,7 @@ class TensorMean(AggregateFnV2[dict, np.ndarray | float]): >>> ds.aggregate(TensorMean(on="m", axis=(0, 1))) """ - aggregate_axis: tuple[int, ...] | None = None + _aggregate_axis: tuple[int, ...] | None = None def __init__( self, diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index afd8071..333951e 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -60,7 +60,7 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): >>> ds.aggregate(TensorStd(on="m", axis=1)) """ - aggregate_axis: tuple[int, ...] | None = None + _aggregate_axis: tuple[int, ...] | None = None def __init__( self, From 746fc433b42ad776fd351a1818d1bca9b0da990e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Sun, 8 Mar 2026 17:12:08 +0000 Subject: [PATCH 08/20] fix: ddof test --- tests/test_ray_aggregations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index 20cb840..1f62636 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -73,7 +73,7 @@ def test_tensor_std_global(ray_start): data = [{"m": vals[:4].reshape(2, 2)}, {"m": vals[4:].reshape(2, 2)}] ds = ray.data.from_items(data) - result = ds.aggregate(TensorStd(on="m", axis=None)) + result = ds.aggregate(TensorStd(on="m", axis=None, ddof=0)) expected = np.std(vals) assert pytest.approx(result["std(m)"], 0.0001) == expected @@ -86,7 +86,7 @@ def test_tensor_std_batch_only(ray_start): {"m": np.array([30, 40])}, # Sample 2 ] ds = ray.data.from_items(data) - result = ds.aggregate(TensorStd(on="m", axis=0)) + result = ds.aggregate(TensorStd(on="m", axis=0, ddof=0)) # Std of [10, 30] is 10; Std of [20, 40] is 10 expected = np.array([10.0, 10.0]) @@ -121,8 +121,8 @@ def test_tensor_aggregate_groupby(ray_start): np.testing.assert_array_equal(res_mean[1]["mean(m)"], [10.0, 10.0]) # Test Std Groupby - res_std = ds.groupby("id").aggregate(TensorStd(on="m", axis=0)).take_all() + res_std = ds.groupby("id").aggregate(TensorStd(on="m", axis=0, ddof=0)).take_all() res_std = sorted(res_std, key=lambda x: x["id"]) np.testing.assert_array_equal(res_std[0]["std(m)"], [1.0, 1.0]) # Std of [1,3] - np.testing.assert_array_equal(res_std[1]["std(m)"], [0.0, 0.0]) # Std of [10 + np.testing.assert_array_equal(res_std[1]["std(m)"], [0.0, 0.0]) # Std of [10,10] From 0343a791ddc1e2baa9262de4c2821bd2eb1252a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 09:53:22 +0000 Subject: [PATCH 09/20] feat: std stability --- ratiopath/ray/aggregate/tensor_std.py | 68 +++++++++++++++++---------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index 333951e..f8c63a0 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -52,9 +52,11 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): ... ) >>> # 1. Global Std (axis=None) -> All elements reduced to one scalar >>> ds.aggregate(TensorStd(on="m", axis=None)) + >>> # 2. Batch Std (axis=0) -> Result is a 2x2 matrix of std values >>> # calculated across the dataset rows. >>> ds.aggregate(TensorStd(on="m", axis=0)) + >>> # 3. Int shorthand (axis=1) -> Internally uses axis=(0, 1) >>> # Collapses batch and the first dimension of the tensor. >>> ds.aggregate(TensorStd(on="m", axis=1)) @@ -74,7 +76,6 @@ def __init__( name=alias_name if alias_name else f"std({on})", on=on, ignore_nulls=ignore_nulls, - # sum: partial sum, ssd: sum of squared differences, count: elements reduced zero_factory=self.zero_factory, ) @@ -94,37 +95,34 @@ def __init__( @staticmethod def zero_factory() -> dict: - return {"sum": None, "ssd": None, "shape": None, "count": 0} + return {"k": 0, "mean": 0, "ssd": 0, "shape": None, "count": 0} def aggregate_block(self, block: Block) -> dict: block_acc = BlockAccessor.for_block(block) - # If there are no valid (non-null) entries, return the zero value if block_acc.count(self._target_col_name, self._ignore_nulls) == 0: # type: ignore [arg-type] return self.zero_factory() col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) # Partial sum and element count - block_sum = np.sum(col_np, axis=self._aggregate_axis) + block_sum = np.sum(col_np, axis=self._aggregate_axis, keepdims=True) block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) - # SSD calculation: sum((x - mean)^2) - if self._aggregate_axis is None: - block_ssd = np.sum((col_np - (block_sum / block_count)) ** 2) - else: - # Re-expand sum for broadcasting - expanded_sum = block_sum - for ax in self._aggregate_axis: - expanded_sum = np.expand_dims(expanded_sum, ax) - block_ssd = np.sum( - (col_np - (expanded_sum / block_count)) ** 2, axis=self._aggregate_axis - ) + # Compute the reference point K for stable variance calculation# + k = block_sum / block_count + + # Shift the data by K to improve numerical stability when calculating SSD + shifted = col_np - k + mean = np.sum(shifted, axis=self._aggregate_axis) / block_count + + block_ssd = np.sum((shifted - mean) ** 2, axis=self._aggregate_axis) return { - "sum": block_sum.flatten(), + "k": k.flatten(), + "mean": mean.flatten(), "ssd": block_ssd.flatten(), - "shape": block_sum.shape, + "shape": mean.shape, "count": block_count, } @@ -135,20 +133,33 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: if current_accumulator["count"] == 0: return new - mean_a = np.asarray(current_accumulator["sum"]) / current_accumulator["count"] - mean_b = np.asarray(new["sum"]) / new["count"] - delta = mean_b - mean_a + n_current = current_accumulator["count"] + n_new = new["count"] + combined_count = n_current + n_new + + k_current = np.asarray(current_accumulator["k"]) + k_new = np.asarray(new["k"]) + + mean_current = np.asarray(current_accumulator["mean"]) + mean_new = np.asarray(new["mean"]) + + # Calculate delta stably using the reference points. + # This is algebraically identical to (mean_b - mean_a), but Sterbenz Lemma + # ensures (K2 - K1) is computed without catastrophic precision loss. + delta = (k_new - k_current) + mean_new - mean_current + + # Chan's formula for the combined true mean + combined_true_mean = (k_current + mean_current) + delta * n_new / combined_count - combined_sum = np.asarray(current_accumulator["sum"]) + np.asarray(new["sum"]) - combined_count = current_accumulator["count"] + new["count"] combined_ssd = ( np.asarray(current_accumulator["ssd"]) + np.asarray(new["ssd"]) - + (delta**2 * current_accumulator["count"] * new["count"] / combined_count) + + (delta**2 * n_current * n_new / combined_count) ) return { - "sum": combined_sum, + "K": combined_true_mean, + "mean": np.zeros_like(combined_true_mean), "ssd": combined_ssd, "shape": new["shape"], "count": combined_count, @@ -160,7 +171,12 @@ def finalize(self, accumulator: dict) -> np.ndarray | float: # type: ignore [ov if count - self._ddof <= 0: return np.nan - return np.sqrt( + # np.maximum added as a safety net. Floating point jitter can occasionally + # result in trivially negative numbers (e.g., -1e-16), which crashes np.sqrt + variance = np.maximum( + 0.0, np.asarray(accumulator["ssd"]).reshape(accumulator["shape"]) - / (count - self._ddof) + / (count - self._ddof), ) + + return np.sqrt(variance) From 8bb4c47ede504fec3e751794edd83577bfc7177b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 09:53:57 +0000 Subject: [PATCH 10/20] chore: version update --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7392eb5..db35120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ratiopath" -version = "1.2.0" +version = "1.3.0" description = "A library for efficient processing and analysis of whole-slide pathology images." authors = [ { name = "Matěj Pekár", email = "matejpekar@mail.muni.cz" }, From e403c8167d5efe7e2684f41160a61b59a4b7bfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 09:54:56 +0000 Subject: [PATCH 11/20] chore: lock --- uv.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/uv.lock b/uv.lock index 1a106e3..072407b 100644 --- a/uv.lock +++ b/uv.lock @@ -794,7 +794,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -805,7 +805,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -832,9 +832,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -845,7 +845,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -1390,7 +1390,7 @@ wheels = [ [[package]] name = "ratiopath" -version = "1.2.0" +version = "1.3.0" source = { virtual = "." } dependencies = [ { name = "albumentations" }, @@ -1927,7 +1927,7 @@ name = "triton" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "setuptools" }, + { name = "setuptools", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, From 51891b6673887c158663c7ff1cdefe69741927b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 09:56:43 +0000 Subject: [PATCH 12/20] feat: docs --- ratiopath/ray/aggregate/tensor_std.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index f8c63a0..a8139d7 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -52,11 +52,11 @@ class TensorStd(AggregateFnV2[dict, np.ndarray | float]): ... ) >>> # 1. Global Std (axis=None) -> All elements reduced to one scalar >>> ds.aggregate(TensorStd(on="m", axis=None)) - + >>> >>> # 2. Batch Std (axis=0) -> Result is a 2x2 matrix of std values >>> # calculated across the dataset rows. >>> ds.aggregate(TensorStd(on="m", axis=0)) - + >>> >>> # 3. Int shorthand (axis=1) -> Internally uses axis=(0, 1) >>> # Collapses batch and the first dimension of the tensor. >>> ds.aggregate(TensorStd(on="m", axis=1)) From 3a67fc04ce4b4f0f3f47abad3b6f8b2ce01104d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 09:57:26 +0000 Subject: [PATCH 13/20] feat: zero factory --- ratiopath/ray/aggregate/tensor_mean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index e1cc339..eec49ff 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -87,7 +87,7 @@ def __init__( @staticmethod def zero_factory() -> dict: - return {"sum": None, "shape": None, "count": 0} + return {"sum": 0, "shape": None, "count": 0} def aggregate_block(self, block: Block) -> dict: block_acc = BlockAccessor.for_block(block) From ec462b770b0822dfa6c043f68f5c75b9fa310125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 11:13:19 +0000 Subject: [PATCH 14/20] feat: docs + fix --- .../reference/ray/aggregations/tensor_mean.md | 3 +++ docs/reference/ray/aggregations/tensor_std.md | 3 +++ mkdocs.yml | 3 +++ ratiopath/ray/aggregate/tensor_std.py | 2 +- tests/test_ray_aggregations.py | 24 ++++++++++++++----- 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 docs/reference/ray/aggregations/tensor_mean.md create mode 100644 docs/reference/ray/aggregations/tensor_std.md diff --git a/docs/reference/ray/aggregations/tensor_mean.md b/docs/reference/ray/aggregations/tensor_mean.md new file mode 100644 index 0000000..62f96a5 --- /dev/null +++ b/docs/reference/ray/aggregations/tensor_mean.md @@ -0,0 +1,3 @@ +# ratiopath.ray.aggregate.TensorMean + +::: ratiopath.ray.aggregate.TensorMean diff --git a/docs/reference/ray/aggregations/tensor_std.md b/docs/reference/ray/aggregations/tensor_std.md new file mode 100644 index 0000000..e060aee --- /dev/null +++ b/docs/reference/ray/aggregations/tensor_std.md @@ -0,0 +1,3 @@ +# ratiopath.ray.aggregate.TensorStd + +::: ratiopath.ray.aggregate.TensorStd diff --git a/mkdocs.yml b/mkdocs.yml index fc71d68..162b66f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,9 @@ nav: - estimate_stain_vectors: reference/augmentations/estimate_stain_vectors.md - StainAugmentor: reference/augmentations/stain_augmentor.md - Ray: + - Aggregations: + - tensor_mean: reference/ray/aggregations/tensor_mean.md + - tensor_std: reference/ray/aggregations/tensor_std.md - read_slides: reference/ray/read_slides.md - VipsTiffDatasink: reference/ray/vips_tiff_datasink.md - OpenSlide: reference/openslide.md diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index a8139d7..03de710 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -158,7 +158,7 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: ) return { - "K": combined_true_mean, + "k": combined_true_mean, "mean": np.zeros_like(combined_true_mean), "ssd": combined_ssd, "shape": new["shape"], diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index 1f62636..a4821ef 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -31,7 +31,9 @@ def test_tensor_mean_global(ray_start): {"m": np.array([[2, 4], [6, 8]])}, {"m": np.array([[0, 0], [0, 0]])}, ] - ds = ray.data.from_items(data) + ds = ray.data.from_items(data).repartition( + 2 + ) # Ensure multiple blocks for reduction result = ds.aggregate(TensorMean(on="m", axis=None)) # (2+4+6+8) / 8 = 2.5 assert result["mean(m)"] == 2.5 @@ -43,7 +45,9 @@ def test_tensor_mean_int_shorthand(ray_start): {"m": np.array([[10, 20], [30, 40]])}, # Row sums: 30, 70 {"m": np.array([[0, 0], [0, 0]])}, # Row sums: 0, 0 ] - ds = ray.data.from_items(data) + ds = ray.data.from_items(data).repartition( + 2 + ) # Ensure multiple blocks for reduction # Aggregating over axis 1 (internal becomes (0, 1)) result = ds.aggregate(TensorMean(on="m", axis=1)) @@ -57,7 +61,9 @@ def test_tensor_mean_batch_only(ray_start): {"m": np.array([[10, 10], [10, 10]])}, {"m": np.array([[20, 20], [20, 20]])}, ] - ds = ray.data.from_items(data) + ds = ray.data.from_items(data).repartition( + 2 + ) # Ensure multiple blocks for reduction result = ds.aggregate(TensorMean(on="m", axis=0)) expected = np.array([[15.0, 15.0], [15.0, 15.0]]) @@ -71,7 +77,9 @@ def test_tensor_std_global(ray_start): """Tests global standard deviation.""" vals = np.array([1, 2, 3, 4, 5, 6, 7, 8]) data = [{"m": vals[:4].reshape(2, 2)}, {"m": vals[4:].reshape(2, 2)}] - ds = ray.data.from_items(data) + ds = ray.data.from_items(data).repartition( + 2 + ) # Ensure multiple blocks for reduction result = ds.aggregate(TensorStd(on="m", axis=None, ddof=0)) expected = np.std(vals) @@ -85,7 +93,9 @@ def test_tensor_std_batch_only(ray_start): {"m": np.array([10, 20])}, # Sample 1 {"m": np.array([30, 40])}, # Sample 2 ] - ds = ray.data.from_items(data) + ds = ray.data.from_items(data).repartition( + 2 + ) # Ensure multiple blocks for reduction result = ds.aggregate(TensorStd(on="m", axis=0, ddof=0)) # Std of [10, 30] is 10; Std of [20, 40] is 10 @@ -111,7 +121,9 @@ def test_tensor_aggregate_groupby(ray_start): {"id": "A", "m": np.array([3, 3])}, {"id": "B", "m": np.array([10, 10])}, ] - ds = ray.data.from_items(data) + ds = ray.data.from_items(data).repartition( + 2 + ) # Ensure multiple blocks for reduction # Test Mean Groupby res_mean = ds.groupby("id").aggregate(TensorMean(on="m", axis=0)).take_all() From ded51b1336e915983cad3c31d591fb1a0e859ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Thu, 12 Mar 2026 22:48:42 +0000 Subject: [PATCH 15/20] feat: CR fixes --- ratiopath/ray/aggregate/tensor_mean.py | 4 +++- ratiopath/ray/aggregate/tensor_std.py | 5 ++++- tests/test_ray_aggregations.py | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index eec49ff..9c0411e 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -96,8 +96,10 @@ def aggregate_block(self, block: Block) -> dict: if block_acc.count(self._target_col_name, self._ignore_nulls) == 0: # type: ignore [arg-type] return self.zero_factory() - # Access the raw numpy data for the column col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) + if self._ignore_nulls and col_np.dtype == object: + # Filter out Nones) + col_np = np.stack([x for x in col_np if x is not None]) # Perform the partial sum and calculate how many elements contributed block_sum = np.sum(col_np, axis=self._aggregate_axis) diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index 03de710..72573ea 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -91,7 +91,7 @@ def __init__( "independently without collapsing the batch, use .map() instead." ) - self._aggregate_axis = tuple(sorted(axes)) + self._aggregate_axis = tuple(axes) @staticmethod def zero_factory() -> dict: @@ -104,6 +104,9 @@ def aggregate_block(self, block: Block) -> dict: return self.zero_factory() col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) + if self._ignore_nulls and col_np.dtype == object: + # Filter out Nones) + col_np = np.stack([x for x in col_np if x is not None]) # Partial sum and element count block_sum = np.sum(col_np, axis=self._aggregate_axis, keepdims=True) diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index a4821ef..1fa263e 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -30,10 +30,9 @@ def test_tensor_mean_global(ray_start): data = [ {"m": np.array([[2, 4], [6, 8]])}, {"m": np.array([[0, 0], [0, 0]])}, + {"m": None}, ] - ds = ray.data.from_items(data).repartition( - 2 - ) # Ensure multiple blocks for reduction + ds = ray.data.from_items(data) result = ds.aggregate(TensorMean(on="m", axis=None)) # (2+4+6+8) / 8 = 2.5 assert result["mean(m)"] == 2.5 @@ -119,6 +118,7 @@ def test_tensor_aggregate_groupby(ray_start): data = [ {"id": "A", "m": np.array([1, 1])}, {"id": "A", "m": np.array([3, 3])}, + {"id": "B", "m": None}, {"id": "B", "m": np.array([10, 10])}, ] ds = ray.data.from_items(data).repartition( From 521515404699e712423d6443a492830195fac5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Fri, 13 Mar 2026 08:33:46 +0000 Subject: [PATCH 16/20] fix: test --- tests/test_ray_aggregations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index 1fa263e..4636610 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -118,12 +118,9 @@ def test_tensor_aggregate_groupby(ray_start): data = [ {"id": "A", "m": np.array([1, 1])}, {"id": "A", "m": np.array([3, 3])}, - {"id": "B", "m": None}, {"id": "B", "m": np.array([10, 10])}, ] - ds = ray.data.from_items(data).repartition( - 2 - ) # Ensure multiple blocks for reduction + ds = ray.data.from_items(data) # Test Mean Groupby res_mean = ds.groupby("id").aggregate(TensorMean(on="m", axis=0)).take_all() From b65f7d9645f81eb99b5ccd3e5c569f93ab46d0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Fri, 13 Mar 2026 09:25:05 +0000 Subject: [PATCH 17/20] feat: handle nulls --- ratiopath/ray/aggregate/tensor_mean.py | 31 +++++++++++++++++--------- ratiopath/ray/aggregate/tensor_std.py | 23 +++++++++++++++---- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index 9c0411e..cb5f494 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -97,13 +97,28 @@ def aggregate_block(self, block: Block) -> dict: return self.zero_factory() col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) - if self._ignore_nulls and col_np.dtype == object: - # Filter out Nones) - col_np = np.stack([x for x in col_np if x is not None]) + + # Handle object dtype (triggered by nulls or ragged tensor shapes) + if col_np.dtype == object: + valid_tensors = [x for x in col_np if x is not None] + + # If lengths differ, we dropped at least one None. + if len(valid_tensors) != col_np.size and not self._ignore_nulls: + raise ValueError( + f"Column '{self._target_col_name}' contains null values, but " + "ignore_nulls is False." + ) + + # Handle the all-null block case + if not valid_tensors: + return self.zero_factory() + + # Reconstruct the contiguous numeric tensor + col_np = np.stack(valid_tensors) # Perform the partial sum and calculate how many elements contributed block_sum = np.sum(col_np, axis=self._aggregate_axis) - block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) + block_count = col_np.size // block_sum.size return { "sum": block_sum.flatten(), @@ -112,15 +127,9 @@ def aggregate_block(self, block: Block) -> dict: } def combine(self, current_accumulator: dict, new: dict) -> dict: - if new["count"] == 0: - return current_accumulator - - if current_accumulator["count"] == 0: - return new - return { "sum": np.asarray(current_accumulator["sum"]) + np.asarray(new["sum"]), - "shape": new["shape"], + "shape": current_accumulator["shape"] or new["shape"], "count": current_accumulator["count"] + new["count"], } diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index 72573ea..d29191d 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -104,13 +104,28 @@ def aggregate_block(self, block: Block) -> dict: return self.zero_factory() col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) - if self._ignore_nulls and col_np.dtype == object: - # Filter out Nones) - col_np = np.stack([x for x in col_np if x is not None]) + + # Handle object dtype (triggered by nulls or ragged tensor shapes) + if col_np.dtype == object: + valid_tensors = [x for x in col_np if x is not None] + + # If lengths differ, we dropped at least one None. + if len(valid_tensors) != col_np.size and not self._ignore_nulls: + raise ValueError( + f"Column '{self._target_col_name}' contains null values, but " + "ignore_nulls is False." + ) + + # Handle the all-null block case + if not valid_tensors: + return self.zero_factory() + + # Reconstruct the contiguous numeric tensor + col_np = np.stack(valid_tensors) # Partial sum and element count block_sum = np.sum(col_np, axis=self._aggregate_axis, keepdims=True) - block_count = np.prod(col_np.shape) // np.prod(block_sum.shape) + block_count = col_np.size // block_sum.size # Compute the reference point K for stable variance calculation# k = block_sum / block_count From 75051d740cbae33cf0d7eb01039c65f3729c0e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Fri, 13 Mar 2026 23:04:59 +0000 Subject: [PATCH 18/20] feat: fix std & test --- ratiopath/ray/aggregate/tensor_mean.py | 37 +++-- ratiopath/ray/aggregate/tensor_std.py | 68 ++++---- tests/test_ray_aggregations.py | 210 +++++++++++++------------ 3 files changed, 159 insertions(+), 156 deletions(-) diff --git a/ratiopath/ray/aggregate/tensor_mean.py b/ratiopath/ray/aggregate/tensor_mean.py index cb5f494..44b090b 100644 --- a/ratiopath/ray/aggregate/tensor_mean.py +++ b/ratiopath/ray/aggregate/tensor_mean.py @@ -92,28 +92,31 @@ def zero_factory() -> dict: def aggregate_block(self, block: Block) -> dict: block_acc = BlockAccessor.for_block(block) - # If there are no valid (non-null) entries, return the zero value - if block_acc.count(self._target_col_name, self._ignore_nulls) == 0: # type: ignore [arg-type] + # Get exact counts before any NumPy conversion obscures the nulls + valid_count = cast( + "int", + block_acc.count(self._target_col_name, ignore_nulls=True), # type: ignore [arg-type] + ) + total_count = cast( + "int", + block_acc.count(self._target_col_name, ignore_nulls=False), # type: ignore [arg-type] + ) + + # Catch nulls immediately if strict mode is on + if valid_count < total_count and not self._ignore_nulls: + raise ValueError( + f"Column '{self._target_col_name}' contains null values, but " + "ignore_nulls is False." + ) + + if valid_count == 0: return self.zero_factory() col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) - # Handle object dtype (triggered by nulls or ragged tensor shapes) - if col_np.dtype == object: + # Filter out nulls if necessary + if valid_count < total_count: valid_tensors = [x for x in col_np if x is not None] - - # If lengths differ, we dropped at least one None. - if len(valid_tensors) != col_np.size and not self._ignore_nulls: - raise ValueError( - f"Column '{self._target_col_name}' contains null values, but " - "ignore_nulls is False." - ) - - # Handle the all-null block case - if not valid_tensors: - return self.zero_factory() - - # Reconstruct the contiguous numeric tensor col_np = np.stack(valid_tensors) # Perform the partial sum and calculate how many elements contributed diff --git a/ratiopath/ray/aggregate/tensor_std.py b/ratiopath/ray/aggregate/tensor_std.py index d29191d..554b9a2 100644 --- a/ratiopath/ray/aggregate/tensor_std.py +++ b/ratiopath/ray/aggregate/tensor_std.py @@ -95,52 +95,49 @@ def __init__( @staticmethod def zero_factory() -> dict: - return {"k": 0, "mean": 0, "ssd": 0, "shape": None, "count": 0} + return {"mean": 0, "ssd": 0, "shape": None, "count": 0} def aggregate_block(self, block: Block) -> dict: block_acc = BlockAccessor.for_block(block) - if block_acc.count(self._target_col_name, self._ignore_nulls) == 0: # type: ignore [arg-type] + # Get exact counts before any NumPy conversion obscures the nulls + valid_count = cast( + "int", + block_acc.count(self._target_col_name, ignore_nulls=True), # type: ignore [arg-type] + ) + total_count = cast( + "int", + block_acc.count(self._target_col_name, ignore_nulls=False), # type: ignore [arg-type] + ) + + # Catch nulls immediately if strict mode is on + if valid_count < total_count and not self._ignore_nulls: + raise ValueError( + f"Column '{self._target_col_name}' contains null values, but " + "ignore_nulls is False." + ) + + if valid_count == 0: return self.zero_factory() col_np = cast("np.ndarray", block_acc.to_numpy(self._target_col_name)) - # Handle object dtype (triggered by nulls or ragged tensor shapes) - if col_np.dtype == object: + # Filter out nulls if necessary + if valid_count < total_count: valid_tensors = [x for x in col_np if x is not None] - - # If lengths differ, we dropped at least one None. - if len(valid_tensors) != col_np.size and not self._ignore_nulls: - raise ValueError( - f"Column '{self._target_col_name}' contains null values, but " - "ignore_nulls is False." - ) - - # Handle the all-null block case - if not valid_tensors: - return self.zero_factory() - - # Reconstruct the contiguous numeric tensor col_np = np.stack(valid_tensors) # Partial sum and element count block_sum = np.sum(col_np, axis=self._aggregate_axis, keepdims=True) block_count = col_np.size // block_sum.size - # Compute the reference point K for stable variance calculation# - k = block_sum / block_count - - # Shift the data by K to improve numerical stability when calculating SSD - shifted = col_np - k - mean = np.sum(shifted, axis=self._aggregate_axis) / block_count - - block_ssd = np.sum((shifted - mean) ** 2, axis=self._aggregate_axis) + mean = block_sum / block_count + block_ssd = np.sum((col_np - mean) ** 2, axis=self._aggregate_axis) return { - "k": k.flatten(), - "mean": mean.flatten(), - "ssd": block_ssd.flatten(), - "shape": mean.shape, + "mean": mean.ravel(), + "ssd": block_ssd.ravel(), + "shape": block_ssd.shape, "count": block_count, } @@ -155,19 +152,13 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: n_new = new["count"] combined_count = n_current + n_new - k_current = np.asarray(current_accumulator["k"]) - k_new = np.asarray(new["k"]) - mean_current = np.asarray(current_accumulator["mean"]) mean_new = np.asarray(new["mean"]) - # Calculate delta stably using the reference points. - # This is algebraically identical to (mean_b - mean_a), but Sterbenz Lemma - # ensures (K2 - K1) is computed without catastrophic precision loss. - delta = (k_new - k_current) + mean_new - mean_current + delta = mean_new - mean_current # Chan's formula for the combined true mean - combined_true_mean = (k_current + mean_current) + delta * n_new / combined_count + combined_mean = (mean_current * n_current + mean_new * n_new) / combined_count combined_ssd = ( np.asarray(current_accumulator["ssd"]) @@ -176,8 +167,7 @@ def combine(self, current_accumulator: dict, new: dict) -> dict: ) return { - "k": combined_true_mean, - "mean": np.zeros_like(combined_true_mean), + "mean": combined_mean, "ssd": combined_ssd, "shape": new["shape"], "count": combined_count, diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index 4636610..c356bc2 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -1,137 +1,147 @@ -import os - import numpy as np import pytest import ray - -# Set environment variables before ray.init -os.environ["RAY_ENABLE_METRICS_EXPORT"] = "0" -os.environ["RAY_IGNORE_VENV_MISMATCH"] = "1" -os.environ["RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO"] = "0" - -# Adjust imports based on your file structure from ratiopath.ray.aggregate import TensorMean, TensorStd -@pytest.fixture(scope="module") -def ray_start(): - if not ray.is_initialized(): - ray.init(ignore_reinit_error=True) +@pytest.fixture(scope="module", autouse=True) +def ray_init(): + """Initializes and tears down Ray for the test module.""" + ray.init(ignore_reinit_error=True) yield ray.shutdown() -## --- TensorMean Tests --- +@pytest.fixture +def sample_tensors(): + """Provides a list of dictionaries containing numpy arrays.""" + np.random.seed(42) + return [{"m": np.random.rand(4, 5)} for _ in range(10)] -def test_tensor_mean_global(ray_start): - """Tests axis=None: Global reduction to a single scalar.""" - data = [ - {"m": np.array([[2, 4], [6, 8]])}, - {"m": np.array([[0, 0], [0, 0]])}, - {"m": None}, - ] - ds = ray.data.from_items(data) - result = ds.aggregate(TensorMean(on="m", axis=None)) - # (2+4+6+8) / 8 = 2.5 - assert result["mean(m)"] == 2.5 +@pytest.fixture +def stacked_sample_tensors(sample_tensors): + """Provides the exact NumPy equivalent of the stacked dataset for validation.""" + return np.stack([item["m"] for item in sample_tensors]) -def test_tensor_mean_int_shorthand(ray_start): - """Tests axis=1: Should aggregate over batch (0) AND dim 1.""" - data = [ - {"m": np.array([[10, 20], [30, 40]])}, # Row sums: 30, 70 - {"m": np.array([[0, 0], [0, 0]])}, # Row sums: 0, 0 - ] - ds = ray.data.from_items(data).repartition( - 2 - ) # Ensure multiple blocks for reduction - # Aggregating over axis 1 (internal becomes (0, 1)) - result = ds.aggregate(TensorMean(on="m", axis=1)) +class TestTensorAggregatorInit: + """Tests initialization and validation logic of the aggregators.""" - expected = np.array([10.0, 15.0]) # [(10+30+0+0)/4, (20+40+0+0)/4] - np.testing.assert_array_equal(result["mean(m)"], expected) + @pytest.mark.parametrize("AggClass", [TensorMean, TensorStd]) + def test_invalid_axis_raises_value_error(self, AggClass): + with pytest.raises(ValueError, match="Axis 0 .* must be included"): + AggClass(on="m", axis=(1, 2)) + @pytest.mark.parametrize("AggClass", [TensorMean, TensorStd]) + def test_valid_axis_initialization(self, AggClass): + # Should not raise + AggClass(on="m", axis=None) + AggClass(on="m", axis=0) + AggClass(on="m", axis=1) + AggClass(on="m", axis=(0, 1)) -def test_tensor_mean_batch_only(ray_start): - """Tests axis=0: Should collapse only the batch dimension.""" - data = [ - {"m": np.array([[10, 10], [10, 10]])}, - {"m": np.array([[20, 20], [20, 20]])}, - ] - ds = ray.data.from_items(data).repartition( - 2 - ) # Ensure multiple blocks for reduction - result = ds.aggregate(TensorMean(on="m", axis=0)) - expected = np.array([[15.0, 15.0], [15.0, 15.0]]) - np.testing.assert_array_equal(result["mean(m)"], expected) +class TestTensorMean: + """End-to-end tests for TensorMean over Ray datasets.""" + @pytest.mark.parametrize( + "axis, expected_axis", + [ + (None, None), # Global mean + (0, 0), # Batch mean + (1, (0, 1)), # Batch + Dim 1 + ((0, 2), (0, 2)), # Batch + Dim 2 + ], + ) + def test_mean_accuracy( + self, sample_tensors, stacked_sample_tensors, axis, expected_axis + ): + ds = ray.data.from_items(sample_tensors).repartition(4) -## --- TensorStd Tests --- - + agg = TensorMean(on="m", axis=axis, alias_name="result") + ray_result = ds.aggregate(agg)["result"] -def test_tensor_std_global(ray_start): - """Tests global standard deviation.""" - vals = np.array([1, 2, 3, 4, 5, 6, 7, 8]) - data = [{"m": vals[:4].reshape(2, 2)}, {"m": vals[4:].reshape(2, 2)}] - ds = ray.data.from_items(data).repartition( - 2 - ) # Ensure multiple blocks for reduction + expected = np.mean(stacked_sample_tensors, axis=expected_axis) - result = ds.aggregate(TensorStd(on="m", axis=None, ddof=0)) - expected = np.std(vals) - assert pytest.approx(result["std(m)"], 0.0001) == expected + np.testing.assert_allclose(ray_result, expected, rtol=1e-6, atol=1e-8) + def test_mean_ignore_nulls(self): + data = [ + {"m": np.array([1.0, 2.0])}, + {"m": None}, + {"m": np.array([3.0, 4.0])}, + ] + ds = ray.data.from_items(data) -def test_tensor_std_batch_only(ray_start): - """Tests STD across the batch dimension only.""" - # Two identical matrices with different offsets - data = [ - {"m": np.array([10, 20])}, # Sample 1 - {"m": np.array([30, 40])}, # Sample 2 - ] - ds = ray.data.from_items(data).repartition( - 2 - ) # Ensure multiple blocks for reduction - result = ds.aggregate(TensorStd(on="m", axis=0, ddof=0)) + agg_ignore = TensorMean(on="m", axis=0, ignore_nulls=True, alias_name="res") - # Std of [10, 30] is 10; Std of [20, 40] is 10 - expected = np.array([10.0, 10.0]) - np.testing.assert_array_equal(result["std(m)"], expected) + res_ignore = ds.aggregate(agg_ignore)["res"] + np.testing.assert_allclose(res_ignore, np.array([2.0, 3.0])) + agg_strict = TensorMean(on="m", axis=0, ignore_nulls=False, alias_name="res") + with pytest.raises(Exception, match="contains null values"): + ds.aggregate(agg_strict) -## --- Validation & Logic Tests --- +class TestTensorStd: + """End-to-end tests for TensorStd over Ray datasets.""" -def test_invalid_axis_tuple(ray_start): - """Verifies that providing a tuple without axis 0 raises ValueError.""" - with pytest.raises( - ValueError, match=r"Axis 0 \(the batch dimension\) must be included" + @pytest.mark.parametrize( + "axis, expected_axis", + [ + (None, None), + (0, 0), + (1, (0, 1)), + ((0, 2), (0, 2)), + ], + ) + @pytest.mark.parametrize("ddof", [0.0, 1.0]) + def test_std_accuracy( + self, sample_tensors, stacked_sample_tensors, axis, expected_axis, ddof ): - TensorMean(on="m", axis=(1, 2)) + ds = ray.data.from_items(sample_tensors).repartition(4) + + agg = TensorStd(on="m", axis=axis, ddof=ddof, alias_name="result") + + ray_result = ds.aggregate(agg)["result"] + + expected = np.std(stacked_sample_tensors, axis=expected_axis, ddof=ddof) + np.testing.assert_allclose(ray_result, expected, rtol=1e-6, atol=1e-8) + + def test_std_all_null_or_empty(self): + ds_empty = ray.data.from_items([{"m": np.array([1, 2])}]).filter( + lambda x: False + ) + + res_empty = ds_empty.aggregate(TensorStd(on="m", axis=0, alias_name="res"))[ + "res" + ] + assert res_empty is None or np.isnan(res_empty) + ds_single = ray.data.from_items([{"m": np.array([1.0, 2.0])}]) -def test_tensor_aggregate_groupby(ray_start): - """Verifies Mean and Std work within groupby operations.""" - data = [ - {"id": "A", "m": np.array([1, 1])}, - {"id": "A", "m": np.array([3, 3])}, - {"id": "B", "m": np.array([10, 10])}, - ] - ds = ray.data.from_items(data) + res_single = ds_single.aggregate( + TensorStd(on="m", axis=0, ddof=1, alias_name="res") + )["res"] + assert np.isnan(res_single).all() - # Test Mean Groupby - res_mean = ds.groupby("id").aggregate(TensorMean(on="m", axis=0)).take_all() - res_mean = sorted(res_mean, key=lambda x: x["id"]) + def test_std_numerical_stability(self): + """Tests Chan's algorithm with large numbers where naive variance fails.""" + base = 1e9 + data = [ + {"m": np.array([base + 1, base + 2])}, + {"m": np.array([base + 1, base + 2])}, + {"m": np.array([base + 3, base + 4])}, + {"m": np.array([base + 3, base + 4])}, + ] + ds = ray.data.from_items(data).repartition(4) - np.testing.assert_array_equal(res_mean[0]["mean(m)"], [2.0, 2.0]) # Mean of [1,3] - np.testing.assert_array_equal(res_mean[1]["mean(m)"], [10.0, 10.0]) + agg = TensorStd(on="m", axis=0, ddof=1.0, alias_name="res") + ray_result = ds.aggregate(agg)["res"] - # Test Std Groupby - res_std = ds.groupby("id").aggregate(TensorStd(on="m", axis=0, ddof=0)).take_all() - res_std = sorted(res_std, key=lambda x: x["id"]) + stacked = np.stack([d["m"] for d in data]) + expected = np.std(stacked, axis=0, ddof=1.0) - np.testing.assert_array_equal(res_std[0]["std(m)"], [1.0, 1.0]) # Std of [1,3] - np.testing.assert_array_equal(res_std[1]["std(m)"], [0.0, 0.0]) # Std of [10,10] + np.testing.assert_allclose(ray_result, expected, rtol=1e-6, atol=1e-8) From 876e7ae1d0a856fea372a4e920b0a586b27432a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Mon, 16 Mar 2026 11:46:28 +0000 Subject: [PATCH 19/20] fix: lint --- tests/test_ray_aggregations.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index c356bc2..845adb7 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -1,3 +1,5 @@ +import re + import numpy as np import pytest import ray @@ -30,12 +32,12 @@ class TestTensorAggregatorInit: """Tests initialization and validation logic of the aggregators.""" @pytest.mark.parametrize("AggClass", [TensorMean, TensorStd]) - def test_invalid_axis_raises_value_error(self, AggClass): - with pytest.raises(ValueError, match="Axis 0 .* must be included"): + def test_invalid_axis_raises_value_error(self, AggClass): # noqa: N803 + with pytest.raises(ValueError, match=re.escape("Axis 0 .* must be included")): AggClass(on="m", axis=(1, 2)) @pytest.mark.parametrize("AggClass", [TensorMean, TensorStd]) - def test_valid_axis_initialization(self, AggClass): + def test_valid_axis_initialization(self, AggClass): # noqa: N803 # Should not raise AggClass(on="m", axis=None) AggClass(on="m", axis=0) From 62d51f6c10cba232e7107c0211246377784ff854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pek=C3=A1r?= <492788@mail.muni.cz> Date: Mon, 16 Mar 2026 11:52:06 +0000 Subject: [PATCH 20/20] fix: tests --- tests/test_ray_aggregations.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_ray_aggregations.py b/tests/test_ray_aggregations.py index 845adb7..6c3e9de 100644 --- a/tests/test_ray_aggregations.py +++ b/tests/test_ray_aggregations.py @@ -1,5 +1,3 @@ -import re - import numpy as np import pytest import ray @@ -33,7 +31,7 @@ class TestTensorAggregatorInit: @pytest.mark.parametrize("AggClass", [TensorMean, TensorStd]) def test_invalid_axis_raises_value_error(self, AggClass): # noqa: N803 - with pytest.raises(ValueError, match=re.escape("Axis 0 .* must be included")): + with pytest.raises(ValueError, match=r"Axis 0 .* must be included"): AggClass(on="m", axis=(1, 2)) @pytest.mark.parametrize("AggClass", [TensorMean, TensorStd])