Skip to content

Resolve gene symbols to genomic regions

resolve_gene_regions is the canonical entry point for any analysis that starts from a list of genes and needs the corresponding genomic regions to query for variants. It accepts a GeneQuery (genes by symbol, file, or inheritance filter), resolves each gene against a GeneRepository, and returns per-gene regions with splice buffers applied.

Minimal example

from pathlib import Path
from iris.adapters.driven.genomic_analysis.biocpy import BiocpyRangeOperations
from iris.adapters.driven.readers.providers.gencode import GencodeGeneRepository
from iris.applications.genomics.use_cases import resolve_gene_regions
from iris.domain.genomics.objects import GeneQuery
from iris.domain.genomics.services import GenomicRangeService

repo = GencodeGeneRepository(
    "/data/gencode.v49.annotation.gff3.gz",
    assembly="GRCh38",
    tags={"MANE_Select", "Ensembl_canonical"},
)
service = GenomicRangeService(ops=BiocpyRangeOperations())

result = resolve_gene_regions(
    GeneQuery(symbols=("BRCA1", "CFTR", "HEXA")),
    gene_repository=repo,
    range_service=service,
    splice_buffer_bp=8,
)

# Flat list of GenomicPosition — pass directly to a pipeline:
regions = result.all_regions()

# Gene cache keyed by symbol and Ensembl ID — for VepFlightAnnotator:
gene_cache = result.gene_cache()

Reading symbols from a file

One gene symbol per line; lines starting with # and blank lines are ignored.

result = resolve_gene_regions(
    GeneQuery.from_file(Path("recgenes.txt")),
    gene_repository=repo,
    range_service=service,
)

You can combine a file with extra explicit symbols — duplicates are removed, file symbols come first:

GeneQuery.from_file(Path("panel.txt"), symbols=("BRCA1", "TP53"))

Filtering by inheritance mode

GeneQuery.recessive() and GeneQuery.dominant() are convenience constructors that set inheritance_modes to AR and AD respectively.

# All autosomal-recessive genes in the repository:
result = resolve_gene_regions(
    GeneQuery.recessive(),
    gene_repository=repo,
    range_service=service,
)

# AR genes from a curated panel:
result = resolve_gene_regions(
    GeneQuery.recessive(symbols_file=Path("panel.txt")),
    gene_repository=repo,
    range_service=service,
)

Note

inheritance_modes filtering requires GeneAnnotation.diseases to carry MendelianInheritance data from a rich source (OMIM, Orphanet). Until that data is available, the filter is a no-op.

Filtering by disease name

disease_names is a case-insensitive substring filter applied against all disease names and aliases in GeneAnnotation.diseases:

result = resolve_gene_regions(
    GeneQuery(symbols=("CFTR", "BRCA1"), disease_names=("fibrosis", "breast cancer")),
    gene_repository=repo,
    range_service=service,
)

Genes with no diseases are excluded when disease_names is set.

Skipping the splice buffer

Pass splice_buffer_bp=0 to use raw exon coordinates. range_service is not required when the buffer is zero:

result = resolve_gene_regions(
    GeneQuery(symbols=("TP53",)),
    gene_repository=repo,
    splice_buffer_bp=0,
)

Inspecting results

for rg in result.resolved:
    method = rg.resolution_method   # "transcript" | "gene_body"
    ts = rg.transcript_used         # TranscriptStructure | None
    print(f"{rg.gene.symbol}: {len(rg.regions)} region(s) via {method}")
    if ts:
        print(f"  transcript: {ts.name} (MANE={ts.is_mane_select})")

if result.unresolved_symbols:
    print("Unresolved:", result.unresolved_symbols)

Resolution strategy

For each gene symbol, the use case applies the following strategy in order:

  1. Direct lookupgene_repository.get_annotation(symbol).
  2. HGNC alias fallback (only when nomenclature_resolver is supplied) — resolves the input symbol to its approved HGNC name and retries the lookup. Useful for clinical gene lists that use aliases ("MLL""KMT2A").
  3. Transcript selection — MANE Select > Ensembl canonical > first available; exons of the chosen transcript with splice buffer applied.
  4. Gene body fallback — if no transcript has exons, the full gene span is used as a single region (no splice buffer applied).

