Skip to content

Commit 8ccf21a

Browse files
committed
Add batch_key support for hvg step
1 parent 9d25c29 commit 8ccf21a

4 files changed

Lines changed: 102 additions & 11 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ The pipeline requires the following primary inputs, typically configured via com
7676
* QC mode (`--qc_mode`): `original`, `multires`, or `combined` (default: `original`).
7777
* CellTypist model (`--celltypist_model`): `gut` preset or a path to a custom model.
7878
* Integration HVG strategy (`--hvg_strategy`): `cell_nuclei_union` or `global` (default: `cell_nuclei_union`).
79+
* Optional batch-aware HVG selection (`--hvg_batch_key`): obs column passed to `scanpy.pp.highly_variable_genes(batch_key=...)`.
80+
* HVG span (`--hvg_span`): span passed to Scanpy Seurat v3 HVG selection (default: `auto`, using `1.0` with `--hvg_batch_key` and `0.3` otherwise).
7981
* (optional) Cell‑level metadata CSV to add after pooling (`--metadata`).
8082
* (optional) CSV with custom QC thresholds for the GMM (`--metrics_csv`).
8183
* CSV for manual QC cutoffs when running in `subset` mode (`--limits_csv`) (see `example_cutoffs.csv`).
@@ -455,11 +457,13 @@ This step requires the h5ad object from `finalize_qc` step.
455457

456458
The integration script keeps memory lower by opening the input H5AD in backed mode, reading only the retained cells and selected HVGs for scVI training, then re-reading the original object at the end to write a full-gene integrated output. The output keeps all genes for retained cells and adds the scVI embedding, neighbours, UMAP, and HVG annotations.
457459

458-
Note: integrated outputs may differ from previous scAutoQC releases because HVG selection is now configurable and defaults to `cell_nuclei_union`. Use `--hvg_strategy global` to select one HVG set across all retained cells.
460+
Note: integrated outputs may differ from previous scAutoQC releases because HVG selection is now configurable and defaults to `cell_nuclei_union`. Use `--hvg_strategy global` to select one HVG set across all retained cells. Set `--hvg_batch_key <obs_column>` to run Scanpy HVG selection with `batch_key=<obs_column>`. With the default `--hvg_span auto`, the pipeline uses `span=1.0` for batch-aware HVG selection and Scanpy's `span=0.3` default otherwise.
459461

460462
The following preprocessing is applied before scVI training:
461463
* Stringent doublets are removed when `--from_scautoqc true`.
462464
* Cell-cycle genes are excluded from HVG selection when `--from_scautoqc true`.
465+
* If `--hvg_batch_key` is set, that obs column is passed to `scanpy.pp.highly_variable_genes` as `batch_key`; leave it empty to keep the existing non-batch-aware HVG behavior.
466+
* `--hvg_span` is passed to `scanpy.pp.highly_variable_genes` as `span`; `auto` uses `1.0` for batch-aware HVG selection to reduce LOESS near-singularity failures, while retaining Scanpy's `0.3` default when no HVG batch key is used.
463467
* HVGs are selected with `--hvg_strategy`:
464468
* `cell_nuclei_union` (default): calculates HVGs separately for observations annotated as `cell` and `nuclei` in `cell_or_nuclei`, then uses the union of both sets. `--n_top_genes` is applied per group, so the final union can contain more than `--n_top_genes` genes.
465469
* `global`: ignores `cell_or_nuclei` and calculates one HVG set from all retained cells. `--n_top_genes` is the total target number of HVGs.

bin/integration.py

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ def str_to_bool(value):
5252
return str(value).strip().lower() in {"1", "true", "t", "yes", "y"}
5353

5454

55+
def optional_obs_key(value):
56+
"""Return a non-empty obs key or None for unset Nextflow/CLI values."""
57+
if value is None:
58+
return None
59+
value = str(value).strip()
60+
return value or None
61+
62+
63+
def resolve_hvg_span(value, hvg_batch_key):
64+
"""Resolve an explicit HVG span or choose a batch-aware default."""
65+
if value is None or str(value).strip().lower() in {"", "auto"}:
66+
return 1.0 if hvg_batch_key is not None else 0.3
67+
68+
try:
69+
hvg_span = float(value)
70+
except ValueError as error:
71+
raise ValueError("--hvg_span must be 'auto' or a number in (0, 1].") from error
72+
73+
if not 0 < hvg_span <= 1:
74+
raise ValueError("--hvg_span must be greater than 0 and less than or equal to 1.")
75+
return hvg_span
76+
77+
5578
def close_backed(adata):
5679
if getattr(adata, "isbacked", False):
5780
adata.file.close()
@@ -240,7 +263,9 @@ def get_training_masks(source_ad, from_scautoqc):
240263
return keep_cells, keep_genes
241264

