Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 34 additions & 5 deletions src/seqforge/cli/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,18 @@ def io_umi_count(
rendered command line saying two contradictory things about which GTF was used, in a repo whose
wiring gate reads rendered commands.

**Which of that annotation's genes are ribosomal is looked up here too, and deliberately after
the pair above is settled.** The counter is handed gene ids rather than a category name, for the
same reason it is handed a resolved database: the package that knows which genes are which is the
one this verb already imports, and the module below stays testable without it. Asking after the
`--component` rewrite is what keeps the answer one organism's — the same question put to a
Chimera concatenates every Component's ids, and a ribosomal share pooling two organisms is a
share of a population nobody asked about, while this verb is already invoked once per Component.
An annotation no curated list ships for, or one whose list declares no ribosomes, is not a
refusal: it costs the plate that one `obs` column and never a zero one, which is what an
unmeasured thing looks like everywhere else on this path. A curated file that is present and
broken is a different thing, and stays the refusal below.

**`--threads` counts cells at once, and defaults to one.** The counting rule asks the scheduler
for threads and hands them over here; a hand invocation that says nothing gets the single-core
plate it used to get. The cells are independent and the annotation is read once before the
Expand Down Expand Up @@ -889,9 +901,9 @@ def io_umi_count(
typer.echo(json.dumps({"error": str(exc)}), err=True)
raise typer.Exit(2) from exc
try:
from genome import (
Genome, # untyped lab package; resolved here, off the strict workflow path
)
# untyped lab package; resolved here, off the strict workflow path. The two absences are
# imported by name because they have to be CAUGHT by name -- see the lookup below.
from genome import GeneCategoryNotDeclaredError, Genome, NoGeneCategoriesError
except ImportError as exc: # pragma: no cover - depends on the host
typer.echo(json.dumps({"error": f"liulab-genome is not importable: {exc}"}), err=True)
raise typer.Exit(3) from exc
Expand All @@ -918,10 +930,27 @@ def io_umi_count(
# pair becomes the Component's own and the resolution below is the single one both
# forms take -- and the refusal below names the Component whose GTF went missing.
assembly, annotation = component, registered
# Bound once, AFTER the pair above is settled, so both lookups below are put to the same
# genome -- under `--component` that is the Component's own and never the Chimera's.
genome = Genome(assembly)
# The gate above leaves exactly one of the two forms and the arm above has filled the
# annotation in; the checker cannot see a relation between two options, so it is said here.
gtf = Path(str(Genome(assembly).annotations.path(cast(str, annotation))))
written = write_umi_counts(plate, gtf.with_suffix(".db"), out, workers=threads)
gtf = Path(str(genome.annotations.path(cast(str, annotation))))
# Which of this annotation's genes are ribosomal, asked of that same settled pair -- what a
# Chimera would answer instead, and why it may not be asked, is in the docstring above.
try:
rrna_gene_ids = genome.gene_list("rRNA", annotation).gene_ids
except (GeneCategoryNotDeclaredError, NoGeneCategoriesError):
# No curated list ships for this annotation, or one does and does not declare
# ribosomes: either way the metric is unmeasurable here, which costs the plate that
# column and never a zero one. Caught by NAME rather than as `LookupError`, which an
# unregistered annotation also raises -- swallowing that would turn the refusal below
# into a silently missing column, and a defect in a shipped curated file lands there
# too, as the `ValueError` it is.
rrna_gene_ids = None
written = write_umi_counts(
plate, gtf.with_suffix(".db"), out, workers=threads, rrna_gene_ids=rrna_gene_ids
)
except UmiCountError as exc:
typer.echo(json.dumps({"error": str(exc)}), err=True)
raise typer.Exit(3) from exc
Expand Down
14 changes: 13 additions & 1 deletion src/seqforge/workflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
from ..kb.schema import Spec

#: CalVer YYYY.M.PATCH; bump when any shipped module's rules/params change.
#: 2026.8.24 — `rule umi_count` says how much of each cell's yield was ribosomal (#471). Two more
#: `obs` columns on the plate object — `n_umis_rrna` and `rrna_fraction`, both counted over
#: `umi_combined`, the same population `n_umis` and the saturation are read off — and the page carries
#: the share beside them, ungraded. The gene set is liulab-genome's curated `rRNA` category for the
#: annotation the plate was actually counted against, so nothing here owns a list of ribosomal genes;
#: an annotation that cannot answer, or one whose axis shares no id with the answer, yields NEITHER
#: column rather than a plate of `0.0%`, which is the exact claim the measurement exists to prevent.
#: **The bump is owed by the ARTIFACT and not by a moved command line**, the same as 2026.8.18: the
#: rule's params, its declared outputs and every matrix it writes are byte-identical — these genes
#: were already counted as expression and still are, and nothing was filtered — and what moves is the
#: shape of the object that comes out, which an h5ad written before this could not otherwise be told
#: apart from one written after.
#: 2026.8.23 — `rule genome_index` is DELETED from all five modules and the lookup it performed is a
#: `params:` callable in each (#478, from #475). It owned `results/index/<assembly>`, a path every
#: concurrent instance over one results directory shares, and snakemake removes an output before
Expand Down Expand Up @@ -611,7 +623,7 @@
#: dereferenced and never declared. The contract was wrong, not the module.
#: 2026.7.1 — star.smk hardcodes --outSAMtype (it is a module detail, and starsolo.smk always
#: hardcoded it); required_config gains primary_feature and drops bulk.outSAMtype.
WORKFLOW_VERSION = "2026.8.23"
WORKFLOW_VERSION = "2026.8.24"

_MODULE_DIR = Path(__file__).parent

Expand Down
Loading
Loading