Skip to content

Fix: Critical updates for Seurat v5, spacexr 2.0, and robust spot alignment - #266

Open
Atakannl wants to merge 11 commits into
BIMSBbioinfo:mainfrom
Atakannl:main
Open

Fix: Critical updates for Seurat v5, spacexr 2.0, and robust spot alignment#266
Atakannl wants to merge 11 commits into
BIMSBbioinfo:mainfrom
Atakannl:main

Conversation

@Atakannl

Copy link
Copy Markdown
Contributor

Hi VoltRon team,

While utilizing getDeconvolution() for a GBM spatial analysis, I encountered breaking compatibility issues with Seurat v5 (layer handling) and spacexr 2.0+ (Reference object requirements).

This PR implements the following fixes to modernize the vrDeconvolution module while maintaining strict backward compatibility:

Seurat v5 Support: Added a check for JoinLayers to correctly handle count retrieval (layer="counts" vs slot="counts"), fixing the "slot 'counts' not found" error.

spacexr 2.0 Compliance:

  • Implemented the native spacexr::Reference constructor for v2.0+ users.

  • Updated getRCTD to use the SpatialRNA constructor with data.frame coordinates (required by v2.0).

  • Preserved the legacy SpatialExperiment path for users on older versions.

Robust Spot Alignment: Added logic to align RCTD results back to the original VoltRon spot IDs. This fixes issues where RCTD drops spots during QC or sanitizes barcodes (e.g., changing - to .), which previously resulted in dimension mismatches or silent data loss.

Safety Validations:

  • Added explicit checks for sc.cluster existence in metadata to prevent cryptic errors.

  • Added hard failure stops if spot alignment finds zero matches.

I have verified these fixes on a merged GBM cohort using both Seurat v5 and spacexr v2.2.1.

Thanks for the great tool!

@Artur-man

Artur-man commented Jan 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks so much for the PR @Atakannl, I will review this one with my use cases and get back to you! Artür

@Artur-man

Artur-man commented Feb 21, 2026

Copy link
Copy Markdown
Collaborator

Dear @Atakannl, thanks so much for the PR. Can you take a look at my review and correct if can.

In summary, i do not believe we should implicitly join layers of a Seurat object. I would rather mention this in the documentation/tutorial, you can attempt this for the doc/rmd of the website which is here in the package too.

I would explain that people who use v5 should join layers first (I find the need to do JoinLayers in Seurat pretty annoying btw)

