Skip to content

feat: add converter of reference from seurat to h5 and parquet#114

Open
mariiabilous wants to merge 2 commits into
mainfrom
rmd_2_h5
Open

feat: add converter of reference from seurat to h5 and parquet#114
mariiabilous wants to merge 2 commits into
mainfrom
rmd_2_h5

Conversation

@mariiabilous

Copy link
Copy Markdown
Collaborator

Add Seurat → HDF5/Parquet reference conversion

Summary

Adds a rule that converts scRNA-seq reference .rds objects into HDF5 counts + Parquet metadata, and wires those files as inputs to the reference-based annotation rule. Lets Python tooling (e.g. py-RCTD) read the reference without going through R/rhdf5.

Changes

  • Snakefile: REFERENCE_STEM wildcard + REFERENCE_PATH_TO_RDS lookup, built from experiments.<_cell_type_annotation>.<reference_files>.
  • rules/_reference_preprocessing/convert_seurat_2_h5.smk (new): runConvertSeuratReferenceToH5{reference_stem}.rds_counts.h5 + _metadata.parquet.
  • scripts/_reference_preprocessing/convert_seurat_2_h5.R (new): does the actual conversion via write10xCounts() + arrow::write_parquet().
  • rules/reference_preprocessing.smk: helpers to resolve an annotation rule's wildcards to the corresponding .h5/.parquet paths.

Notes

  • Bumped mem_mb multiplier after an OOM on external_CRC.rds.
  • Old draft files (rules/seurat_to_h5.smk, scripts/utils/seurat_to_h5.R) are unused, safe to delete.

Testing

  • temporarily added .h5 as input to rules/_cell_type_annotation/_reference_based/rctd.smk
  • ./run.sh -n confirms DAG pulls in conversion automatically when outputs are missing.
  • Ran conversion directly on external_CRC.rds; succeeded after memory fix.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds a Seurat reference .rds to HDF5/Parquet conversion pipeline: a new Snakemake rule and R script convert references into 10x-style count matrices and metadata tables, with supporting path-collection logic in the Snakefile and new helper functions wired into cell type annotation rules. Default workflow targets are also toggled.

Changes

Reference RDS to H5/Parquet Conversion Pipeline

Layer / File(s) Summary
Snakefile reference path collection and target toggles
workflow/Snakefile
Adds _collect_reference_rds_paths(), REFERENCE_PATH_TO_RDS, REFERENCE_STEM globals, extends wildcard_constraints, includes the new reference_preprocessing.smk module, and disables several rule all targets (reports, coexpression, joint_analysis, count_correction, post_count_correction_cell_type_annotation) while keeping cell_type_annotation and doublet_finding enabled.
Reference preprocessing module and conversion rule
workflow/rules/reference_preprocessing.smk, workflow/rules/_reference_preprocessing/convert_seurat_2_h5.smk
Adds get_path2reference_by_reference_path helper and the runConvertSeuratReferenceToH5 rule defining inputs/outputs, dynamic memory/retry resources, logging, container, and script invocation.
Seurat to H5/Parquet conversion script
workflow/scripts/_reference_preprocessing/convert_seurat_2_h5.R
Implements seurat_to_h5_parquet to extract counts (version-aware), write 10x HDF5 output, build metadata with optional reduction embeddings, write Parquet, and the Snakemake entry point with logging.
Cell type annotation path helpers
workflow/rules/cell_type_annotation.smk
Adds functions to compute reference-based annotation paths and derive H5/Parquet paths via REFERENCE_PATH_TO_RDS.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Snakefile
    participant runConvertSeuratReferenceToH5
    participant convertScript as convert_seurat_2_h5.R
    participant CellTypeAnnotation as cell_type_annotation.smk

    Snakefile->>Snakefile: _collect_reference_rds_paths()
    Snakefile->>Snakefile: build REFERENCE_PATH_TO_RDS, REFERENCE_STEM
    Snakefile->>runConvertSeuratReferenceToH5: resolve reference_stem input
    runConvertSeuratReferenceToH5->>convertScript: invoke with assay/layer/genome params
    convertScript->>convertScript: extract counts, write HDF5
    convertScript->>convertScript: build metadata, write Parquet
    convertScript-->>runConvertSeuratReferenceToH5: outputs (_counts.h5, _metadata.parquet)
    CellTypeAnnotation->>CellTypeAnnotation: get_h5path4reference_based_annotation
    CellTypeAnnotation->>CellTypeAnnotation: get_parquetpath4reference_based_annotation
    CellTypeAnnotation->>Snakefile: lookup via REFERENCE_PATH_TO_RDS
