Stores & Pipeline¶
Adapters for cohort-scale variant extraction and parallel annotation pipelines.
ZarrVariantStore¶
Cohort store backed by a vcf2zarr Zarr v3 archive.
Implements the VariantAnnotationStore port.
Key design points¶
variant_positionvalues are 1-based (VCF native, as written by vcf2zarr without--zero-based).open()loads the fullvariant_contigarray once and builds per-contig start/end offsets vianumpy.searchsorted, then discards the array.- Per-chromosome position vectors are pre-loaded into
_chrom_pos_cachefor O(1) mask computation insidefetch(). fetch()is thread-safe: it performs only read operations on pre-loaded numpy arrays and issues independent fancy-index reads against the Zarr store (Zarr'sLocalStoresupports concurrent reads).
Usage¶
from iris.adapters.driven.stores import ZarrVariantStore
with ZarrVariantStore(path="/data/wes_cohort.zarr") as store:
variants = store.fetch(region)
Genotype processors¶
ZarrVariantStore accepts an optional processor (a
GenotypeProcessor implementation)
and processor_context dict. When set, the processor is called once per
fetch block per ALT allele with the raw genotype arrays
(n_variants, n_samples, ploidy), and its returned dict is merged into each
VariantAnnotation.metadata.data. Built-in genotype counts (n_hom, n_het,
…) are always computed regardless of whether a processor is supplied. See
Genomic analysis for
concrete implementations (AncestryStratifiedAFProcessor,
PerIndividualAltCountProcessor).
iris.adapters.driven.stores.zarr
¶
ZarrVariantStore — vcf2zarr cohort store adapter.
Supports user-defined GenotypeProcessor callables injected at construction time, enabling cohort-level statistics, stratified allele frequencies, and regression models without subclassing.
ZarrVariantStore
¶
Bases: VariantAnnotationStore
Cohort-scale variant store backed by a vcf2zarr Zarr v3 archive.
Designed for variant_position values that are 1-based (VCF native,
as written by vcf2zarr without the --zero-based flag).
The store must be opened before use — either explicitly via
:meth:open or through the context-manager protocol::
with ZarrVariantStore(path=ZARR_PATH) as store:
variants = store.fetch(region)
After open():
- Contig offsets are built once from the full
variant_contigarray usingnumpy.searchsorted. - Per-chromosome position arrays are pre-loaded and cached for O(1) mask
computation in :meth:
fetch.
:meth:fetch is thread-safe: it performs only read operations on the
pre-loaded numpy arrays and issues independent fancy-index reads against
the underlying Zarr store (Zarr's LocalStore supports concurrent
reads).
| ATTRIBUTE | DESCRIPTION |
|---|---|
path |
Path to the Zarr store directory or
TYPE:
|
assembly |
Genome assembly label stored in created
:class:
TYPE:
|
source |
Population / source label used in the generated
:class:
TYPE:
|
processor
class-attribute
instance-attribute
¶
Optional user-defined processor called for each variant block.
When set, the processor receives the raw genotype arrays
(n_variants, n_samples, ploidy) and returns a dict of computed
values that are merged into VariantAnnotation.metadata.data.
The built-in genotype counts (n_hom, n_het, etc.) are always
computed regardless of whether a processor is supplied.
See :class:~iris.domain.genomics.ports.driven.genotype_processor.GenotypeProcessor.
processor_context
class-attribute
instance-attribute
¶
Arbitrary context dict forwarded to the processor on every call.
Use this to pass sample covariates, phenotypes, or configuration that the processor needs but that are not part of the genotype arrays.
sample_ids
property
¶
Sample IDs in the same order as the store's sample dimension.
Empty until :meth:open has been called. Used to align external
per-sample covariates (ancestry, phenotype, ...) to the gt/
gt_mask arrays a :class:GenotypeProcessor receives — the
processor itself is never given this ordering directly, so callers
that need it (e.g. building a valid_mask) read it from here
before constructing the processor.
open
¶
Open the Zarr store and build in-memory indices.
Loads the full variant_contig array (~35 MB for a 79k cohort)
once to build per-contig start / end offsets, then pre-loads each
per-chromosome position array into _chrom_pos_cache. The large
variant_contig array is discarded after the index is built.
| RETURNS | DESCRIPTION |
|---|---|
ZarrVariantStore
|
Self, so this can be used fluently: |
| RAISES | DESCRIPTION |
|---|---|
ImportError
|
If |
Source code in src/iris/adapters/driven/stores/zarr.py
close
¶
Release in-memory caches and close the Zarr store handle.
fetch
¶
Return all variants whose positions fall within region.
region.start and region.end must both be non-None and are
interpreted as 1-based inclusive coordinates matching the
variant_position encoding in the store.
The method opens the store lazily if :meth:open has not been called.
It is safe to call concurrently from multiple threads.
If you only need self.processor's side effects (e.g.
AltCountProcessor's per-sample accumulator, read back via
summary_stats()) and never look at the returned
VariantAnnotation objects, use :meth:scan instead — building
this list is most of fetch()'s cost with a wide multi-stratum
processor, and :meth:scan skips it entirely.
| PARAMETER | DESCRIPTION |
|---|---|
region
|
1-based inclusive genomic window.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[VariantAnnotation]
|
Possibly-empty list of |
list[VariantAnnotation]
|
class: |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
ImportError
|
If |
Source code in src/iris/adapters/driven/stores/zarr.py
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 | |
scan
¶
Run self.processor over region without materializing VariantAnnotation.
Use this instead of :meth:fetch when you only need the processor's
side effects — e.g. AltCountProcessor accumulates its per-sample
counters (read back via summary_stats() once every region has
been scanned) regardless of what a caller does with its per-block
return value. Building the Variant/Allele/GenomicPosition/
Metadata object graph in :meth:fetch is most of its cost when
the processor is configured with many strata; :meth:scan skips it
entirely while still calling self.processor with the exact same
arrays, in the exact same order, as :meth:fetch would.
It is safe to call concurrently from multiple threads, same as
:meth:fetch.
| PARAMETER | DESCRIPTION |
|---|---|
region
|
1-based inclusive genomic window.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
int
|
Number of (variant, ALT allele) pairs found in region — useful |
int
|
for progress logging, not otherwise meaningful. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
ImportError
|
If |
Source code in src/iris/adapters/driven/stores/zarr.py
UvarVariantStore¶
Region-scale store backed by the per-chromosome uvar Parquet database — an
exhaustive precomputed annotation source (~8.9B rows, ~0.55 TB across
GRCh38). Implements the VariantAnnotationStore
port via fetch(), plus two lower-level bulk-query methods
(query_region_columns, query_positions_columns) for callers that need
more than the fixed default column set.
Full data model, category-by-category schema, and live query output: see Genomic Data → uvar.
Key design points¶
- Both access modes are region-bounded —
region.start/region.endmust be set; there is no "fetch the whole file" mode given file sizes up to ~46 GB per chromosome. - Relies entirely on
pyarrow.datasetpredicate pushdown (row-group statistics pruning), not manual indexing — files are pre-sorted byposition. - Column projection works at top-level struct granularity only — e.g.
columns=["gnomad_genome"]returns the whole nested struct, not individual leaf fields. require_population_signal=Truepushes an "observed gnomAD frequency OR ClinVar entry" filter into the same scan — necessary because uvar is exhaustive (every theoretical SNV has a row) and a gene-sized region otherwise returns hundreds of thousands of never-observed substitutions.aggregate="pos_bin_1m"groups by the schema's own 1 Mb bin column for genome/chromosome-scale summaries instead of row-level data.
Usage¶
from iris.adapters.driven.stores.uvar import UvarVariantStore, COLUMN_GROUPS
store = UvarVariantStore(data_dir="/mnt/cephfs/hot_nvme/uvar")
annotations = store.fetch(region) # small (gene-sized) regions
cols = [*COLUMN_GROUPS["population_variation"], *COLUMN_GROUPS["clinical_significance"]]
table = store.query_region_columns(region, columns=cols, require_population_signal=True)
iris.adapters.driven.stores.uvar
¶
UvarVariantStore — Parquet-backed adapter for the uvar universal SNV database.
uvar is a per-chromosome Parquet database with exhaustive coverage of
every possible SNV in GRCh38 (3 rows per position, one per alternate allele),
416 columns of precomputed annotation (population frequencies, ClinVar,
in-silico predictors, conservation, regulatory scores, ...). Files are large
(up to ~46 GB per chromosome) and sorted by position, so this adapter relies
entirely on pyarrow.dataset predicate pushdown (row-group statistics
pruning) rather than any manual indexing.
Two access modes, both region-bounded (region.start/region.end must
both be set — there is no "fetch the whole file" mode given the file sizes):
- :meth:
fetch— full port conformance (VariantAnnotationStore), maps a small fixed default column set into :class:VariantAnnotation. Intended for small regions (gene-level); never requests all 416 columns. - :meth:
query_region_columns— low-level bulk path for the region-features use cases. Column projection works at TOP-LEVEL struct/scalar granularity only (e.g. requesting"gnomad_genome"returns the whole nested struct; there is no per-leaf-field pushdown).aggregate="pos_bin_1m"groups by the schema's existing 1 Mb bin column for genome/chromosome-level summaries instead of returning row-level data.
COLUMN_GROUPS
module-attribute
¶
COLUMN_GROUPS = {
name: (load_str_tuple(_TOML, "column_groups", name))
for name in (
"population_variation",
"clinical_significance",
"insilico",
)
}
Maps a dashboard "source" filter name to the top-level uvar columns it needs.
UvarVariantStore
¶
Bases: VariantAnnotationStore
Region-scale variant store backed by per-chromosome uvar Parquet files.
| ATTRIBUTE | DESCRIPTION |
|---|---|
data_dir |
Directory containing
TYPE:
|
file_pattern |
Filename template, formatted with the canonical
chromosome name (e.g.
TYPE:
|
assembly |
Genome assembly label stored in produced GenomicPosition objects.
TYPE:
|
source |
Label stored on produced VariantAnnotation.metadata.
TYPE:
|
query_region_columns
¶
Return uvar rows within region, projected to columns (plus position/chromosome).
| PARAMETER | DESCRIPTION |
|---|---|
region
|
1-based inclusive genomic window. Both start and end must be set — there is no unbounded query mode.
TYPE:
|
columns
|
Top-level uvar column names to project (see COLUMN_GROUPS for the dashboard-source groupings). Struct columns are returned whole (no per-leaf-field projection).
TYPE:
|
aggregate
|
When
TYPE:
|
require_population_signal
|
uvar is exhaustive (every theoretical
SNV, 3 rows/position) — for gene/cytoband-sized regions this
is hundreds of thousands to millions of rows, almost all
never-observed. When True, pushes down a row-group/predicate
filter for an observed gnomAD frequency or a ClinVar
classification (mirrors
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Table
|
A pyarrow.Table. Empty (zero columns) if the chromosome has no |
Table
|
backing file. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If region.start or region.end is None. |
Source code in src/iris/adapters/driven/stores/uvar.py
query_positions_columns
¶
Point-lookup rows for an explicit, possibly scattered set of positions.
Uses an isin predicate rather than :meth:query_region_columns's
>=/<= range — still row-group-prunable via pyarrow's statistics,
but the right tool when the targets are arbitrary positions from a variant
list (e.g. annotate-by-key) rather than one contiguous window. Used by
UvarAnnotator.
| PARAMETER | DESCRIPTION |
|---|---|
chrom
|
Chromosome, any spelling
TYPE:
|
positions
|
1-based positions to fetch.
TYPE:
|
columns
|
Top-level uvar column names to project.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Table
|
A pyarrow.Table. Empty (zero columns) if the chromosome has no |
Table
|
backing file or positions is empty. |
Source code in src/iris/adapters/driven/stores/uvar.py
fetch
¶
Return VariantAnnotation objects within region, using a fixed default column set.
Intended for small (gene-level) regions — always requests
_DEFAULT_FETCH_COLUMNS, never the full 416-column schema. Larger
regions should use :meth:query_region_columns directly with a
narrower, purpose-specific column set.
| PARAMETER | DESCRIPTION |
|---|---|
region
|
1-based inclusive genomic window.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[VariantAnnotation]
|
Possibly-empty list of VariantAnnotation. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If region.start or region.end is None. |
Source code in src/iris/adapters/driven/stores/uvar.py
VariantExtractionPipeline¶
Orchestrates parallel variant extraction followed by sequential annotation.
Two-phase execution¶
- Extraction — the store's
fetch()is called once per region using aThreadPoolExecutor. Because Zarr stores release the GIL for array reads, multi-threading yields near-linear speedups on I/O-bound workloads. - Annotation — collected
VariantAnnotationobjects are passed sequentially through each annotator. Annotators run in serial to avoid saturating tabix file handles or in-memory lookup tables.
Usage¶
from iris.adapters.driven.pipeline import VariantExtractionPipeline
from iris.adapters.driven.stores import ZarrVariantStore
from iris.adapters.driven.annotators.clinvar import ClinvarAnnotator
annotators = (ClinvarAnnotator(path=CLINVAR_TSV),)
with ZarrVariantStore(path=ZARR_PATH) as store:
pipeline = VariantExtractionPipeline(
store, annotators, workers=28, show_progress=True
)
variants = pipeline.run(regions=regions_from_bed)
iris.adapters.driven.pipeline
¶
VariantExtractionPipeline — parallel extraction and sequential annotation.
VariantExtractionPipeline
¶
Orchestrates parallel variant extraction followed by sequential annotation.
The pipeline executes in two phases:
-
Extraction — :attr:
storeis queried once per region using a :class:~concurrent.futures.ThreadPoolExecutorwith up to :attr:workersthreads. Because Zarr stores release the GIL for array reads, multi-threading yields near-linear speedups. -
Annotation — the collected :class:
~iris.domain.genomics.entities.VariantAnnotationobjects are passed through each annotator in :attr:annotatorssequentially. Annotators run in serial to avoid saturating the I/O devices they read from (tabix files, in-memory lookup tables, etc.).
Example::
annotators = (ClinvarAnnotator(path=CLINVAR_TSV),)
with ZarrVariantStore(path=ZARR_PATH) as store:
pipeline = VariantExtractionPipeline(store, annotators, workers=28)
variants = pipeline.run(regions=regions_from_bed)
| ATTRIBUTE | DESCRIPTION |
|---|---|
store |
Opened
:class:
TYPE:
|
annotators |
Zero or more
:class:
TYPE:
|
workers |
Number of threads for the extraction phase. Use
TYPE:
|
show_progress |
When
TYPE:
|
run
¶
Run the pipeline and return fully annotated variants.
Exactly one of regions or variants must be supplied.
- regions — each region is submitted as one extraction task.
- variants — each variant is converted to a point region
(pos, pos)and then de-duplicated by region to avoid redundant store reads. Variants without a valid position are skipped.
| PARAMETER | DESCRIPTION |
|---|---|
regions
|
Genomic windows to extract variants from.
TYPE:
|
variants
|
Specific variants whose positions define point regions.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[VariantAnnotation]
|
Fully annotated list of |
list[VariantAnnotation]
|
class: |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If both or neither of regions and variants are supplied. |