Skip to content

feat: Updates for 10x Genomics Atera bundles - #426

Open
stephenwilliams22 wants to merge 21 commits into
scverse:mainfrom
10XGenomics:atera-reader
Open

stephenwilliams22 wants to merge 21 commits into
scverse:mainfrom
10XGenomics:atera-reader

Conversation

@stephenwilliams22

@stephenwilliams22 stephenwilliams22 commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a native reader for 10x Genomics Atera datasets: cell-by-gene expression table, cell/nucleus segmentation labels and boundary polygons, per-transcript locations, and morphology images.

  • Table (cell_feature_matrix.zarr.zip/csc_cell_feature_matrix.zarr.zip) is read directly with anndata's own zarr IO (anndata.io.read_elem/ anndata.io.sparse_dataset), with X lazily backed by dask.
  • Cell/nucleus labels are lazily backed by dask; boundary polygons are read via shapely.from_ragged_array, with an optional tiled/pyramided partial-read path (read_cell_boundaries) for large bundles.
  • Transcripts are read lazily per spatial tile via dask-delayed.
  • Morphology images are read via tifffile's native OME-TIFF tile grid, avoiding materializing full-resolution planes.
  • An example notebook with a finalized tiny atera output bundle is provided for testing. This can be found in the atera_example.ipynb for testing.

Enables massive datasets on a basic laptop (2M cells, 11 billion transcripts). Basic analysis workflow (loading, plotting, HVG, PCA, normalization, clustering, etc) max memory usage was 7GB on a new mac in ~15min.

Adds a native reader for 10x Genomics Atera datasets: cell-by-gene
expression table, cell/nucleus segmentation labels and boundary
polygons, per-transcript locations, and morphology images.

- Table (`cell_feature_matrix.zarr.zip`/`csc_cell_feature_matrix.zarr.zip`)
  is read directly with `anndata`'s own zarr IO (`anndata.io.read_elem`/
  `anndata.io.sparse_dataset`), with `X` lazily backed by dask.
- Cell/nucleus labels are lazily backed by dask; boundary polygons are
  read via `shapely.from_ragged_array`, with an optional tiled/pyramided
  partial-read path (`read_cell_boundaries`) for large bundles.
- Transcripts are read lazily per spatial tile via dask-delayed.
- Morphology images are read via `tifffile`'s native OME-TIFF tile grid,
  avoiding materializing full-resolution planes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@codecov-commenter

codecov-commenter commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.54296% with 218 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.00%. Comparing base (9548686) to head (488acf7).

Files with missing lines Patch % Lines
src/spatialdata_io/readers/_atera_common.py 45.92% 166 Missing ⚠️
src/spatialdata_io/readers/atera.py 71.11% 52 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #426      +/-   ##
==========================================
- Coverage   66.60%   66.00%   -0.61%     
==========================================
  Files          26       29       +3     
  Lines        3345     3927     +582     
==========================================
+ Hits         2228     2592     +364     
- Misses       1117     1335     +218     
Files with missing lines Coverage Δ
src/spatialdata_io/__init__.py 100.00% <ø> (ø)
src/spatialdata_io/_constants/_constants.py 100.00% <100.00%> (ø)
src/spatialdata_io/converters/atera_to_parquet.py 100.00% <100.00%> (ø)
src/spatialdata_io/readers/atera.py 71.11% <71.11%> (ø)
src/spatialdata_io/readers/_atera_common.py 45.92% <45.92%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Zethson

Zethson commented Oct 2, 2026

Copy link
Copy Markdown
Member

@stephenwilliams22 could you please always ensure that the pre-commit checks are green? Ideally a PR should always have green CI before anyone has a look.

Thanks!

stephenwilliams22 and others added 2 commits October 2, 2026 12:01
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@stephenwilliams22

Copy link
Copy Markdown
Collaborator Author

@stephenwilliams22 could you please always ensure that the pre-commit checks are green? Ideally a PR should always have green CI before anyone has a look.

Thanks!

@Zethson sorry about that. we should be good to go now.

@timtreis timtreis self-assigned this Oct 22, 2026
@xinaesthete

Copy link
Copy Markdown

@stephenwilliams22 It seems you write attrs such that the table annotates cell_labels, but the raster values there don't correspond to the cell_ids in the table. You could probably associate with cell_boundaries shapes instead, or have another obs['label-ids'] and use that.

I'm going from what Claude has said more than actually having read through the code in detail.

Ideally you'd be able to record an association between more than one somewhat equivalent spatial element (so a given cell could be associated with nucleus & boundary shapes/labels), I've been meaning to find if there's an open issue or discussion on that.

@xinaesthete

Copy link
Copy Markdown

Also sdata.write() fails on the reader’s output, at the table:

ValueError: DataFrame.index.name ('barcode') is also used by a column whose values are different.

var fails the same way with feature_id. The values aren’t actually different: every barcode and feature ID matches its column. Under pandas 3 the index is the string dtype (str) while the column is object, and anndata checks the pair with pd.Series.equals, which also compares dtypes:

s = pd.Series(obs.index, index=obs.index)
s.equals(obs["barcode"])                          # False
s.equals(obs["barcode"].astype(obs.index.dtype))  # True

Building the index and the column from the same array (so their dtypes match), or not naming the index after an existing column, fixes it. Seen with the Atera v1 WTA kidney dataset; spatialdata 0.8.0, anndata 0.13.4, pandas 3.1.0.

@stephenwilliams22

Copy link
Copy Markdown
Collaborator Author

@stephenwilliams22 It seems you write attrs such that the table annotates cell_labels, but the raster values there don't correspond to the cell_ids in the table. You could probably associate with cell_boundaries shapes instead, or have another obs['label-ids'] and use that.

I'm going from what Claude has said more than actually having read through the code in detail.

Ideally you'd be able to record an association between more than one somewhat equivalent spatial element (so a given cell could be associated with nucleus & boundary shapes/labels), I've been meaning to find if there's an open issue or discussion on that.

Thanks @xinaesthete I put in a fix

@stephenwilliams22

Copy link
Copy Markdown
Collaborator Author

Also sdata.write() fails on the reader’s output, at the table:

ValueError: DataFrame.index.name ('barcode') is also used by a column whose values are different.

var fails the same way with feature_id. The values aren’t actually different: every barcode and feature ID matches its column. Under pandas 3 the index is the string dtype (str) while the column is object, and anndata checks the pair with pd.Series.equals, which also compares dtypes:

s = pd.Series(obs.index, index=obs.index)
s.equals(obs["barcode"])                          # False
s.equals(obs["barcode"].astype(obs.index.dtype))  # True

Building the index and the column from the same array (so their dtypes match), or not naming the index after an existing column, fixes it. Seen with the Atera v1 WTA kidney dataset; spatialdata 0.8.0, anndata 0.13.4, pandas 3.1.0.

Fixed

A tile can hold 100M+ transcripts. Each dask partition was a whole tile, so
writing a large dataset ran many multi-GB partitions at once and swapped to
disk. atera_to_parquet likewise decoded a whole tile per batch.

- _read_transcript_tile takes start/stop and reads only those rows; the
  per-row gene index comes from searchsorted over gene_offset.
- _get_points makes one partition per 5M-row chunk of a tile.
- atera_to_parquet reads and writes each tile in the same chunks.
- Add tests that chunked reads match whole-tile reads across gene
  boundaries, and that partitions are bounded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Removed code for calculating the number of genes detected per cell as this is now in the atera loader
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.

5 participants