242265

243-
def select_cell_nuclei_hvg_union(source_ad, keep_cells, keep_genes, n_top_genes):
266+
def select_cell_nuclei_hvg_union(
267+
source_ad, keep_cells, keep_genes, n_top_genes, hvg_batch_key, hvg_span
268+
):
244269
"""Select union HVGs and retain per-modality HVG annotations."""
245270
group_key = "cell_or_nuclei"
246271
if group_key not in source_ad.obs:
@@ -281,6 +306,8 @@ def select_cell_nuclei_hvg_union(source_ad, keep_cells, keep_genes, n_top_genes)
281306
modality_ad,
282307
flavor="seurat_v3",
283308
n_top_genes=min(n_top_genes, modality_ad.n_vars),
309+
batch_key=hvg_batch_key,
310+
span=hvg_span,
284311
subset=False,
285312
inplace=False,
286313
)
@@ -317,7 +344,9 @@ def select_cell_nuclei_hvg_union(source_ad, keep_cells, keep_genes, n_top_genes)
317344
return hvg_genes, hvg_info
318345

319346

320-
def select_global_hvgs(source_ad, keep_cells, keep_genes, n_top_genes):
347+
def select_global_hvgs(
348+
source_ad, keep_cells, keep_genes, n_top_genes, hvg_batch_key, hvg_span
349+
):
321350
"""Select HVGs from all retained cells regardless of cell/nuclei status."""
322351
log("Global HVG selection will use all retained cells.")
323352
with log_step("Read matrix for global HVG selection"):
@@ -334,6 +363,8 @@ def select_global_hvgs(source_ad, keep_cells, keep_genes, n_top_genes):
334363
hvg_ad,
335364
flavor="seurat_v3",
336365
n_top_genes=min(n_top_genes, hvg_ad.n_vars),
366+
batch_key=hvg_batch_key,
367+
span=hvg_span,
337368
subset=False,
338369
inplace=False,
339370
)
@@ -354,23 +385,52 @@ def select_global_hvgs(source_ad, keep_cells, keep_genes, n_top_genes):
354385
return hvg_genes, hvg_info
355386

356387

357-
def select_hvgs(source_ad, keep_cells, keep_genes, n_top_genes, hvg_strategy):
388+
def select_hvgs(
389+
source_ad,
390+
keep_cells,
391+
keep_genes,
392+
n_top_genes,
393+
hvg_strategy,
394+
hvg_batch_key,
395+
hvg_span,
396+
):
397+
if hvg_batch_key is not None and hvg_batch_key not in source_ad.obs:
398+
raise KeyError(f"Expected HVG batch key {hvg_batch_key!r} in adata.obs.")
399+
400+
if hvg_batch_key is None:
401+
log("HVG selection will not use a batch key.")
402+
else:
403+
log(f"HVG selection will use batch key: {hvg_batch_key}")
404+
log(f"HVG selection will use span: {hvg_span}")
405+
358406
if hvg_strategy == "cell_nuclei_union":
359-
return select_cell_nuclei_hvg_union(source_ad, keep_cells, keep_genes, n_top_genes)
407+
return select_cell_nuclei_hvg_union(
408+
source_ad, keep_cells, keep_genes, n_top_genes, hvg_batch_key, hvg_span
409+
)
360410
if hvg_strategy == "global":
361-
return select_global_hvgs(source_ad, keep_cells, keep_genes, n_top_genes)
411+
return select_global_hvgs(
412+
source_ad, keep_cells, keep_genes, n_top_genes, hvg_batch_key, hvg_span
413+
)
362414
raise ValueError(f"Unknown HVG strategy: {hvg_strategy}")
363415

364416

