Source code for genoray._reference
from __future__ import annotations
from pathlib import Path
import numpy as np
import pysam
from numpy.typing import NDArray
from ._contigs import ContigNormalizer
_PAD = ord("N")
[docs]
class Reference:
"""A reference genome backed by an indexed FASTA, read on demand via pysam.
One contig is held in memory at a time and sliced for flanking-base lookups.
Queries accept ``chr``-prefixed or unprefixed contig names interchangeably.
Do not instantiate directly; use :meth:`Reference.from_path`.
"""
def __init__(self, path: Path, contigs: list[str]):
self.path = path
self._fasta = pysam.FastaFile(str(path))
# pysam reports the FASTA's own contig names; build a normalizer from them.
self._c_norm = ContigNormalizer(list(self._fasta.references))
self.contigs = contigs
self._cur_contig: str | None = None
self._cur_seq: NDArray[np.uint8] | None = None
def __del__(self):
if hasattr(self, "_fasta"):
self._fasta.close()
@classmethod
def from_path(
cls, fasta: str | Path, contigs: list[str] | None = None
) -> "Reference":
path = Path(fasta)
if not path.exists():
raise FileNotFoundError(f"FASTA {path} does not exist.")
fai = path.with_suffix(path.suffix + ".fai")
if not fai.exists():
pysam.faidx(str(path))
f = pysam.FastaFile(str(path))
all_contigs = list(f.references)
f.close()
return cls(path, contigs if contigs is not None else all_contigs)
def _load_contig(self, contig: str) -> NDArray[np.uint8]:
norm = self._c_norm.norm(contig)
if norm is None:
raise ValueError(f"Contig {contig!r} not found in reference {self.path}.")
if norm != self._cur_contig:
seq = self._fasta.fetch(norm) # whole contig as str
self._cur_seq = np.frombuffer(seq.encode("ascii"), dtype=np.uint8)
self._cur_contig = norm
assert self._cur_seq is not None
return self._cur_seq
[docs]
def contig_array(self, contig: str) -> NDArray[np.uint8]:
"""Return the full contig sequence as a cached uint8 array.
Accepts ``chr``-prefixed or unprefixed names. One contig is held in
memory at a time (shared with :meth:`fetch`).
"""
return self._load_contig(contig)
[docs]
def fetch(self, contig: str, start: int, end: int) -> NDArray[np.uint8]:
"""Return reference bytes for 0-based half-open ``[start, end)``.
Positions outside the contig are padded with ``N``. Returns a uint8
array; ``bytes(...)`` gives the ASCII sequence.
"""
seq = self._load_contig(contig)
n = len(seq)
out = np.full(end - start, _PAD, dtype=np.uint8)
src_s = max(start, 0)
src_e = min(end, n)
if src_e > src_s:
out[src_s - start : src_e - start] = seq[src_s:src_e]
return out