Genes that cannot be resolved by any strategy are collected in result.unresolved_symbols.

Reference

iris.applications.genomics.use_cases.resolve_gene_regions

Use case: resolve gene symbols to genomic query regions via a GeneRepository.

This is the canonical entry point for any analysis that starts from a list of genes (by name, by inheritance mode, or by disease) and needs the corresponding genomic regions to query for variants.

The use case is intentionally free of infrastructure
  • It receives a GeneRepository port, not a GencodeGeneRepository.
  • It receives a GenomicRangeService for the splice buffer operation.
  • Any caller — CLI command, API endpoint, notebook, or pipeline script — uses the same function with different concrete dependencies injected.
Example
from iris.adapters.driven.genomic_analysis.biocpy import BiocpyRangeOperations
from iris.adapters.driven.readers.providers.gencode import GencodeGeneRepository
from iris.applications.genomics.use_cases.resolve_gene_regions import resolve_gene_regions
from iris.domain.genomics.objects import GeneQuery
from iris.domain.genomics.services import GenomicRangeService

repo = GencodeGeneRepository(
    "/data/gencode.v49.annotation.gff3.gz", assembly="GRCh38", tags={"MANE_Select", "Ensembl_canonical"}
)
service = GenomicRangeService(ops=BiocpyRangeOperations())

result = resolve_gene_regions(
    GeneQuery.from_file(Path("genes.txt")), gene_repository=repo, range_service=service, splice_buffer_bp=8
)

# Flat list of GenomicPosition for VariantExtractionPipeline:
regions = result.all_regions()

# Gene cache for VepFlightAnnotator:
gene_cache = result.gene_cache()

resolve_gene_regions

resolve_gene_regions(
    query,
    feature_region,
    *,
    gene_repository,
    range_service=None,
    splice_buffer_bp=2,
    nomenclature_resolver=None,
    constraint_store=None,
    log_every=500,
)

Resolve a GeneQuery to per-gene genomic regions ready for variant extraction.

Resolution strategy per gene (first success wins): 1. Direct lookup: gene_repository.get_annotation(symbol). 2. HGNC alias fallback (only when nomenclature_resolver is supplied): resolve the input symbol to its approved HGNC symbol via nomenclature_resolver.resolve_symbol(), then retry the repository lookup with the canonical name. Handles aliases, previous symbols, and capitalisation differences common in curated clinical gene lists (e.g. "MLL""KMT2A"). 3. Transcript selection: MANE Select > Ensembl canonical > first available; exons of the chosen transcript with splice buffer applied. 4. Gene body fallback: if no transcript has exons, the full gene genomic span is used as a single region (no splice buffer).

Post-load filters from query (inheritance_modes, disease_names) are applied after fetching annotations, so they work regardless of whether the backend supports native filtering.

PARAMETER DESCRIPTION
query

Specification of which genes to resolve and how.

TYPE: GeneQuery

gene_repository

Any GeneRepository implementation (e.g. GencodeGeneRepository).

TYPE: GeneRepository

range_service

GenomicRangeService for the splice buffer operation. Required when splice_buffer_bp > 0. If None, the buffer is silently skipped even if splice_buffer_bp is non-zero.

TYPE: GenomicRangeService | None DEFAULT: None

splice_buffer_bp

Bases to extend each exon into flanking introns to capture splice-donor/acceptor variants. Default 2. 0 = no buffer.

TYPE: int DEFAULT: 2

nomenclature_resolver

Optional GeneNomenclatureResolver (e.g. HgncNomenclatureResolver) used as a fallback when a symbol is not found directly in gene_repository. The resolver maps the input name to its approved HGNC symbol via resolve_symbol(), which is then retried against the repository. Allows clinical gene lists that use aliases or previous symbols to resolve without manual curation.