365-
def read_training_object(obj_path, from_scautoqc, n_top_genes, hvg_strategy):
417+
def read_training_object(
418+
obj_path, from_scautoqc, n_top_genes, hvg_strategy, hvg_batch_key, hvg_span
419+
):
366420
"""Load only retained cells and selected HVGs for scVI training."""
367421
with log_step(f"Open input object backed: {obj_path}"):
368422
source_ad = sc.read_h5ad(obj_path, backed="r")
369423
try:
370424
keep_cells, keep_genes = get_training_masks(source_ad, from_scautoqc)
371425
with log_step(f"Select HVGs using strategy: {hvg_strategy}"):
372426
hvg_genes, hvg_info = select_hvgs(
373-
source_ad, keep_cells, keep_genes, n_top_genes, hvg_strategy
427+
source_ad,
428+
keep_cells,
429+
keep_genes,
430+
n_top_genes,
431+
hvg_strategy,
432+
hvg_batch_key,
433+
hvg_span,
374434
)
375435
with log_step("Read final SCVI training matrix"):
376436
train_ad = read_minimal_view(source_ad, keep_cells, hvg_genes)
@@ -462,6 +522,22 @@ def main():
462522
"cell_or_nuclei and calculates one HVG set from all retained cells."
463523
),
464524
)
525+
parser.add_argument(
526+
"--hvg_batch_key",
527+
default="",
528+
help=(
529+
"optional obs column to pass as batch_key to "
530+
"scanpy.pp.highly_variable_genes"
531+
),
532+
)
533+
parser.add_argument(
534+
"--hvg_span",
535+
default="auto",
536+
help=(
537+
"span to pass to scanpy.pp.highly_variable_genes; "
538+
"auto uses 1.0 with --hvg_batch_key and 0.3 otherwise [default: auto]"
539+
),
540+
)
465541
parser.add_argument(
466542
"--verbose",
467543
action="store_true",
@@ -478,7 +554,8 @@ def main():
478554
log(
479555
"integration.py started with "
480556
f"obj={args.obj}, batch={args.batch}, n_top_genes={args.n_top_genes}, "
481-
f"hvg_strategy={args.hvg_strategy}, from_scautoqc={args.from_scautoqc}"
557+
f"hvg_strategy={args.hvg_strategy}, hvg_batch_key={args.hvg_batch_key}, "
558+
f"hvg_span={args.hvg_span}, from_scautoqc={args.from_scautoqc}"
482559
)
483560

484561
arches_params = dict(
@@ -490,10 +567,17 @@ def main():
490567
)
491568

492569
from_scautoqc = str_to_bool(args.from_scautoqc)
570+
hvg_batch_key = optional_obs_key(args.hvg_batch_key)
571+
hvg_span = resolve_hvg_span(args.hvg_span, hvg_batch_key)
493572

494573
with log_step("Prepare training object"):
495574
train_ad, hvg_info = read_training_object(
496-
args.obj, from_scautoqc, args.n_top_genes, args.hvg_strategy
575+
args.obj,
576+
from_scautoqc,
577+
args.n_top_genes,
578+
args.hvg_strategy,
579+
hvg_batch_key,
580+
hvg_span,
497581
)
498582
gc.collect()
499583

main.nf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ process integrate {
199199
path("*.pkl")
200200

201201
script:
202+
def hvgBatchArg = params.hvg_batch_key ? "--hvg_batch_key ${params.hvg_batch_key}" : ""
202203
"""
203-
python ${projectDir}/bin/integration.py --obj ${qc2_out} --batch ${params.batch_key} --n_top_genes ${params.n_top_genes} --hvg_strategy ${params.hvg_strategy} --from_scautoqc ${params.from_scautoqc}
204+
python ${projectDir}/bin/integration.py --obj ${qc2_out} --batch ${params.batch_key} --n_top_genes ${params.n_top_genes} --hvg_strategy ${params.hvg_strategy} ${hvgBatchArg} --hvg_span ${params.hvg_span} --from_scautoqc ${params.from_scautoqc}
204205
"""
205206
}
206207

nextflow.config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ params {
7878
from_scautoqc = true
7979
n_top_genes = 5000
8080
hvg_strategy = "cell_nuclei_union" // Options: cell_nuclei_union, global
81+
hvg_batch_key = "" // Optional obs column for batch-aware HVG selection
82+
hvg_span = "auto" // Uses 1.0 with hvg_batch_key and 0.3 otherwise
8183
batch_key = ""
8284

8385
// Container image used by portable profiles.

0 commit comments

Comments
 (0)