Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added api for renaming coordinate systems #392

Merged
merged 6 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ci:
skip: []
repos:
- repo: https://github.com/psf/black
rev: 23.10.0
rev: 23.10.1
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
Expand All @@ -27,7 +27,7 @@ repos:
additional_dependencies: [numpy, types-requests]
exclude: tests/|docs/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.1
rev: v0.1.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
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
4 changes: 2 additions & 2 deletions tests/core/test_data_extent.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ def test_get_extent_points():
extent = get_extent(sdata["blobs_points"])
check_test_results0(
extent,
min_coordinates=np.array([12.0, 13.0]),
max_coordinates=np.array([500.0, 498.0]),
min_coordinates=np.array([3.0, 4.0]),
max_coordinates=np.array([509.0, 507.0]),
axes=("x", "y"),
)

Expand Down
Loading