Thus, we first deconvolute spots into cells using a single cell reference in order to later use the resultsing cell type abundances per each spot for clustering.
We will use the public single nucleus RNA-seq (snRNA-seq) data from available [here](https://libd-snrnaseq-pilot.s3.us-east-2.amazonaws.com/SCE_DLPFC-n3_tran-etal.rda). See [(Tran, Maynard, Spangler et al., 2021)](https://linkinghub.elsevier.com/retrieve/pii/S0896-6273(21)00655-3) for more information.
We will use this reference for deconvoluting each Visium across
each for Visium assay in the VoltRon object. See [Niche Clustering](nicheclustering.html)
tutorial for more information on spot deconvolution.
```{r class.source="watch-out", eval = FALSE}
# single cell reference
load("SCE_DLPFC-n3_tran-etal.rda")
# prepare reference
tab <- table(sce.dlpfc.tran$cellType)
sce.dlpfc.tran <- sce.dlpfc.tran[,sce.dlpfc.tran$cellType %in% names(tab)[tab > 25]]
sce.dlpfc.tran.seu <- Seurat::as.Seurat(sce.dlpfc.tran)
# Spot deconvolution
library(spacexr)
SRBlock <- readRDS("Visium&Visium_data_registered.rds")
SRBlock <- getDeconvolution(SRBlock, sc.object = sce.dlpfc.tran,
sc.cluster = "cellType", max_cores = 2)
SRBlock
```

- Removed the implicit layer joining
- Added validation for multi-layer assays
- Require user to run JoinLayers before deconvolution
@Atakannl

Atakannl commented Feb 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the suggestion @Artur-man . I removed the implicit JoinLayers call and now explicitly check for multi-layer assays in Seurat v5. If multiple layers are detected, the user is instructed to run JoinLayers beforehand. No automatic layer modification is performed anymore. I will also add a note to the documentation for Seurat v5 users.

@Atakannl

Copy link
Copy Markdown
Contributor Author

I added the Seurat v5 compatibility note to both RCTD sections in the niche clustering vignette. The documentation now explicitly states that users must run SeuratObject::JoinLayers() before calling getDeconvolution() when multiple assay layers are present. @Artur-man

@Artur-man

Copy link
Copy Markdown
Collaborator

@Atakannl thanks so much. I have left some comments I think this PR would make VoltRon comply with Seurat v5 and recent version of spacexr. We should only make the PR a bit cleaner and reduce unnecessary if statements, otherwise I like it.

If you like I can resolve the conflicts for you, then you can continue updating.

@Atakannl

Atakannl commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

That would be great, @Artur-man, thank you. I will continue refining the conditional logic once the conflicts are resolved.

@Artur-man

Copy link
Copy Markdown
Collaborator

@Atakannl ready for you now. Let me know if you have any questions.

@Artur-man

Copy link
Copy Markdown
Collaborator

@Atakannl just checking in, any updates on this ? would be nice to wrap this PR soon.

@Atakannl

Atakannl commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for following up and for pushing the updates. I appreciate it. Apologies for the delay on my side. I’ll review the latest changes and continue cleaning up the conditional logic and structure so we can wrap this PR soon.

@Artur-man

Artur-man commented May 5, 2026

Copy link
Copy Markdown
Collaborator

No worries @Atakannl. Also for clarity let me clarify some stuff that i discussed with folks at Bioconductor.

There are two implementations of spacexr:
https://github.com/dmcable/spacexr
https://github.com/ggrajeda/spacexr

We have to use ggrajeda/spacexr because RCTD in dmcable/spacexr is deprecated (this is a bit strange and not your fault, it is a bad coding practice).

So actually spacexr >=2.0.0 is not the latest version, but the legacy implementation itself (Bioconductor uses a different versioning system so it looks like version in Bioc is previous)

For the Seurat version I limited the versioning of Seurat to >=5.0.0.

@Artur-man

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 939753e48b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread R/deconvolution.R
Comment on lines +371 to +375
if (length(layer_names) > 1) {
stop(
"Seurat v5 layered assay detected with multiple layers. ",
"Please run SeuratObject::JoinLayers(sc.object, assay = '",
sc.assay,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove hard stop on multi-layer Seurat assays

The length(layer_names) > 1 guard in getDeconReferenceSpot() rejects valid Seurat v5 objects that contain multiple layers (e.g., counts, data, scale.data), even though this function immediately requests layer = "counts" via GetAssayData and that usage is supported. This turns many normal Seurat workflows into hard failures and prevents deconvolution from running on otherwise compatible references.

Useful? React with 👍 / 👎.

Comment thread R/deconvolution.R
Comment on lines +285 to +289
if (length(common) == 0 && n_target > 0) {
safe_target <- make.names(target_spots)
safe_raw <- make.names(rownames(results))
map_idx <- match(safe_target, safe_raw)
valid_idx <- which(!is.na(map_idx))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map sanitized spot IDs for partial matches

The fallback make.names mapping is only executed when there are zero exact spot-name matches, so mixed cases (some exact matches plus some sanitized-only matches) skip the fallback and leave valid spots as all-zero rows. This silently drops part of the RCTD output whenever only a subset of barcodes is transformed (for example, IDs containing - alongside IDs that were already syntactically safe).

Useful? React with 👍 / 👎.

Comment thread R/deconvolution.R
Comment on lines +273 to +274
if (res_cols == n_target && res_rows != n_target) {
results <- t(results)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use names, not dimensions, to detect weight orientation

The transpose heuristic in getRCTD() relies on dimensions (res_cols == n_target && res_rows != n_target) instead of dimnames, so a square weights matrix (same number of cell types and spots) is treated as already spot-by-feature even when spacexr returns cell-types-by-spots. In that case the subsequent row-wise normalization runs over cell types rather than spots and produces incorrect deconvolution proportions.

Useful? React with 👍 / 👎.

@Artur-man

Copy link
Copy Markdown
Collaborator

Dear @Atakannl, just checking in. You can also let me know if you do not wanna finish the PR. Although it is not ideal, I might do some adjustments and merge it.

@Atakannl

Copy link
Copy Markdown
Contributor Author

Dear @Artur-man , apologies for the delay. I've been fully occupied with finishing my master's thesis, and the submission deadline is very close, so I haven't been able to give this PR the attention it deserves.

I completely understand if you'd prefer to make the remaining adjustments and merge it yourself so it doesn't stay blocked. Thank you for all the work you've already put into it. Once my thesis is submitted, I'd be happy to contribute again to future improvements if needed.

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