Loading

Possibly related issues

Poem

A rabbit hopped through Seurat's data deep,
Converted counts while others were asleep,
🐇 h5 and parquet, side by side,
New paths through REFERENCE_PATH_TO_RDS glide,
Hop, hop, hooray — the pipeline's leap!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: adding a Seurat reference converter to H5 and Parquet outputs.
Description check ✅ Passed The description accurately summarizes the new reference conversion rule, script, and wiring into annotation inputs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rmd_2_h5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workflow/Snakefile (1)

1406-1422: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Default rule all now excludes several pipeline outputs. get_input2all() still defaults reports, coexpression, joint_analysis, count_correction, and post_count_correction_cell_type_annotation to True, but rule all overrides them to False, so a plain snakemake run no longer builds the same target set. Leave those arguments unset, or restore the previous defaults, unless this narrowing is intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/Snakefile` around lines 1406 - 1422, The default target set in rule
all is narrower than get_input2all()’s defaults, so a plain snakemake run no
longer builds the full expected pipeline outputs. Update rule all to use
get_input2all() with the same default behavior for reports, coexpression,
joint_analysis, count_correction, and
post_count_correction_cell_type_annotation, or remove those overrides if
narrowing is not intended. Use the rule all block and the get_input2all() call
as the places to align the target selection.
🧹 Nitpick comments (5)
workflow/rules/_reference_preprocessing/convert_seurat_2_h5.smk (1)

5-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale header comment doesn't match the actual script: path.

The comment references "../../../scripts/utils/seurat_to_h5_parquet.R" (3 levels up, utils/ dir), but the actual directive at Line 35 is "../../scripts/_reference_preprocessing/convert_seurat_2_h5.R" (2 levels up, _reference_preprocessing/ dir). Left as-is, this will mislead anyone relocating the rule file later.

📝 Suggested fix
-# NOTE: `script:` path is relative to *this* rule file's location — as
-# written it assumes the same nesting depth as seurat.R
-# ("../../../scripts/utils/seurat_to_h5_parquet.R"); adjust the leading
-# "../" count if you place this .smk file elsewhere.
+# NOTE: `script:` path is relative to *this* rule file's location
+# ("../../scripts/_reference_preprocessing/convert_seurat_2_h5.R");
+# adjust the leading "../" count if you place this .smk file elsewhere.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/rules/_reference_preprocessing/convert_seurat_2_h5.smk` around lines
5 - 14, The header comment in convert_seurat_2_h5.smk is stale and no longer
matches the rule’s actual script target. Update the NOTE block near the top to
reference the real script path used by the rule’s script directive, and make
sure the nesting-depth guidance matches the current location of the
`convert_seurat_2_h5` rule so future moves of this file are not guided by the
old `seurat_to_h5_parquet.R` path.
workflow/scripts/_reference_preprocessing/convert_seurat_2_h5.R (2)

21-26: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap conversion in tryCatch/on.exit to guarantee sink cleanup and clean error surfacing.

If readRDS(), stopifnot(), or stop() inside seurat_to_h5_parquet() fail, execution jumps straight past Lines 185-187, leaving sink() diverted and log_con unclosed for the remainder of the R session (harmless on process exit, but fragile if this script is ever sourced/re-run interactively, and means the log file may not be flushed consistently on failure).

♻️ Suggested refactor
 log_file <- snakemake@log[[1]]
 log_con  <- file(log_file, open = "wt")
 sink(log_con, type = "output")
 sink(log_con, type = "message")
