scverse / scverse/spatialdata

Scale/Translation/Affine.__eq__ raise ValueError for a different number of axes (breaks aggregate on Xenium transcripts vs 2D cells)

Open
#1,205 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug 🚨 method: aggregation 🔢 method: transforms 🔃 needs: triage priority: high
Dominant language
Python
Stars
394
Forks
95
Avg merge
4d 3h
Merged PRs (30d)
7

Description

[!NOTE]
This whole message is AI-generated. The issue was automatically discovered and reported by an AI agent (Claude) during an autonomous bug hunt on the spatialdata code base. It has not been verified or triaged by a human yet; the needs: triage label is set so that a maintainer can confirm it. The reproduction script below was executed by the agent in an isolated environment (see Environment) and its output is pasted verbatim.

Summary

Scale([2, 2, 1], axes=("x", "y", "z")) == Scale([2, 2], axes=("x", "y")) raises ValueError: operands could not be broadcast together with shapes (3,) (2,) instead of returning False; same for Translation and Affine. aggregate() compares the two transformations with == (by_transform == values_transform), so on the Xenium dataset from spatialdata-sandbox (xenium_rep1_io), where transcripts carry Scale (x, y, z) and cell boundaries Scale (x, y), the count-matrix aggregation crashes. spatialdata.testing.assert_elements_are_identical hits the same error after a write/read round trip of a raster whose Scale was defined on a subset of the axes.

Severity (agent's assessment): high — the canonical Xenium workflow aggregate(transcripts, by=cell_boundaries, value_key="feature_name", agg_func="count") crashes on real data

Where: src/spatialdata/transformations/transformations.pyTranslation.__eq__, Scale.__eq__, Affine.__eq__ evaluate np.allclose(...) before comparing the axes

Expected behaviour

== returns False for transformations with different axes.

Reproduction

Save as repro.py and run uv run repro.py (the PEP 723 header pins spatialdata to the commit the bug was found on; replace the URL fragment with @main to test the current main branch).

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "spatialdata @ git+https://github.com/scverse/spatialdata.git@ccf1ea048d054b6624214bf618008a9f9ae223e0",
# ]
# ///
"""Scale/Translation/Affine.__eq__ raise ValueError instead of returning False for a different number of axes."""
import warnings
import numpy as np
from spatialdata.transformations import Affine, Identity, Scale, Translation

warnings.simplefilter("ignore")
pairs = [
    ("Scale (x,y,z) vs Scale (x,y)", Scale([2, 2, 1], axes=("x", "y", "z")), Scale([2, 2], axes=("x", "y"))),
    ("Translation (x,y,z) vs (x,y)", Translation([1, 2, 3], axes=("x", "y", "z")), Translation([1, 2], axes=("x", "y"))),
    ("Affine 3D vs 2D", Affine(np.eye(4), input_axes=("x", "y", "z"), output_axes=("x", "y", "z")), Affine(np.eye(3), input_axes=("x", "y"), output_axes=("x", "y"))),
    ("Scale vs Identity (control)", Scale([2, 2], axes=("x", "y")), Identity()),
]
bug = False
for name, a, b in pairs:
    try:
        print(f"{name:32s}: a == b -> {a == b}")
    except Exception as e:  # noqa: BLE001
        print(f"{name:32s}: {type(e).__name__}: {e}")
        bug = True
print("VERDICT:", "BUG REPRODUCED" if bug else "NOT REPRODUCED")
# Real-data consequence: Xenium transcripts carry Scale(x, y, z) while cell boundaries carry Scale(x, y), so
# aggregate(transcripts, by=cell_boundaries, value_key="feature_name", agg_func="count") crashes in the
# `by_transform == values_transform` comparison inside aggregate().
Observed output
Scale (x,y,z) vs Scale (x,y)    : ValueError: operands could not be broadcast together with shapes (3,) (2,)
Translation (x,y,z) vs (x,y)    : ValueError: operands could not be broadcast together with shapes (3,) (2,)
Affine 3D vs 2D                 : ValueError: operands could not be broadcast together with shapes (4,4) (3,3)
Scale vs Identity (control)     : a == b -> False
VERDICT: BUG REPRODUCED

Possible fix direction (unverified)

Compare axes (and matrix shapes) first and short-circuit, e.g. return isinstance(other, Scale) and self.axes == other.axes and np.allclose(self.scale, other.scale); same for Translation and Affine (input/output axes and matrix.shape). Add tests comparing 2D vs 3D transformations.

Environment

uv run repro.py with the PEP 723 metadata in the script (fresh, isolated environment; spatialdata built from main @ ccf1ea0 (2026-08-28); Python 3.13, latest releases of the dependencies at run time: pandas 3.0, anndata 0.13, zarr 3.3, dask 2026.8, numpy 2.5, geopandas 1.1, shapely 2.1). macOS (arm64). Also reproduced in a second environment with pandas 2.3.3 / anndata 0.12.11 / numpy 2.4.4 / zarr 3.2.1.

Possibly related issues

#1029


Automatically generated; discovered by an AI agent (Claude) and not yet reviewed by a human.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/spatialdata/transformations/transformations.py at Translation.eq, Scale.eq, and Affine.eq, then run the supplied repro.py to confirm the shape-mismatch failures. Compare the existing equality behavior with the stated expected results, add coverage for 2D-versus-3D transformations, and verify that the Xenium aggregation comparison no longer raises ValueError.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python
Domain
data
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
75/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.