Skip to content

Commit

Permalink
Added api for renaming coordinate systems (#392)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
LucaMarconato and pre-commit-ci[bot] authored Nov 2, 2023
1 parent 1cb88c6 commit 0446eff
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,21 @@ and this project adheres to [Semantic Versioning][].

## [0.0.15] - tbd

### Added

## [0.0.14] - 2023-10-11

### Added

#### Minor

- new API: sdata.rename_coordinate_systems()

#### Technical

- decompose affine transformation into simpler transformations
- remove padding for blobs()

#### Major

- get_extent() function to compute bounding box of the data
Expand Down
56 changes: 56 additions & 0 deletions src/spatialdata/_core/spatialdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,62 @@ def filter_by_coordinate_system(self, coordinate_system: str | list[str], filter

return SpatialData(**elements, table=table)

def rename_coordinate_systems(self, rename_dict: dict[str, str]) -> None:
"""
Rename coordinate systems.
Parameters
----------
rename_dict
A dictionary mapping old coordinate system names to new coordinate system names.
Notes
-----
The method does not allow to rename a coordinate system into an existing one, unless the existing one is also
renamed in the same call.
"""
from spatialdata.transformations.operations import get_transformation, set_transformation

# check that the rename_dict is valid
old_names = self.coordinate_systems
new_names = list(set(old_names).difference(set(rename_dict.keys())))
for old_cs, new_cs in rename_dict.items():
if old_cs not in old_names:
raise ValueError(f"Coordinate system {old_cs} does not exist.")
if new_cs in new_names:
raise ValueError(
"It is not allowed to rename a coordinate system if the new name already exists and "
"if it is not renamed in the same call."
)
new_names.append(new_cs)

# rename the coordinate systems
for element in self._gen_elements_values():
# get the transformations
transformations = get_transformation(element, get_all=True)
assert isinstance(transformations, dict)

# appends a random suffix to the coordinate system name to avoid collisions
suffixes_to_replace = set()
for old_cs, new_cs in rename_dict.items():
if old_cs in transformations:
random_suffix = hashlib.sha1(os.urandom(128)).hexdigest()[:8]
transformations[new_cs + random_suffix] = transformations.pop(old_cs)
suffixes_to_replace.add(new_cs + random_suffix)

# remove the random suffixes
new_transformations = {}
for cs_with_suffix in transformations:
if cs_with_suffix in suffixes_to_replace:
cs = cs_with_suffix[:-8]
new_transformations[cs] = transformations[cs_with_suffix]
suffixes_to_replace.remove(cs_with_suffix)
else:
new_transformations[cs_with_suffix] = transformations[cs_with_suffix]

# set the new transformations
set_transformation(element=element, transformation=new_transformations, set_all=True)

def transform_element_to_coordinate_system(
self, element: SpatialElement, target_coordinate_system: str
) -> SpatialElement:
Expand Down
53 changes: 53 additions & 0 deletions tests/core/operations/test_spatialdata_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,59 @@ def test_filter_by_coordinate_system_also_table(full_sdata):
assert len(filtered_sdata2.table) == len(full_sdata.table)


def test_rename_coordinate_systems(full_sdata):
# all the elements point to global, add new coordinate systems
set_transformation(
element=full_sdata.shapes["circles"], transformation=Identity(), to_coordinate_system="my_space0"
)
set_transformation(element=full_sdata.shapes["poly"], transformation=Identity(), to_coordinate_system="my_space1")
set_transformation(
element=full_sdata.shapes["multipoly"], transformation=Identity(), to_coordinate_system="my_space2"
)

elements_in_global_before = {
name for _, name, _ in full_sdata.filter_by_coordinate_system("global")._gen_elements()
}

# test a renaming without collisions
full_sdata.rename_coordinate_systems({"my_space0": "my_space00", "my_space1": "my_space11"})
assert {"my_space00", "my_space11", "global", "my_space2"}.issubset(full_sdata.coordinate_systems)
assert "my_space0" not in full_sdata.coordinate_systems
assert "my_space1" not in full_sdata.coordinate_systems

# renaming with collisions (my_space2 already exists)
with pytest.raises(ValueError):
full_sdata.rename_coordinate_systems({"my_space00": "my_space2"})

# renaming with collisions (my_space3 doesn't exist but it's target of two renamings)
with pytest.raises(ValueError):
full_sdata.rename_coordinate_systems({"my_space00": "my_space3", "my_space11": "my_space3"})

# invalid renaming: my_space3 is not a valid coordinate system
with pytest.raises(ValueError):
full_sdata.rename_coordinate_systems({"my_space3": "my_space4"})

# invalid renaming: my_space3 is not a valid coordinate system (it doesn't matter if my_space3 is target of one
# renaming, as it doesn't exist at the time of the function call)
with pytest.raises(ValueError):
full_sdata.rename_coordinate_systems(
{"my_space00": "my_space3", "my_space11": "my_space3", "my_space3": "my_space4"}
)

# valid renaming with collisions
full_sdata.rename_coordinate_systems({"my_space00": "my_space2", "my_space2": "my_space3"})
assert get_transformation(full_sdata.shapes["circles"], get_all=True)["my_space2"] == Identity()
assert get_transformation(full_sdata.shapes["multipoly"], get_all=True)["my_space3"] == Identity()

# renaming without effect
full_sdata.rename_coordinate_systems({"my_space11": "my_space11"})
assert get_transformation(full_sdata.shapes["poly"], get_all=True)["my_space11"] == Identity()

# check that all the elements with coordinate system global are still there
elements_in_global_after = {name for _, name, _ in full_sdata.filter_by_coordinate_system("global")._gen_elements()}
assert elements_in_global_before == elements_in_global_after


def test_concatenate_tables():
"""
The concatenation uses AnnData.concatenate(), here we test the
Expand Down

0 comments on commit 0446eff

Please sign in to comment.