Skip to content

Respect filtered_counts_file when loading Visium HD cell matrices - #421

Open
psgundla wants to merge 1 commit into
scverse:mainfrom
psgundla:fix/visium-hd-raw-cell-counts
Open

psgundla wants to merge 1 commit into
scverse:mainfrom
psgundla:fix/visium-hd-raw-cell-counts

Conversation

@psgundla

Copy link
Copy Markdown

Respect filtered_counts_file when loading Visium HD cell matrices

This PR addresses #390 by making Visium HD cell-matrix loading respect filtered_counts_file.

Passing filtered_counts_file=False currently switches the binned matrix to raw counts, but segmented cells still come from filtered_feature_cell_matrix.h5. This changes the cell-matrix path to follow the same flag, including the check for whether the file exists. The default remains filtered.

The change adds the raw-cell filename to VisiumHDKeys and updates the reader docstring and CLI help. Nucleus counts still come from the filtered 2 µm matrix. The existing barcode-to-polygon matching is unchanged: rows without matching polygons are not included in the cell table.

Testing

I checked the original and patched readers against the public 10x Visium HD 3' Tiny Mouse Brain dataset, generated with Space Ranger 4.0.1.

Cell-matrix selection Expected cells from source Before patch After patch
Default (filtered) 612 612 612
Explicit filtered (True) 612 612 612
Raw (False) 951 612 951

The raw flag previously returned the filtered cell set. After the patch, barcode order and every count match the requested source matrix, while default and explicit filtered reads remain unchanged.

The additional 339 cells have zero counts, so total UMIs stay at 10,823. This checks that raw cell rows are retained; it does not show additional expression being recovered. Gene IDs, polygon coordinates, table-to-shape links and hires/lowres transforms also matched, and source-file checksums were unchanged. This is a downsampled developer dataset, not a biological accuracy test. Both versions emitted the same duplicate-gene-name and missing-full-resolution-image warnings.

The filename and barcode conventions are described in the 10x segmented-output specification. This is a file-selection fix, not a change to segmentation or count aggregation.

I kept the new regression tests in a separate file because #401 is reorganizing the test suite. The file may need moving to fit that layout before merge.

Reproduce the real-data comparison

Run from the repository root with its development environment active. The archive is about 297 MiB and is licensed CC BY 4.0 by 10x Genomics. These commands use a new temporary directory and leave the repository's test data unchanged.

export SDIO390_DATA="$(mktemp -d)"
curl -fL --retry 2 \
  https://cf.10xgenomics.com/samples/spatial-exp/4.0.1/Visium_HD_Tiny_3prime_Dataset/Visium_HD_Tiny_3prime_Dataset_outs.zip \
  -o "$SDIO390_DATA/sample.zip"
python - <<'PY'
import hashlib
import os
import zipfile
from pathlib import Path

root = Path(os.environ["SDIO390_DATA"])
with (root / "sample.zip").open("rb") as handle:
    assert hashlib.file_digest(handle, "md5").hexdigest() == "7cab710801d3776125a7aa5a792cd7e4"
with zipfile.ZipFile(root / "sample.zip") as archive:
    archive.extractall(root / "sample")
PY

Then compare the reader against the source matrices:

PYTHONPATH=src python - <<'PY'
import os
from pathlib import Path

import scanpy as sc
from spatialdata_io import visium_hd

root = Path(os.environ["SDIO390_DATA"]) / "sample"
for flag in (None, True, False):
    kind = "raw" if flag is False else "filtered"
    source = sc.read_10x_h5(root / "segmented_outputs" / f"{kind}_feature_cell_matrix.h5", gex_only=False)
    kwargs = {} if flag is None else {"filtered_counts_file": flag}
    result = visium_hd(root, dataset_id="mouse", load_segmentations_only=True, **kwargs)
    table = result.tables["cell_segmentations"]
    assert table.obs_names.tolist() == source.obs_names.tolist()
    assert table.var["gene_ids"].tolist() == source.var["gene_ids"].tolist()
    assert table.X.shape == source.X.shape
    assert (table.X != source.X).nnz == 0
    assert result.shapes["mouse_cell_segmentations"].index.tolist() == table.obs_names.tolist()
    print(f"filtered_counts_file={flag}: {table.n_obs} cells, {int(table.X.sum())} UMIs; exact source match")
PY

The patched reader passes all three selections: 612, 612 and 951 cells, each with 10,823 UMIs. On the original code, the raw-selection barcode assertion fails. All source barcodes have matching polygons in this particular sample; that is not guaranteed for every dataset.

These commands reproduce the cell/count comparison. The separate local validation also checked polygon coordinates, transforms and unchanged input checksums. No visual overlay review, real-data Zarr round-trip or multi-version validation was performed.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.70%. Comparing base (261578f) to head (0346f5f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #421      +/-   ##
==========================================
+ Coverage   63.53%   63.70%   +0.17%     
==========================================
  Files          26       26              
  Lines        3263     3265       +2     
==========================================
+ Hits         2073     2080       +7     
+ Misses       1190     1185       -5     
Files with missing lines Coverage Δ
src/spatialdata_io/__main__.py 85.90% <ø> (+1.36%) ⬆️
src/spatialdata_io/_constants/_constants.py 100.00% <100.00%> (ø)
src/spatialdata_io/readers/visium_hd.py 76.23% <100.00%> (+0.74%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants