Skip to content

Transfo manager base#1164

Draft
ajkswamy wants to merge 17 commits into
transformation_managerfrom
transfo_manager_base
Draft

Transfo manager base#1164
ajkswamy wants to merge 17 commits into
transformation_managerfrom
transfo_manager_base

Conversation

@ajkswamy

@ajkswamy ajkswamy commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Issue Description

Currently, each spatial element stores its transformations as a dictionary in its attrs (key: TRANSFORM_KEY = "
transform"). This means transformation information is distributed across elements, and there is no mechanism to define
or query transformations between coordinate systems. The proposed change introduces a centralized TransformationManager
object at the
root of SpatialData that owns all coordinate system and transformation metadata, replacing the current per-element
attrs["transform"] dictionaries.

Motivation

  • Missing inter-CS transforms: With per-element storage, defining a transformation from coordinate system A to B
    requires
    touching both element dictionaries independently. A central registry avoids this duplication and inconsistency.

  • Single source of truth: A unified TransformationManager makes it straightforward to inspect the full transformation
    graph, validate
    connectivity/acyclicity, and add new features on top of a stable graph API.

Current Architecture

Currently, transformation information is stored inside each spatial element's attrs dictionary under the key
TRANSFORM_KEY = "transform". Access and mutation happen through _get_transformations() and _set_transformations()
utility functions. Each element independently maps its native pixel/point space to one or more coordinate systems, but
there is no global view of the transformation graph and no way to define transformations between coordinate systems
without touching each element dictionary.

Proposed Solution

Add a TransformationManager attribute to SpatialData.

TransformationManager stores:

  • nx.MultiDiGraph with NgffCoordinateSystem objects as nodes and pass transforms as attributes while adding edges

  • _element_to_cs_mapping: mapping of elementNgffCoordinateSystem object

Scope

Out-of-Scope

File format changes or support of new file formats

TransformationManager serialization to file formats

Advanced transformation graph operations

Exposing new class into Public API

Technical Constraints

Compatibility: Must support all existing spatial element types (images, labels, points, shapes, tables)

@Tomaz-Vieira Tomaz-Vieira left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a bunch of mostly typing-related comments, as you might imagine 😆

Also, this is something we might need to decide as a team, but it's a bit funny that often the docstring is way longer than the code itself, and often there isn't much more to say, like:

souce_cs
    The name of the source coordinate system

I often feel like the typing signature is more concise and more informative than the docstrings, and that sometimes we could survive just with the headline and the "raises" part.

But anyway, good stuff, it's nice to see we're moving already =)

Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
associated_transformations.append(transformation_key)
return associated_transformations

def _get_elements_associated_with_cs(self, cs_name: str) -> list[tuple[ELEMENT_TYPE, str]]:

@Tomaz-Vieira Tomaz-Vieira Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need a word that is better than "associated" to mean that the element belongs or exists in the coordinate system. It's a stronger relation than the transforms that are "associated" with the coordinate systems, because an element can only belong so a single CS, but a CS can have many transforms.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I like belongs for instance, but no hard opinions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about "element set in a coordinate system"? the API would be like

class TransformationManager:

  • __ init__
  • .......
  • ........
  • set_element_in_coordinate_system(....)
  • unset_element_from_coordinate_system(....)
  • get _element_coordinate_system (....) "Get the coordinate system in which an element is set"
  • get_elements_set_in_cs(....)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I have tried to stick to "transformations associated with a cs" and "elements belonging to a cs". Only exception is "unset_element" for which I don't have a better idea. Any ideas?

Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.73%. Comparing base (eb4fb3d) to head (ab98633).

Additional details and impacted files
@@                    Coverage Diff                     @@
##           transformation_manager    #1164      +/-   ##
==========================================================
+ Coverage                   92.44%   92.73%   +0.29%     
==========================================================
  Files                          51       53       +2     
  Lines                        7820     8028     +208     
==========================================================
+ Hits                         7229     7445     +216     
+ Misses                        591      583       -8     
Files with missing lines Coverage Δ
src/spatialdata/__init__.py 95.65% <ø> (ø)
src/spatialdata/_core/spatialdata.py 93.87% <100.00%> (+0.01%) ⬆️
...atialdata/_core/transformation_manager/__init__.py 100.00% <100.00%> (ø)
...ialdata/_core/transformation_manager/exceptions.py 100.00% <100.00%> (ø)
src/spatialdata/_io/io_raster.py 89.52% <100.00%> (+0.47%) ⬆️
src/spatialdata/_io/io_zarr.py 92.38% <100.00%> (ø)
src/spatialdata/_types.py 100.00% <100.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@LucaMarconato

LucaMarconato commented Jul 16, 2026

Copy link
Copy Markdown
Member

(to be moved to a separate issue)
We can probably restore networkx as a global import, as you can see from running profimp, dask.dataframe is what is driving the slow import time.

image

Importing networkx is actually 273 ms.

Comment thread src/spatialdata/_core/transformation_manager/__init__.py Outdated
Comment on lines +205 to +211
def get_existing_transformation(self, input_cs: str, output_cs: str) -> BaseTransformation | None:
"""
Retrieve a transformation defined between coordinate systems.

Parameters
----------
input_cs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: check if the specs allows multi edges between the coordinate systems. There was some discussion regarding this at the Heidelberg hackathon and I don't remmber the outcome.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything in the specification that forbids multiple edges between coordinate systems. Also, transformnd uses nx.MultiDiGraph and does not check if an edge already exists between two nodes when adding edges to graph.

key = (input_cs, output_cs)
return self._coordinate_transforms.get(key)

def remove_transformation(self, input_cs: str, output_cs: str) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we have a multigraph, we need to change the signature to let the user remove a specific transformation.

g.add_edge(input_cs, output_cs, transformation=transformation)
return g

def get_shortest_transformation_sequence(self, source_cs: str, target_cs: str) -> list[BaseTransformation]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will sometimes have multiple shortest paths between 2 coordinate systems (this may happen even if we don't have a multigraph). What we do in spatialdata is shown here. It's not the cleanest approach, but it could be used also here to get the job done:

intermediate_coordinate_systems: SpatialElement | str | None = None,
. Note one can still construct edge cases that fail, the clean approach may be to pass the full list of cs as a path.

transformations.append(g[path[i]][path[i + 1]]["transformation"])
return transformations

def get_all_transformation_sequences(self, source_cs: str, target_cs: str) -> list[list[BaseTransformation]]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could return directly a list[spatialdata.transformation.Sequence]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideas:

  • @ajkswamy suggested to let the user choose the return type list of list vs list of Sequence. This has the con of complicating typing.

Comments:

  • Pro: of returning a list of list: the user can easily extract sub transformations
  • Con: if you have a Sequence you can directly use it to get an affine, to transform things. At the cost that you have to call my_sequence.transformations to get the subcomponents (but it's not that bad)

all_sequences.append(sequence)
return all_sequences

def __repr__(self) -> str:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For later: it would be very nice to let the user be able to plot this. An option could be a hard coded HTML rendering with no additional dependencies, but this comes with extra code to maintain.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user already import matplotlib (via spatialdata-plot), we could plot via networkx + matplotlib.

Comment thread src/spatialdata/__init__.py Outdated
Comment thread src/spatialdata/__init__.py Outdated
Comment thread src/spatialdata/__init__.py
@ajkswamy ajkswamy self-assigned this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants