Skip to content

Fix VCSCAnnData.copy() silently dropping X/raw_X - #48

Merged
aarmey merged 3 commits into
mainfrom
fix-vcsc-anndata-copy
Sep 13, 2026
Merged

Fix VCSCAnnData.copy() silently dropping X/raw_X#48
aarmey merged 3 commits into
mainfrom
fix-vcsc-anndata-copy

Conversation

@aarmey

@aarmey aarmey commented Sep 13, 2026

Copy link
Copy Markdown
Member

Summary

VCSCAnnData stores X/raw_X in private _vcs_X/_vcs_raw_X attributes,
since anndata's own X validation rejects a VCSCArray/VCSRArray directly
(see the class docstring). It never overrides .copy(), though, so the
inherited anndata.AnnData.copy() copies the standard (unused, always-None
for this class) _X attribute instead:

>>> ad = VCSCAnnData(X=arr)
>>> ad.X is None
False
>>> c = ad.copy()
>>> type(c)
<class 'anndata.AnnData'>
>>> c.X is None
True

X silently comes back None, and the returned object is downgraded to a
plain AnnData rather than preserving VCSCAnnData (or a subclass, such as
one overriding the X property for a lazy-normalized view -- as BAL-Pf2's
own import_data() does). Any caller relying on adata[mask].copy() hits
this immediately -- in particular parafac2's BiCV rank-selection routine
does exactly this for its train/test splits, which is how this surfaced.

Adds an explicit copy() override that copies every field (mirroring
__getitem__'s existing field-by-field approach), including a real
VCSCArray/VCSRArray copy of X/raw_X, and returns type(self).

Test plan

  • uv run pytest (1266 passed, 53 skipped, 1 pre-existing unrelated
    failure -- test_property_normalization.py::test_recipe_matches_reference[scanpy-VCSRArray],
    a numeric edge case on an all-constant column that reproduces
    identically on main without this change) -- includes new tests in
    tests/test_anndata_class.py covering: X/raw_X preserved and
    independent after .copy(), obs/var/uns preserved, a subclass
    overriding the X property round-tripping through .copy(), and the
    exact adata[mask][:, mask].copy() pattern parafac2's BiCV uses.
  • uv run ruff check . / uv run ruff format --check . / uv run codespell
  • uv run ty check

aarmey and others added 3 commits September 12, 2026 19:50
VCSCAnnData stores X/raw_X in private _vcs_X/_vcs_raw_X attributes
(anndata's own X validation rejects a VCSCArray/VCSRArray directly),
but never overrode copy(), so the inherited anndata.AnnData.copy()
copies the standard (unused, always-None) _X attribute instead: X
silently comes back None, and the returned object is downgraded to a
plain AnnData rather than preserving this class (or a subclass, such
as one overriding the X property for a lazy-normalized view).

Any caller relying on `adata[mask].copy()` -- e.g. parafac2's BiCV
train/test splitting -- hits this immediately. Add an explicit
override that copies every field, including a real VCSCArray/VCSRArray
copy of X/raw_X, and returns type(self).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The inherited anndata.AnnData.to_memory() has the same root cause as
copy() (previous commit): it only knows about the standard, unused _X
attribute, not this class's _vcs_X/_vcs_raw_X, and reconstructs a plain
AnnData that silently loses X. This class never actually supports a
lazily backed X/raw_X, so to_memory() now just delegates to copy().

scrise's BiCV rank selection calls to_memory() on its input before the
train/test split loop, so this was hit immediately after fixing copy().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
__getitem__ already returned a new, eagerly-copied object (rather than
a lazy view) since anndata's own view machinery can't handle _vcs_X,
but it hardcoded the returned type to VCSCAnnData instead of
type(self) -- so a subclass overriding X (e.g. one that always hands
back a value normalized fresh from _vcs_X, as with copy()/to_memory()
in the previous two commits) silently reverted to the raw,
un-normalized array after any slice.

scrise's BiCV rank selection slices its input for every train/test
split, so this surfaced immediately after fixing copy()/to_memory():
the sliced object's X was the raw VCSRArray, which doesn't implement
the norm_sq() a normalized view needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aarmey
aarmey merged commit 02400bf into main Sep 13, 2026
5 of 6 checks passed
@aarmey
aarmey deleted the fix-vcsc-anndata-copy branch September 13, 2026 20:29
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.

1 participant