"""Chromosome lengths of a genomic score, and where each one came from.
A score's table can usually say how long a contig is, but the backends
answer with different confidence -- a bigWig header is exact, the tabix
probe brackets an upper bound, the in-memory backend only knows how far its
rows reach -- and the callers that split a contig into regions also need to
know WHY there is no length when there is none (gain#509). So the record
here keeps three things apart: the number, its provenance, and the
:class:`~gain.genomic_resources.genomic_position_table.ContigExtent` reason
when the number is absent.
The ladder the epic (gain#1412) settles is genome label → bigWig header →
tabix estimate, applied per contig of the score. The resolver here answers
every rung live: the genome rung from the ``ReferenceGenome`` the caller
hands in, the rest through the table. It holds no repository, so
resolving the score's ``reference_genome`` label into that genome is the
caller's job.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from gain.genomic_resources.genomic_position_table import (
ChromLengthSource,
ContigExtent,
)
if TYPE_CHECKING:
from gain.genomic_resources.reference_genome import ReferenceGenome
from .base import GenomicScore
# Both enums are the table layer's -- the provenance vocabulary because
# three of its four members are facts each backend declares about its own
# format (``chrom_length_source``), the extent because it is the table
# that proves a contig empty or fails to measure it; re-exported from
# here, where the record that carries them lives, so a caller finds them
# beside the resolver (the ``BIGWIG_VALUE_COLUMN`` pattern) and the
# statistics implementation need not import the table package at all
# (gain#410).
__all__ = [
"ChromLength",
"ChromLengthSource",
"ContigExtent",
"derive_chrom_length",
"derive_chrom_lengths",
]
[docs]
@dataclass(frozen=True)
class ChromLength:
"""One contig's length, its source, or the reason there is none.
Exactly one of the two shapes: ``length`` and ``source`` set with
``extent`` ``None``, or both ``None`` with ``extent`` saying why --
``EMPTY`` when the table proved the contig holds no records,
``UNDETERMINED`` when the probe could not answer for a contig that may
well hold some.
"""
length: int | None
source: ChromLengthSource | None
extent: ContigExtent | None
[docs]
def derive_chrom_length(
score: GenomicScore, chrom: str,
ref_genome: ReferenceGenome | None = None,
) -> ChromLength:
"""Resolve one contig of ``score`` through the ladder.
``ref_genome`` is the top rung: a contig it lists is answered with its
true length, tagged ``REFERENCE_GENOME``, and the table is never asked.
A contig it does not list -- or no genome at all -- falls through to
the table, whose source is whatever the backend declares its lengths
to be. Raises ``ValueError`` when the score is not open or does not
carry ``chrom`` -- a bad question, as opposed to an absent answer -- in
the TABLE's words, since it is the table that refuses; the genome rung
sits behind the table's own screen so that a contig only the genome
knows is refused the same way.
"""
if (ref_genome is not None
and score.table.has_chromosome(chrom)
and chrom in ref_genome.get_all_chrom_lengths()):
return ChromLength(
length=ref_genome.get_chrom_length(chrom),
source=ChromLengthSource.REFERENCE_GENOME, extent=None)
length = score.table.find_chromosome_length(chrom)
if isinstance(length, ContigExtent):
return ChromLength(length=None, source=None, extent=length)
return ChromLength(
length=length, source=score.chrom_length_source, extent=None)
[docs]
def derive_chrom_lengths(
score: GenomicScore,
ref_genome: ReferenceGenome | None = None,
) -> dict[str, ChromLength]:
"""Resolve every contig of ``score``, in the table's order.
The universe is the score's contigs -- ``get_all_chromosomes()`` --
never the whole genome; a whole-reference denominator is a property of
the genome and belongs to coverage (gain#1041). Keyed by contig so
the consumer that splits regions (the statistics build) looks up by
name; the dict keeps table order.
Not memoised: on a tabix score every call re-runs the probe per
contig. Raises ``ValueError`` on a score that is not open.
"""
return {
chrom: derive_chrom_length(score, chrom, ref_genome)
for chrom in score.get_all_chromosomes()
}