+on.exit({
+  sink(type = "message")
+  sink(type = "output")
+  close(log_con)
+}, add = TRUE)

Also applies to: 185-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/scripts/_reference_preprocessing/convert_seurat_2_h5.R` around lines
21 - 26, Wrap the script’s logging and conversion flow in a guarded cleanup so
sinks are always restored even on failure. In the `log setup` block and around
`seurat_to_h5_parquet()`, use `tryCatch` with an `on.exit` cleanup to close
`log_con` and undo both `sink()` calls, and make sure errors from `readRDS()`,
`stopifnot()`, or `stop()` are rethrown or logged clearly. Use the `log_file`,
`log_con`, and `seurat_to_h5_parquet()` symbols to locate the setup and cleanup
points.

113-129: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No collision guard when merging derived columns into metadata.

meta[shared, colnames(emb)] <- emb[shared, , drop = FALSE] (Line 123) will silently overwrite an existing metadata column if a reduction's generated name (e.g. pca_1) already exists in meta.data. Similarly, df[["cell_id"]] <- rownames(df) (Line 137) silently overwrites any pre-existing cell_id metadata column. Both are edge cases, but failures would be silent data corruption in the exported Parquet rather than a visible error.

🛡️ Suggested guard
       colnames(emb) <- paste0(red_name, "_", seq_len(n_keep))
+      clobbered <- intersect(colnames(emb), colnames(meta))
+      if (length(clobbered)) {
+        warning(sprintf("Reduction '%s' overwrites existing metadata columns: %s",
+                         red_name, paste(clobbered, collapse = ", ")))
+      }
       shared <- intersect(rownames(meta), rownames(emb))
   df <- as.data.frame(meta)
+  if ("cell_id" %in% colnames(df)) {
+    warning("Existing 'cell_id' metadata column will be overwritten with cell barcodes.")
+  }
   df[["cell_id"]] <- rownames(df)

Also applies to: 136-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/scripts/_reference_preprocessing/convert_seurat_2_h5.R` around lines
113 - 129, The metadata merge in convert_seurat_2_h5.R can silently overwrite
existing columns when adding reduction-derived fields or the cell_id field.
Update the preprocessing flow around meta, the extra_reductions loop, and the
cell_id assignment to detect name collisions before writing into df/meta.data,
and either skip, rename, or stop with a clear error if any generated column
already exists. Use the existing symbols Embeddings, extra_reductions, and
cell_id handling to place the guard where the derived columns are added.
workflow/rules/cell_type_annotation.smk (1)

