Genomic analysis¶
Driven adapters that implement genomics domain ports backed by third-party
scientific libraries (BiocPy, pyhpo), plus concrete GenotypeProcessor
implementations for cohort-level statistics injected into ZarrVariantStore.
Overview¶
Ports implemented¶
| Adapter | Port | Backend |
|---|---|---|
BiocpyRangeOperations |
RangeOperations |
BiocPy GenomicRanges |
PyHpoOperations |
HpoOperations |
pyhpo |
BiocpyRangeOperations¶
Bridges GenomicPosition domain objects and BiocPy's GenomicRanges. All
coordinate conversion is handled internally: iris is 1-based inclusive,
BiocPy GenomicRanges is 0-based half-open (iris start=1, end=10 ↔
BiocPy start=0, end=10).
PyHpoOperations¶
Implements HpoOperations over the pyhpo ontology. Term resolution prefers
hpo_id when available, falling back to name otherwise. pyhpo initializes
a module-level singleton on the first Ontology() call — subsequent
PyHpoOperations instances reuse the same in-memory ontology rather than
re-reading it from disk. data_directory may be passed to point at a custom
HPO data dump; when None (default), pyhpo's bundled data is used.
GenotypeProcessor implementations¶
Concrete implementations of the GenotypeProcessor
port, passed to ZarrVariantStore(processor=...) — see
Stores & Pipeline.
| Processor | Adds to VariantAnnotation.metadata |
|---|---|
AncestryStratifiedAFProcessor |
Per-decile allele frequency stratified by an ancestry covariate, plus a linear regression (slope, SE, p-value, R²) of AF against ancestry proportion |
PerIndividualAltCountProcessor |
Median and IQR of alt-allele counts and carrier status per individual across a variant block |
BiocPy range operations¶
iris.adapters.driven.genomic_analysis.biocpy
¶
BiocPy adapter for the RangeOperations port.
Bridges between iris GenomicPosition domain objects and the BiocPy GenomicRanges library. All coordinate conversion (1-based inclusive ↔ 0-based half-open) is handled here, keeping the domain model clean.
BiocPy GenomicRanges uses 0-based half-open intervals internally
- iris start=1, end=10 → BiocPy start=0, end=10
- BiocPy start=0, end=10 → iris start=1, end=10
BiocpyRangeOperations
¶
Bases: RangeOperations
RangeOperations adapter backed by BiocPy GenomicRanges.
Converts between iris GenomicPosition objects and BiocPy GenomicRanges, delegating all computation to the BiocPy library.
| PARAMETER | DESCRIPTION |
|---|---|
default_assembly
|
Assembly name to attach to returned GenomicPosition objects. If None, assembly is not set.
TYPE:
|
Example
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
union
¶
Return the union of two sets of genomic ranges.
| PARAMETER | DESCRIPTION |
|---|---|
a
|
First set of genomic ranges.
TYPE:
|
b
|
Second set of genomic ranges.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Merged, non-overlapping ranges covering all positions in a or b. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
intersect
¶
Return ranges present in both a and b.
| PARAMETER | DESCRIPTION |
|---|---|
a
|
First set of genomic ranges.
TYPE:
|
b
|
Second set of genomic ranges.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Ranges representing the intersection of a and b. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
setdiff
¶
Return ranges in a not covered by b.
| PARAMETER | DESCRIPTION |
|---|---|
a
|
Query ranges.
TYPE:
|
b
|
Ranges to subtract.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Ranges in a not overlapping any range in b. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
coverage
¶
Compute per-base coverage across all ranges.
| PARAMETER | DESCRIPTION |
|---|---|
ranges
|
Genomic ranges to compute coverage for.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict[str, list[int]]
|
Dict mapping chromosome to list of per-base coverage values. |
dict[str, list[int]]
|
Index 0 corresponds to position 1 on that chromosome. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
gaps
¶
Return gaps between ranges on each chromosome.
| PARAMETER | DESCRIPTION |
|---|---|
ranges
|
Input genomic ranges.
TYPE:
|
genome
|
Optional chromosome lengths for boundary gaps.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Ranges representing uncovered regions. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
disjoin
¶
Split ranges into non-overlapping disjoint intervals.
| PARAMETER | DESCRIPTION |
|---|---|
ranges
|
Input genomic ranges, possibly overlapping.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Maximal set of non-overlapping ranges. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
nearest
¶
Find the nearest subject range for each query range.
| PARAMETER | DESCRIPTION |
|---|---|
query
|
Ranges to search for neighbors.
TYPE:
|
subject
|
Ranges to search within.
TYPE:
|
ignore_strand
|
If False, only consider same-strand ranges.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[int | None]
|
List of indices into subject. None if no neighbor found. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
precede
¶
Find the subject range immediately upstream of each query range.
| PARAMETER | DESCRIPTION |
|---|---|
query
|
Ranges to search for upstream neighbors.
TYPE:
|
subject
|
Ranges to search within.
TYPE:
|
ignore_strand
|
If False, only consider same-strand ranges.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[int | None]
|
List of indices into subject. None if no upstream range found. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
follow
¶
Find the subject range immediately downstream of each query range.
| PARAMETER | DESCRIPTION |
|---|---|
query
|
Ranges to search for downstream neighbors.
TYPE:
|
subject
|
Ranges to search within.
TYPE:
|
ignore_strand
|
If False, only consider same-strand ranges.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[int | None]
|
List of indices into subject. None if no downstream range found. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
flank
¶
Return flanking regions for each input range.
| PARAMETER | DESCRIPTION |
|---|---|
ranges
|
Input genomic ranges.
TYPE:
|
width
|
Width of the flanking region in bases.
TYPE:
|
start
|
If True, flank the start; if False, flank the end.
TYPE:
|
both
|
If True, return flanks on both sides.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Flanking ranges. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
resize
¶
Resize ranges to a fixed width anchored at start, end, or center.
| PARAMETER | DESCRIPTION |
|---|---|
ranges
|
Input genomic ranges.
TYPE:
|
width
|
Target width in bases.
TYPE:
|
fix
|
Anchor point — one of 'start', 'end', or 'center'.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Resized ranges. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
shift
¶
Shift all ranges by a fixed number of bases.
| PARAMETER | DESCRIPTION |
|---|---|
ranges
|
Input genomic ranges.
TYPE:
|
shift
|
Bases to shift. Positive = downstream, negative = upstream.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[GenomicPosition]
|
Shifted ranges. |
Source code in src/iris/adapters/driven/genomic_analysis/biocpy.py
pyhpo operations¶
iris.adapters.driven.genomic_analysis.pyhpo
¶
PyHPO adapter for the HpoOperations port.
Bridges between iris Phenotype domain objects and the pyhpo library. Term resolution follows a two-step strategy: prefer hpo_id when available, fall back to name otherwise.
pyhpo initialises a module-level singleton on first Ontology() call.
Subsequent PyHpoOperations instances reuse the same ontology in memory.
PyHpoOperations
¶
Bases: HpoOperations
HpoOperations adapter backed by the pyhpo library.
On construction the HPO ontology is loaded into memory (once per process — pyhpo is a singleton). Subsequent instantiations reuse the already-loaded data without re-reading from disk.
| PARAMETER | DESCRIPTION |
|---|---|
data_directory
|
Path to a directory containing HPO data files.
When
TYPE:
|
Example
Source code in src/iris/adapters/driven/genomic_analysis/pyhpo.py
PyHpoEnrichmentModel
¶
Bases: PhenotypeEnrichmentModel
PhenotypeEnrichmentModel adapter backed by pyhpo's EnrichmentModel.
On construction the HPO ontology is loaded into memory (once per
process — pyhpo is a singleton). The 'gene' and 'omim'
EnrichmentModel instances are stateful and built lazily on first
use, then cached for the lifetime of the adapter.
| PARAMETER | DESCRIPTION |
|---|---|
data_directory
|
Path to a directory containing HPO data files.
When
TYPE:
|
Example
Source code in src/iris/adapters/driven/genomic_analysis/pyhpo.py
Genotype processors¶
iris.adapters.driven.genomic_analysis.processors
¶
Concrete GenotypeProcessor implementations for cohort-level analyses.
AncestryStratifiedAFProcessor
¶
AncestryStratifiedAFProcessor(
sample_amr,
*,
valid_mask=None,
n_deciles=10,
p_threshold=0.05,
min_ac_per_decile=5,
min_valid_deciles=3,
)
Compute per-decile allele frequencies and an ancestry linear regression.
For each variant computes:
af_decile: list of lengthn_decileswith AF per AMR quantile bin.amr_beta: regression slope AF ~ AMR decile midpoint.amr_se: standard error of the slope.amr_pvalue: two-sided p-value of the slope.amr_r2: coefficient of determination R².
| PARAMETER | DESCRIPTION |
|---|---|
sample_amr
|
float array
TYPE:
|
n_deciles
|
number of quantile bins. Default
TYPE:
|
p_threshold
|
significance threshold for downstream filtering.
Default
TYPE:
|
min_ac_per_decile
|
minimum AC in a decile to include it in the
regression. Default
TYPE:
|
min_valid_deciles
|
minimum non-NaN deciles required to run the
regression. Default
TYPE:
|
Example
import numpy as np
import pandas as pd
covars = pd.read_csv("ancestry.csv", index_col="sample_id")
amr = covars.loc[sample_order, "AMR"].values
processor = AncestryStratifiedAFProcessor(sample_amr=amr)
with ZarrVariantStore(path=ZARR_PATH, processor=processor) as store:
variants = store.fetch(region)
for va in variants:
print(va.metadata["af_decile"]) # list[float | None]
print(va.metadata["amr_pvalue"]) # float
Initialize the processor.
| PARAMETER | DESCRIPTION |
|---|---|
sample_amr
|
float array (n_valid_samples,) with AMR ancestry proportion, ALREADY FILTERED to only the samples that have a known ancestry value (same length and order as the True entries in valid_mask, if provided).
TYPE:
|
valid_mask
|
optional bool array (n_zarr_samples,) — True for samples present in the Zarr store that also have a known ancestry value. Samples with False are excluded from every computation (AF, deciles, regression). When None, all Zarr samples are assumed valid and sample_amr must cover all of them.
TYPE:
|
n_deciles
|
number of quantile bins. Default 10.
TYPE:
|
p_threshold
|
significance threshold for downstream filtering.
TYPE:
|
min_ac_per_decile
|
minimum AC in a decile to include it.
TYPE:
|
min_valid_deciles
|
minimum non-NaN deciles to run regression.
TYPE:
|
Source code in src/iris/adapters/driven/genomic_analysis/processors.py
HomozygousByQuintileProcessor
¶
Bases: GenotypeProcessor
Cuenta homocigotos alt (2 alelos alternativos en la misma muestra) por quintil.
Deprecated: usar AltCountProcessor(sample_strata=[SampleStratum(f"q{i}", mask), ...])
— mismo resultado (hom_qX/called_qX equivalen a all__qX__n_hom/
all__qX__n_called) más estratificación por variante y acumulación por
muestra (summary_stats()).
Para cada variante, agrupa las muestras por quintil (1..n_quintiles — p.ej. quintil de ancestría AMR) y cuenta cuántas son homocigotas para el alelo alternativo (ambos alelos == alt_idx) frente a cuántas tienen genotipo llamado en ese quintil.
| PARAMETER | DESCRIPTION |
|---|---|
sample_quintile
|
int array (n_valid_samples,) con el quintil (1.. n_quintiles) de cada muestra VÁLIDA, ya alineado y filtrado al mismo orden que las entradas True de valid_mask — mismo contrato que AncestryStratifiedAFProcessor.sample_amr.
TYPE:
|
valid_mask
|
bool array opcional (n_zarr_samples,) — True para las muestras del store que tienen un quintil conocido. Las muestras con False quedan fuera de todo el cómputo (nunca cuentan como homocigotas ni como llamadas). Si es None, se asume que todas las muestras del store son válidas y sample_quintile debe cubrirlas todas, en el mismo orden.
TYPE:
|
n_quintiles
|
número de quintiles. Default 5.
TYPE:
|
Example
import csv
import numpy as np
quintile_by_sample: dict[str, int] = {}
with open("mcps_ancestry_quintiles.csv") as fh:
for row in csv.DictReader(fh):
q = (row["amr_quintil"] or "").strip()
if q.isdigit():
quintile_by_sample[row["sample_code"]] = int(q)
with ZarrVariantStore(path=ZARR_PATH) as store:
valid, quintiles = [], []
for sample in store.sample_ids:
q = quintile_by_sample.get(sample)
valid.append(q is not None)
if q is not None:
quintiles.append(q)
sq, vm = np.array(quintiles), np.array(valid)
store.processor = HomozygousByQuintileProcessor(sample_quintile=sq, valid_mask=vm)
variants = store.fetch(region)
for va in variants:
print(va.metadata["hom_q1"], va.metadata["hom_rate_q1"])
Initialize the processor.
| PARAMETER | DESCRIPTION |
|---|---|
sample_quintile
|
int array (n_valid_samples,), ver docstring de clase.
TYPE:
|
valid_mask
|
bool array opcional (n_zarr_samples,), ver docstring de clase.
TYPE:
|
n_quintiles
|
número de quintiles. Default 5.
TYPE:
|
Source code in src/iris/adapters/driven/genomic_analysis/processors.py
PerIndividualAltCountProcessor
¶
Bases: GenotypeProcessor
Calcula la distribución de alelos alt por individuo sobre un bloque.
Deprecated: usar AltCountProcessor() (sin strata) — mismo resultado
(all__all__alt_counts_median/all__all__vars_median, etc.) más
estratificación opcional por muestra/variante y acumulación por muestra
(summary_stats()).
Nota: 'alt_counts' = 0/½ por individuo; 'vars' = 1 si portador (het o hom), 0 si no.
Initialize the processor.
Source code in src/iris/adapters/driven/genomic_analysis/processors.py
SampleStratum
dataclass
¶
Un grupo de muestras sobre el que estratificar (p.ej. un quintil de ancestría).
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
nombre del stratum — usado en el prefijo de las keys de salida.
TYPE:
|
mask |
bool array
TYPE:
|
VariantStratum
dataclass
¶
Un grupo de variantes sobre el que estratificar (p.ej. un panel de genes).
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
nombre del stratum — usado en el prefijo de las keys de salida.
TYPE:
|
cpra_set |
set de identificadores
TYPE:
|
AltCountProcessor
¶
Bases: GenotypeProcessor
Conteo de alelos alt estratificado por muestra y por variante, ortogonal.
Generaliza a PerIndividualAltCountProcessor (sin estratificar) y a
HomozygousByQuintileProcessor (estratificado por muestra) bajo una
sola implementación con dos ejes de estratificación independientes:
sample_strata: agrupa las muestras (p.ej. quintiles de ancestría). Por defecto (None) usa un único stratum"all"con todas las muestras del bloque.variant_strata: agrupa las variantes por identidad CPRA víacontext["cpra"](p.ej. un panel de genes). Por defecto (None) usa un único stratum"all"sin filtrar.
Para cada combinación (variant_stratum, sample_stratum) agrega al
metadata de cada variante, bajo el prefijo {vs_name}__{ss_name}__:
n_het, n_hom, n_called, af, alt_counts_median,
alt_counts_q25, alt_counts_q75, vars_median, vars_q25,
vars_q75 — todas NaN en las variantes que no pertenecen al
variant_stratum. Las últimas 6 (percentiles/medianas) se pueden omitir
con compute_percentiles=False — son las más caras de calcular y, con
muchas combinaciones de strata, las que más pesan en el metadata por
variante cuando el caller no las necesita (ver __init__).
Además acumula, por bloque, métricas por muestra recuperables al final
vía :meth:summary_stats:
n_variants: variantes portadas (het u hom) por la muestra, sumadas sobre todos los bloques — no depende de la agrupación por gen.n_genes_affected: 1 por gen si la muestra porta >= 1 variante delvariant_stratumen ese gen (sumado sobre todos los genes vistos).n_genes_biallelic: 1 por gen si la muestra es hom en alguna variante del gen, o portadora de >= 2 variantes het distintas del gen (en cualquier combinación de bloques de ese gen).
Identidad de "gen": context["gene"] — que ZarrVariantStore
completa automáticamente con region.name (la columna 4 del BED que
generó esa región) si el caller no lo puso ya. Cuando el BED tiene una
fila por gen (*.genes.bed), cada bloque ES un gen y el resultado es
idéntico al de antes. Cuando el BED tiene varias filas por gen
(*.exons.bed — un exón por fila), los bloques con el mismo
context["gene"] se combinan: portador en dos exones distintos del
mismo gen sigue contando n_genes_affected una sola vez, y compound-het
entre exones (variante het en el exón 2, otra variante het distinta en
el exón 7) sí se detecta como bialélico, sin volver a incluir los
intrones entre exones en el cómputo (cada bloque sigue siendo solo la
región exacta que le pasó el caller).
Si context["gene"] es None (BED sin columna de nombre, o el
caller no la puso), cada bloque vuelve a tratarse como su propio "gen"
— comportamiento histórico, sin agrupación.
n_genes_affected/n_genes_biallelic en _acc/summary_stats()
se derivan del estado por gen recién al llamar :meth:finalize (que
:meth:summary_stats invoca automáticamente) — no durante el escaneo,
porque los bloques de un mismo gen pueden llegar en cualquier orden o
en hilos distintos. Si leés processor._acc directamente en vez de
summary_stats() (como hace 04_gene_stats.py), llamá
processor.finalize() primero.
| PARAMETER | DESCRIPTION |
|---|---|
sample_strata
|
lista de
TYPE:
|
variant_strata
|
lista de
TYPE:
|
Example
strata = []
for q in range(1, 6):
strata.append(SampleStratum(f"q{q}", sample_quintile == q))
processor = AltCountProcessor(sample_strata=strata)
with ZarrVariantStore(path=ZARR_PATH, processor=processor) as store:
variants = store.fetch(region)
for va in variants:
print(va.metadata["all__q1__n_hom"])
stats = processor.summary_stats()
print(stats["all"]["q1"]["n_genes_affected"]["median"])
Initialize the processor.
| PARAMETER | DESCRIPTION |
|---|---|
sample_strata
|
ver docstring de clase.
TYPE:
|
variant_strata
|
ver docstring de clase.
TYPE:
|
compute_percentiles
|
si es
TYPE:
|
Source code in src/iris/adapters/driven/genomic_analysis/processors.py
finalize
¶
Deriva n_genes_affected/n_genes_biallelic finales desde el estado por gen.
Solo hace falta llamarlo explícitamente si leés processor._acc
directamente (p.ej. 04_gene_stats.py) — summary_stats() ya lo
invoca. Idempotente: siempre recalcula desde self._gene_state en
vez de incrementar, así que llamarlo varias veces no duplica conteos.
No toca las combinaciones (vs, ss) que nunca recibieron un
context["gene"] — esas ya quedaron acumuladas de inmediato en
_accumulate().
Source code in src/iris/adapters/driven/genomic_analysis/processors.py
summary_stats
¶
Distribución (median/q25/q75/mean/max) de las métricas por muestra.
Llama :meth:finalize primero — ver su docstring y el de la clase
sobre agrupación por gen.
Solo considera, para cada SampleStratum, las muestras que
pertenecen a ese stratum (ss.mask) — nunca las que quedaron
fuera de todo stratum de muestra.
| RETURNS | DESCRIPTION |
|---|---|
dict[str, dict[str, dict[str, dict[str, float]]]]
|
|