TYPE: GeneNomenclatureResolver | None DEFAULT: None

constraint_store

Optional GeneConstraintStore (e.g. GnomadConstraintStore) used to attach gnomAD constraint metrics (LOEUF, pLI, ...) to each resolved gene's GeneAnnotation.constraint. Skipped (stays None) when omitted or when the gene has no constraint coverage.

TYPE: GeneConstraintStore | None DEFAULT: None

log_every

Log a progress message every N genes resolved.

TYPE: int DEFAULT: 500

RETURNS DESCRIPTION
ResolvedGeneRegions

ResolvedGeneRegions with one ResolvedGeneRegion per resolved gene

ResolvedGeneRegions

and a tuple of symbols that could not be resolved.

RAISES DESCRIPTION
ValueError

If splice_buffer_bp > 0 and range_service is None.

Source code in src/iris/applications/genomics/use_cases/resolve_gene_regions.py
def resolve_gene_regions(
    query: GeneQuery,
    feature_region: str,
    *,
    gene_repository: GeneRepository,
    range_service: GenomicRangeService | None = None,
    splice_buffer_bp: int = 2,
    nomenclature_resolver: GeneNomenclatureResolver | None = None,
    constraint_store: GeneConstraintStore | None = None,
    log_every: int = 500,
) -> ResolvedGeneRegions:
    """Resolve a GeneQuery to per-gene genomic regions ready for variant extraction.

    Resolution strategy per gene (first success wins):
      1. Direct lookup: ``gene_repository.get_annotation(symbol)``.
      2. HGNC alias fallback (only when ``nomenclature_resolver`` is supplied):
         resolve the input symbol to its approved HGNC symbol via
         ``nomenclature_resolver.resolve_symbol()``, then retry the
         repository lookup with the canonical name.  Handles aliases,
         previous symbols, and capitalisation differences common in curated
         clinical gene lists (e.g. ``"MLL"`` → ``"KMT2A"``).
      3. Transcript selection: MANE Select > Ensembl canonical > first available;
         exons of the chosen transcript with splice buffer applied.
      4. Gene body fallback: if no transcript has exons, the full gene
         genomic span is used as a single region (no splice buffer).

    Post-load filters from ``query`` (``inheritance_modes``, ``disease_names``)
    are applied after fetching annotations, so they work regardless of
    whether the backend supports native filtering.

    Args:
        query: Specification of which genes to resolve and how.
        gene_repository: Any GeneRepository implementation
            (e.g. GencodeGeneRepository).
        range_service: GenomicRangeService for the splice buffer operation.
            Required when ``splice_buffer_bp > 0``.  If None, the buffer
            is silently skipped even if ``splice_buffer_bp`` is non-zero.
        splice_buffer_bp: Bases to extend each exon into flanking introns
            to capture splice-donor/acceptor variants. Default 2.
            0 = no buffer.
        nomenclature_resolver: Optional GeneNomenclatureResolver (e.g.
            ``HgncNomenclatureResolver``) used as a fallback when a symbol
            is not found directly in ``gene_repository``.  The resolver maps
            the input name to its approved HGNC symbol via
            ``resolve_symbol()``, which is then retried against the
            repository.  Allows clinical gene lists that use aliases or
            previous symbols to resolve without manual curation.
        constraint_store: Optional GeneConstraintStore (e.g.
            ``GnomadConstraintStore``) used to attach gnomAD constraint
            metrics (LOEUF, pLI, ...) to each resolved gene's
            ``GeneAnnotation.constraint``.  Skipped (stays ``None``) when
            omitted or when the gene has no constraint coverage.
        log_every: Log a progress message every N genes resolved.

    Returns:
        ResolvedGeneRegions with one ResolvedGeneRegion per resolved gene
        and a tuple of symbols that could not be resolved.

    Raises:
        ValueError: If ``splice_buffer_bp > 0`` and ``range_service`` is None.
    """
    if splice_buffer_bp > 0 and range_service is None:
        raise ValueError(
            "range_service is required when splice_buffer_bp > 0. "
            "Pass GenomicRangeService(ops=BiocpyRangeOperations()) or "
            "set splice_buffer_bp=0 to skip the splice buffer."
        )

    # Resolve the full symbol list from query
    all_symbols: list[str] = list(query.symbols)
    if query.symbols_file is not None:
        file_symbols = [
            line.strip()
            for line in query.symbols_file.read_text().splitlines()
            if line.strip() and not line.strip().startswith("#")
        ]
        # Prepend file symbols not already in explicit list, keep all explicit.
        seen: set[str] = set(all_symbols)
        all_symbols = [s for s in file_symbols if s not in seen] + all_symbols

    if not all_symbols:
        log.warning("GeneQuery resolved to an empty symbol list — nothing to resolve.")
        return ResolvedGeneRegions(resolved=(), unresolved_symbols=())

    resolved: list[ResolvedGeneRegion] = []
    unresolved: list[str] = []

    for i, symbol in enumerate(all_symbols, 1):
        ann = gene_repository.get_annotation(symbol)

        # HGNC alias fallback: if the repository doesn't know this symbol,
        # ask the nomenclature resolver for the approved canonical symbol
        # and retry.  Many clinical gene lists use aliases or previous
        # symbols (e.g. "MLL" instead of "KMT2A") that repositories indexed
        # under the approved name won't find on direct lookup.
        # Uses resolve_symbol() from the GeneNomenclatureResolver port so
        # any conforming implementation works — not just HgncNomenclatureResolver.
        if ann is None and nomenclature_resolver is not None:
            nom = nomenclature_resolver.resolve_symbol(symbol)
            if nom is not None:
                canonical = nom.symbol
                if canonical != symbol:
                    ann = gene_repository.get_annotation(canonical)
                    if ann is not None:
                        log.debug(
                            "resolve_gene_regions: '%s' resuelto via nomenclature_resolver"
                            " como '%s'",
                            symbol,
                            canonical,
                        )

        if ann is None:
            unresolved.append(symbol)
            continue

        if constraint_store is not None:
            ann = attrs.evolve(ann, constraint=constraint_store.get(ann.gene.symbol))

        if not _matches_query_filters(ann, query):
            continue

        # Try primary transcript first
        ts = ann.primary_transcript()

        if feature_region == "exons" and ts is not None and ts.exons:
            regions = (
                _apply_splice_buffer(ts.exons, splice_buffer_bp, range_service)
                if range_service
                else ts.exons
            )
            method = "transcript"
        else:
            # Gene body fallback
            regions_tuple = _gene_body_regions(ann)
            if not regions_tuple:
                unresolved.append(symbol)
                continue
            regions = regions_tuple
            ts = None
            method = "gene_body"

        resolved.append(
            ResolvedGeneRegion(
                gene=ann.gene,
                annotation=ann,
                regions=regions,
                transcript_used=ts,
                resolution_method=method,
            )
        )

        if i % log_every == 0:
            log.info("resolve_gene_regions: %d/%d genes resueltos hasta ahora", len(resolved), i)

    n_transcript = sum(1 for r in resolved if r.resolution_method == "transcript")
    n_gene_body = sum(1 for r in resolved if r.resolution_method == "gene_body")
    log.info(
        "resolve_gene_regions: %d/%d genes resueltos "
        "(transcrito: %d, gen completo: %d, buffer ±%dpb) · %d no resueltos",
        len(resolved),
        len(all_symbols),
        n_transcript,
        n_gene_body,
        splice_buffer_bp,
        len(unresolved),
    )
    if unresolved:
        log.warning(
            "%d genes no resueltos: %s%s",
            len(unresolved),
            ", ".join(unresolved[:10]),
            "..." if len(unresolved) > 10 else "",
        )

    return ResolvedGeneRegions(resolved=tuple(resolved), unresolved_symbols=tuple(unresolved))