120-138: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Wire the reference-based outputs through the new helpers The _cell_type_annotation/_reference_based/*.smk rules still use get_path2reference4reference_based_annotation; get_h5path4reference_based_annotation and get_parquetpath4reference_based_annotation are currently unused, so the shared stem logic and .rds check never apply.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/rules/cell_type_annotation.smk` around lines 120 - 138, Wire the
reference-based rules to the new path helpers so the shared stem logic is
actually used. Update the `_cell_type_annotation/_reference_based/*.smk` rule
inputs/outputs to call get_h5path4reference_based_annotation and
get_parquetpath4reference_based_annotation instead of
get_path2reference4reference_based_annotation, and keep
_reference_path4reference_based_annotation as the single source of truth for
deriving the reference key and validating the .rds-backed stem.
workflow/rules/reference_preprocessing.smk (1)

6-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused get_path2reference_by_reference_path helper
workflow/rules/_reference_preprocessing/convert_seurat_2_h5.smk uses reference_stem directly, and there are no repo-wide call sites for this helper. Dropping it would remove dead code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/rules/reference_preprocessing.smk` around lines 6 - 12, Remove the
dead `get_path2reference_by_reference_path` helper from
`reference_preprocessing.smk`; it is not referenced anywhere and
`convert_seurat_2_h5.smk` already uses `reference_stem` directly. Delete the
unused function and keep the surrounding Snakefile logic intact, making sure no
remaining rules or imports depend on `REFERENCE_PATH_TO_RDS` through this
helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workflow/Snakefile`:
- Around line 126-159: The debug print in the reference path collection block
should be removed or replaced with a properly available logger, since `sys` is
not defined where `_collect_reference_rds_paths()` and `REFERENCE_STEM` are
built. Also harden `_collect_reference_rds_paths()` so it skips or normalizes
null `references` entries before calling `.items()`, using the existing config
access around `get_dict_value` and the `reference_files_by_condition` loop to
avoid crashes on explicit nulls.

---

Outside diff comments:
In `@workflow/Snakefile`:
- Around line 1406-1422: The default target set in rule all is narrower than
get_input2all()’s defaults, so a plain snakemake run no longer builds the full
expected pipeline outputs. Update rule all to use get_input2all() with the same
default behavior for reports, coexpression, joint_analysis, count_correction,
and post_count_correction_cell_type_annotation, or remove those overrides if
narrowing is not intended. Use the rule all block and the get_input2all() call
as the places to align the target selection.

---

Nitpick comments:
In `@workflow/rules/_reference_preprocessing/convert_seurat_2_h5.smk`:
- Around line 5-14: The header comment in convert_seurat_2_h5.smk is stale and
no longer matches the rule’s actual script target. Update the NOTE block near
the top to reference the real script path used by the rule’s script directive,
and make sure the nesting-depth guidance matches the current location of the
`convert_seurat_2_h5` rule so future moves of this file are not guided by the
old `seurat_to_h5_parquet.R` path.

In `@workflow/rules/cell_type_annotation.smk`:
- Around line 120-138: Wire the reference-based rules to the new path helpers so
the shared stem logic is actually used. Update the
`_cell_type_annotation/_reference_based/*.smk` rule inputs/outputs to call
get_h5path4reference_based_annotation and
get_parquetpath4reference_based_annotation instead of
get_path2reference4reference_based_annotation, and keep
_reference_path4reference_based_annotation as the single source of truth for
deriving the reference key and validating the .rds-backed stem.

In `@workflow/rules/reference_preprocessing.smk`:
- Around line 6-12: Remove the dead `get_path2reference_by_reference_path`
helper from `reference_preprocessing.smk`; it is not referenced anywhere and
`convert_seurat_2_h5.smk` already uses `reference_stem` directly. Delete the
unused function and keep the surrounding Snakefile logic intact, making sure no
remaining rules or imports depend on `REFERENCE_PATH_TO_RDS` through this
helper.

In `@workflow/scripts/_reference_preprocessing/convert_seurat_2_h5.R`:
- Around line 21-26: Wrap the script’s logging and conversion flow in a guarded
cleanup so sinks are always restored even on failure. In the `log setup` block
and around `seurat_to_h5_parquet()`, use `tryCatch` with an `on.exit` cleanup to
close `log_con` and undo both `sink()` calls, and make sure errors from
`readRDS()`, `stopifnot()`, or `stop()` are rethrown or logged clearly. Use the
`log_file`, `log_con`, and `seurat_to_h5_parquet()` symbols to locate the setup
and cleanup points.
- Around line 113-129: The metadata merge in convert_seurat_2_h5.R can silently
overwrite existing columns when adding reduction-derived fields or the cell_id
field. Update the preprocessing flow around meta, the extra_reductions loop, and
the cell_id assignment to detect name collisions before writing into
df/meta.data, and either skip, rename, or stop with a clear error if any
generated column already exists. Use the existing symbols Embeddings,
extra_reductions, and cell_id handling to place the guard where the derived
columns are added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7623f6ee-bb80-4d46-9df4-1b959427e56d

📥 Commits

Reviewing files that changed from the base of the PR and between f0ee419 and 97f77a4.

📒 Files selected for processing (5)
  • workflow/Snakefile
  • workflow/rules/_reference_preprocessing/convert_seurat_2_h5.smk
  • workflow/rules/cell_type_annotation.smk
  • workflow/rules/reference_preprocessing.smk
  • workflow/scripts/_reference_preprocessing/convert_seurat_2_h5.R

Comment thread workflow/Snakefile
Comment on lines +126 to +159
def _collect_reference_rds_paths() -> list[str]:
"""
All unique .rds reference paths configured across conditions/reference
names under experiments.<_cell_type_annotation>.<reference_files>.
"""
mapping = {}
reference_files_by_condition = get_dict_value(
config,
"experiments",
cc.EXPERIMENTS_CELL_TYPE_ANNOTATION_NAME,
cc.EXPERIMENTS_CELL_TYPE_ANNOTATION_REFERENCE_FILES_NAME,
replace_none={},
)
for condition_id, references in reference_files_by_condition.items():
for reference_name, path in references.items():
if path:
mapping[f"{condition_id}/{reference_name}"] = path
return mapping

REFERENCE_PATH_TO_RDS = _collect_reference_rds_paths()

# reference_stem is the .rds path with the extension stripped — the
# conversion rule writes <reference_stem>_counts.h5 and
# <reference_stem>_metadata.parquet right next to the source .rds.
REFERENCE_STEM = uniquify_elements_in_list(
[
path[: -len(".rds")]
for path in REFERENCE_PATH_TO_RDS.values()
if path.endswith(".rds")
]
)

print(f"[DEBUG] REFERENCE_STEM: {REFERENCE_STEM}", file=sys.stderr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '^import sys|^import re' workflow/scripts/utils/config_utils.py workflow/scripts/utils/raw_data_utils.py workflow/scripts/utils/run_time_utils.py 2>/dev/null
rg -n 'import sys' workflow/Snakefile

Repository: bdsc-tds/xenium_analysis_pipeline

Length of output: 328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Snakefile imports and sys usages =="
rg -n '^(import|from) ' workflow/Snakefile
rg -n '\bsys\b' workflow/Snakefile

echo "== Snakefile includes =="
rg -n '^include:' workflow/Snakefile

echo "== Surrounding lines for the reviewed block =="
nl -ba workflow/Snakefile | sed -n '110,170p'

echo "== Search for the referenced config helper behavior =="
rg -n 'def get_dict_value|replace_none|None' workflow/scripts/utils/config_utils.py
nl -ba workflow/scripts/utils/config_utils.py | sed -n '1,220p'

Repository: bdsc-tds/xenium_analysis_pipeline

Length of output: 1183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All sys imports in repo =="
rg -n '^import sys$|^from sys import|\\bsys\\b' workflow -g '!**/.snakemake/**' || true

echo "== get_dict_value definition =="
rg -n 'def get_dict_value|replace_none|replace_' workflow/scripts/utils/config_utils.py
sed -n '1,220p' workflow/scripts/utils/config_utils.py

echo "== included utility files (top of file) =="
for f in workflow/scripts/utils/config_utils.py workflow/scripts/utils/raw_data_utils.py workflow/scripts/utils/run_time_utils.py; do
  echo "--- $f ---"
  sed -n '1,40p' "$f"
done

echo "== Snakefile block around reference stem =="
sed -n '120,165p' workflow/Snakefile

Repository: bdsc-tds/xenium_analysis_pipeline

Length of output: 13631


Import sys or remove this debug print, and guard null reference entries
print(..., file=sys.stderr) will raise NameError here because workflow/Snakefile and the included utility scripts don’t define sys. Remove or gate the debug line before merge. Also handle references before calling .items(), since an explicit null config entry would currently crash the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/Snakefile` around lines 126 - 159, The debug print in the reference
path collection block should be removed or replaced with a properly available
logger, since `sys` is not defined where `_collect_reference_rds_paths()` and
`REFERENCE_STEM` are built. Also harden `_collect_reference_rds_paths()` so it
skips or normalizes null `references` entries before calling `.items()`, using
the existing config access around `get_dict_value` and the
`reference_files_by_condition` loop to avoid crashes on explicit nulls.

@mariiabilous mariiabilous changed the title feat: add sonverter of reference from seurat to h5 and parquet feat: add converter of reference from seurat to h5 and parquet Jul 6, 2026
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