Skip to content

contains_element returns False if element is contained more than once #425

Closed

Description

@aeisenbarth

SpatialData allows an element to be contained more than once with different names. When checking whether it is contained, the result is unexpectedly False although the element is contained.

Example

from spatialdata import SpatialData
from spatialdata.models import Image2DModel
import numpy as np
img = Image2DModel.parse(np.arange(4).reshape((1, 2, 2)), dims=("c", "y", "x"))
sdata = SpatialData(images={"img1": img, "img2": img})
assert sdata.contains_element(img)

Cause

_locate_spatial_element raises a ValueError if the element is not found and another ValueError if the element is found more than once. contains_element catches both and returns False, but it should catch the second case separately and return True.

It's maybe worth to consider whether to disallow the same element under different keys (how would that be represented on disk?), because it is a very special edge case that breaks many other code places of spatialdata (e.g. _write_transformations_to_disk) and would need to be completely covered by test cases (if it has already any).

Instead of having contains_element inspect the AssertionError string from locate_spatial_element to distinguish the two ValueErrors, we could either have ValueError subclasses, or a private helper method that returns the counts of found elements (0, 1, 2…) instead of raising exceptions.

Activity

  1. LucaMarconato commented on Jan 9, 2024

    @LucaMarconato
    Member

    @aeisenbarth thanks for reporting!

    @giovp @kevinyamauchi @melonora I think we should prevent the same object to be added twice, what do you think? There were discussions on allowing the same object in-memory to be present in multiple locations of a SpatialData object and to change the io functions to save it only once. This would have helped with an earlier design of the multiple tables. But with the current design I can't really see a benefit and probably we should just drop this complexity, at least for now.

    WDYT?

  2. melonora commented on Jan 9, 2024

    @melonora
    Collaborator

    I agree, I don't see a benefit of it either, just added complexity.

  3. giovp commented on Jan 16, 2024

    @giovp
    Member

    mmh I really don't know though, I think this should be checked before and not with the check in #427 . Either a SpatialData can be instantiated with copies of elements or it can't. AnnData allows it for instance, so not sure why we shouldn't. I think on the contrary there is additional complexity on checking for the same element being present, as it should be done for sure at instantiation/IO and maybe in other parts (e.g. in subset ?).

  4. LucaMarconato commented on Jan 16, 2024

    @LucaMarconato
    Member

    I agree, it should be checked before, I'd say at instantiation (constructor andinit_from_elements) and when the user uses the syntaxes sdata.images['xxx'] = xxx. I'd still keep it also in locate_element() as a double check for safety, to make sure that the result is not ambiguous.

  5. melonora commented on Jan 17, 2024

    @melonora
    Collaborator

    @giovp What is the reason in AnnData for allowing it? Coming back to think about this, I would not see a use case personally right away. On the other hand what would be the problem (other than what @aeisenbarth reported currently) if the name of the element is different?

  6. giovp commented on Jan 17, 2024

    @giovp
    Member

    I don't think checking that the element value is unique is something we should enforce, I don't see a use case but also I think it creates quite some constraints. Why not just returning a list of str in locate_element in #427 , what's the issue?

  7. melonora commented on Jan 17, 2024

    @melonora
    Collaborator

    yes, I agree. It is what I tried to point at as the only problem currently is that False gets returned. If a user has added one element twice with different names it is an intention of the user. We can just return the corresponding keys.

  8. LucaMarconato commented on Jan 17, 2024

    @LucaMarconato
    Member

    Ok, I have changed locate_element() and to return an optional list of strings.

  9. aeisenbarth commented on Jan 18, 2024

    @aeisenbarth
    ContributorAuthor

    Semantically, an empty list of strings equally expresses "not found", just like None. But it is a simpler type, and one needs less code to work with the return value.

    Using current implementation:

    locations: Union[list[str], None] = locate_element(...)
    # Use case 1
    if locations is not None:
        first_location = locations[0] # assuming list is never returned empty
    # Use case 2
    if locations is not None:
        for location in locations:
            ...

    Using empty list:

    locations: list[str] = locate_element(...)
    # Use case 1
    if locations:
        first_location = locations[0] # assuming list is never returned empty
    # Use case 2
    for location in locations:
        ...
  10. LucaMarconato commented on Jan 18, 2024

    @LucaMarconato
    Member

    Very true, I was still focused on the older approach, gonna make a quick patch now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions