| name | genoray-api |
| description | Use when writing or modifying Python code that imports `genoray` to read genotypes/dosages from VCF, PGEN, or SparseVar (`.svar`) files. Covers the public API surface, mode constants, range queries, chunking, filtering, and the SparseVar workflow. Skip for unrelated bioinformatics work. |
genoray public API
genoray is a NumPy-first range-query layer over VCF/BCF (cyvcf2), PGEN
(pgenlib), and a sparse memmap format (SparseVar / .svar).
Public surface
import genoray exposes exactly:
genoray.PGEN — PLINK 2 PGEN reader
genoray.Reference — indexed-FASTA reference genome reader
genoray.VCF — VCF/BCF reader
genoray.Filter — VCF filter value object bundling a cyvcf2 record predicate (record) with its matching .gvi polars expression (expr)
genoray.SparseVar — sparse .svar reader/writer
genoray.SparseVar2 — next-gen sparse variant store (VCF/BCF → SVAR2 conversion via from_vcf (supports regions=/samples=/merge_overlapping=/regions_overlap=), PLINK2 PGEN → SVAR2 conversion via from_pgen, N single-sample VCFs/BCFs → one SVAR2 store via a native k-way merge in from_vcf_list (reference/no_reference supported like from_vcf, absent sites fill hom-ref; supports regions=/merge_overlapping=/regions_overlap= but no samples= — the cohort is the file set), SVAR1 (SparseVar) → SVAR2 native migration via from_svar1 (reads no VCF/htslib; biallelic SVAR1 only; supports regions=/samples=/merge_overlapping=/regions_overlap= like from_vcf/from_pgen); range queries via decode/region_counts/read_ranges; mutational-signature support (SBS96/DBS78/ID83) via annotate_mutations/mutation_matrix/assign_signatures, or classify during the write with from_vcf(signatures=True)/from_pgen(signatures=True)/from_svar1(signatures=True); scalar-numeric INFO/FORMAT field extraction during the write via from_vcf(info_fields=, format_fields=)/from_vcf_list(info_fields=, format_fields=) (from_vcf_list merges INFO first-carrier-wins, FORMAT per-sample); from_pgen instead stores per-sample dosage tracks as FORMAT fields via dosages=Sequence[DosageField] (from the hardcall .pgen itself via source="self", or a separate .pgen) — it still has no info_fields=/format_fields= (PGEN has no VCF INFO/FORMAT); from_svar1 carries SVAR1's existing fields through selectively via fields= (None default = all, [] = none, or a name subset) — read back opt-in via fields=/with_fields/available_fields and attached to decode's result)
genoray.InfoField / genoray.FormatField — frozen dataclasses (name, dtype=None, default=None) configuring a single INFO/FORMAT field for SparseVar2.from_vcf; a bare str name uses inferred defaults instead
genoray.DosageField — frozen dataclass (name="dosage", source="self"|Path, dtype="f16"|"f32"="f32", default=None) configuring a PGEN dosage FORMAT field for SparseVar2.from_pgen
genoray.exprs — polars filter expressions for .gvi indexes
genoray.cosmic_signatures — fetch/cache COSMIC reference signatures
genoray.fit_signatures — sparse forward-selection signature refit
Nothing else is public. Anything starting with _ (e.g. genoray._vcf) is
internal — do not import it from user code.
Where to look for details
Prefer reading these over guessing:
docs/source/index.md — narrative tour with full examples (VCF, PGEN, filtering, chunking)
docs/source/svar.md — SparseVar usage
genoray/__init__.py — confirms the public surface
genoray/_vcf.py — VCF class: constructor, read, chunk, mode constants near the top of the class; get_record_info(contig=None, start=None, end=None, fields=None, info=None, lazy=False) — non-FORMAT record-level fields (including INFO) for a range or the whole file, returns pl.DataFrame (or pl.LazyFrame when lazy=True)
genoray/_pgen.py — PGEN class: constructor, read, chunk, read_ranges, chunk_ranges, mode constants near the top of the class
genoray/_svar.py — SparseVar: __init__, from_vcf, from_pgen, read_ranges, read_ranges_with_length(contig, starts=0, ends=POS_MAX, samples=None) (length-guaranteed range read; returns the same type as read_ranges — a Ragged or fields-augmented record), with_fields, annotate_mutations, mutation_matrix, assign_signatures, annotate_with_gtf(gtf, level_filter=1, write_back=True, *, strand_encoding=None, codon_null_token=None) (GTF CDS annotation entry point, returns pl.DataFrame with varID/gene_id/strand/codon_pos), cache_afs() (computes and persists an AF column to the .gvi index; returns None)
genoray/_svar2.py — SparseVar2: __init__(path, *, fields=None), with_fields(fields) (new reader over the same store with those fields selected), available_fields (dict[str, StoredField], set in __init__), (VCF/BCF → SVAR2 conversion entry point, classifies during the write, / extract scalar-numeric fields during the write; supports ///), (PLINK2 PGEN → SVAR2 conversion entry point; diploid-only, no //; stores per-sample dosage tracks as FORMAT fields, read from the hardcall itself () or a separate ; supports /// like ), (N single-sample VCFs/BCFs → one SVAR2 store via a native k-way merge; accepts a /directory/manifest, resolved by module-level ; / supported (no_reference skips left-alignment, so cross-file joins require pre-normalized inputs); / supported — INFO merges first-carrier-wins, FORMAT stays per-sample; supports // like , but — the cohort is the file set), (SVAR1 () → SVAR2 native migration entry point; reads no VCF/htslib, from SVAR1 metadata, biallelic SVAR1 only, no / (those are VCF-specific) — instead selects which SVAR1 fields carry through ( default = all, = none, a subset carries only those names, unknown name raises ); is never selectable this way and is always dropped; supports /// like /, though regions filter per-record rather than narrowing a covering range up front); /// metadata. Read/query methods live in the mixins: ( — attaches one per selected field, ), (public ; internal gvl-only //), and (, , — COSMIC mutational-signature workflow, mirroring 's but backed by a per-contig Rust sidecar instead of a -attached field)
When a signature, kwarg, or shape is unclear, read the docstring in the
source rather than reasoning from first principles.
Cross-cutting conventions
- Ranges are 0-based, half-open
[start, end).
max_mem accepts strings like "4g", "512m", "2GB" — except
SparseVar2.from_vcf, SparseVar2.from_pgen, and
SparseVar2.from_vcf_list's max_mem, all a whole-process planning
budget, not a per-chunk cap; see their entries under "Conversion" below
before assuming they mean the same thing as everywhere else this name
appears (VCF.chunk/chunk_ranges, PGEN.chunk/chunk_ranges, etc.,
where it caps one chunk directly).
- Contig names auto-normalize:
"chr1" and "1" both work regardless of file convention (ContigNormalizer).
- Missing genotype =
-1 (int). Missing dosage = np.nan (float32).
- Ploidy is 2 by default;
SparseVar.from_vcf/from_pgen (and genoray write-svar1) accept haploid=True / --haploid, which OR-collapses haplotypes into a single haploid call per sample and records ploidy=1 in metadata (intended for unphased somatic data).
- All return arrays are NumPy;
mode selects which arrays you get back.
Sample accessors — canonical name + why the idioms diverge
available_samples (a list[str]) is the canonical "all samples in the
file" accessor — present on all four readers (VCF, PGEN, SparseVar,
SparseVar2).
VCF and PGEN additionally expose:
current_samples — the currently-selected subset (read-only property).
set_samples(samples) -> Self — a stateful call that mutates the reader
in place to select a subset (or restore all samples with None), then
returns self.
SparseVar and SparseVar2 have no current_samples/set_samples.
Instead, every read method (read_ranges, read_ranges_with_length, etc.)
takes samples as a per-call samples= kwarg.
Why the two idioms differ (performance): subsetting samples on VCF/PGEN
is costly — it re-initializes the backend reader — so it's a deliberate,
stateful set_samples() call made once and reused across reads. On
SparseVar/SparseVar2, subsetting is ~free (it's just an index selection
over already-memory-mapped data), so it's exposed as a lightweight per-call
samples= kwarg instead of a persistent reader state. This is an
intentional divergence, not an inconsistency — don't "fix" one to match
the other.
Mode constants — gotcha
Modes are class attributes, not top-level names:
genoray.VCF.Genos8 # not genoray.Genos8
genoray.PGEN.GenosPhasingDosages
To discover the available modes for a class, read the class body in
_vcf.py / _pgen.py (search for Genos near the top).
When a mode bundles multiple arrays, the return tuple follows the order in
the constant name. PGEN.GenosPhasingDosages returns (genos, phasing, dosages); VCF.Genos8Dosages returns (genos, dosages).
VCF — quick reference
vcf = genoray.VCF(
"file.vcf.gz",
phasing=True, # constructor-time, not per-read
dosage_field="DS", # required to read dosages; FORMAT field with Number=A
filter=genoray.Filter(
record=lambda v: ..., # cyvcf2.Variant -> bool
expr=~genoray.exprs.is_symbolic, # matching .gvi index predicate
),
)
# Single range
arr = vcf.read("chr1", start=0, end=1_000_000, mode=genoray.VCF.Genos8)
# Chunked
for chunk in vcf.chunk("chr1", start=0, end=1_000_000,
max_mem="2g", mode=genoray.VCF.Genos8Dosages):
...
- Shape with
phasing=False: (samples, ploidy=2, variants).
- Shape with
phasing=True: (samples, ploidy+1=3, variants) — the 3rd row along the ploidy axis is 0 (unphased) / 1 (phased), matching cyvcf2.
- Dosage arrays drop the ploidy axis:
(samples, variants), dtype float32.
- VCF intentionally has no
read_ranges — benchmarking showed no throughput benefit.
read(out=...) is VCF-only — pass a pre-allocated array to fill in place. PGEN random-access reads allocate fresh and have no out= buffer.
PGEN — quick reference
pgen = genoray.PGEN(
"hardcalls.pgen", # hardcalls live in the main path
dosage_path="dosages.pgen", # optional; defaults to the main path
filter=genoray.exprs.is_snp & genoray.exprs.is_biallelic,
)
Important: when you have a dosage-only PGEN and a separate hardcalls PGEN,
hardcalls go in the main path and dosages go in dosage_path. If you
only pass one path, both hardcalls and dosages come from it (with the
hardcalls inferred from dosage threshold — see PLINK 2 docs).
A .gvi index file is created next to the PGEN on first construction.
Don't delete it.
# Single range
genos = pgen.read("chr2", start=0, end=1000)
# Multiple ranges in one call (PGEN-only optimization)
data, offsets = pgen.read_ranges(
"chr2",
starts=[0, 1000, 2000],
ends=[1000, 2000, 3000],
mode=genoray.PGEN.GenosPhasingDosages,
)
# `data` matches the mode (tuple when mode bundles multiple arrays)
# `offsets` shape: (n_ranges + 1,). Slice range i with: arr[..., offsets[i]:offsets[i+1]]
# Chunked variants of both
for chunk in pgen.chunk("chr2", 0, 1000, max_mem="4g"): ...
for range_iter in pgen.chunk_ranges("chr2", starts, ends, max_mem="4g"):
for chunk in range_iter: ...
Genotype dtype: int32. Dosage dtype: float32. Phasing is a separate
bool array of shape (samples, variants) — not an extra row in the
genotype array (unlike VCF with phasing=True).
SparseVar (.svar) — quick reference
Build:
# From a configured VCF reader
vcf = genoray.VCF("file.vcf.gz", dosage_field="DS")
genoray.SparseVar.from_vcf("out.svar", vcf, max_mem="4g",
with_dosages=True, overwrite=True)
# Or from a PGEN
genoray.SparseVar.from_pgen("out.svar", "file.pgen", max_mem="4g")
# Unphased somatic data: collapse to a single haploid call per sample (ploidy=1)
genoray.SparseVar.from_vcf("out.svar", vcf, max_mem="4g", haploid=True)
SparseVar.from_vcf / from_pgen inherit and apply the source's filter — filter the VCF/PGEN to filter the SVAR.
SparseVar.from_vcf / from_pgen accept regions=, samples=,
merge_overlapping=, regions_overlap= to subset by region and/or sample
during conversion (same semantics as SparseVar.write_view); a sample subset
drops MAC=0 variants from the output.
Read:
# Plain ragged: data is just variant indices
svar = genoray.SparseVar("out.svar")
ragged = svar.read_ranges("chr1", starts=[0, 50_000], ends=[10_000, 60_000],
samples=["S1", "S2"])
# shape: (ranges, samples, ploidy, ~variants) — last axis is ragged
# With extra fields attached
svar = genoray.SparseVar("out.svar", fields={"dosages": np.float32})
# or, on an existing instance:
svar_with = svar.with_fields({"dosages": np.float32})
result = svar_with.read_ranges("chr1", [0], [10_000])
result.genos # Ragged of variant indices (uint32)
result.dosages # Ragged of dosages (float32)
with_fields(False) drops all extras and returns a plain
Ragged[V_IDX_TYPE] again from subsequent reads.
Each leaf value in the ragged result is a variant index — a row number
into svar.index, a polars DataFrame with at least CHROM, POS, REF, ALT (list[str]), ILEN. To map indices back to chrom/pos/ref/alt, row-index
that DataFrame.
v_idxs = ragged[0, 0, 0].to_numpy()
rows = svar.index[v_idxs.tolist()].select("CHROM", "POS", "REF", "ALT")
svar.index.POS is 1-based (VCF convention), while query coordinates
are 0-based half-open. Don't conflate them.
SparseVar2 (.svar2) — quick reference
SparseVar2 is the next-gen sparse variant store (VariantKey-style inline
encoding + per-variant dense/sparse cost model). Two halves: conversion
(from_vcf, below) writes a store; range queries (decode / region_counts
/ read_ranges, further below) read it back. All coordinates are 0-based
half-open [start, end), as everywhere else in genoray.
Conversion
from genoray import SparseVar2
dropped = SparseVar2.from_vcf(
"out.svar2", "file.vcf.gz", "ref.fa", # reference: validates REF + left-aligns indels
overwrite=True,
)
# Pre-normalized input (e.g. `bcftools norm`'d): skip REF validation/left-align
dropped = SparseVar2.from_vcf("out.svar2", "file.vcf.gz", no_reference=True)
Signature: from_vcf(out, source, reference=None, *, regions=None, samples=None, merge_overlapping=False, regions_overlap="pos", no_reference=False, skip_out_of_scope=False, ploidy=2, chunk_size=25_000, threads=None, overwrite=False, long_allele_capacity=8*1024*1024, signatures=False, info_fields=None, format_fields=None, check_ref="e", progress=False, log_level="info", max_mem=None) -> int
-
source — a bgzipped VCF (.vcf.gz, or the equivalent .vcf.bgz spelling)
or BCF (.bcf). Auto-indexes (.csi) if no .csi/.tbi is found. For a PLINK2 PGEN source, use from_pgen instead
(below).
-
regions=/merge_overlapping=/regions_overlap= — restricts conversion
to one or more indexed VCF fetch intervals. Region strings use the existing
genoray convention ("chrom:start-end" is 1-based inclusive, converted to
0-based half-open; tuple/BED/frame inputs are already 0-based half-open).
Overlapping regions raise unless merge_overlapping=True. regions_overlap
picks one of three modes, matching bcftools --regions-overlap: "pos"
(default; POS inside [start,end)), "record" (POS in [start,end+1), so
an indel at the region's last base is kept), or "variant" (the
anchor-trimmed variant extent overlaps the region). In "variant" mode a
multiallelic record is kept whole if ANY of its alleles truly overlaps the
region; individual non-overlapping alleles are not dropped. "variant"
currently requires at most one region per contig; multiple regions per
contig raise — use "pos"/"record", or convert separately.
-
samples= — selects and reorders VCF samples by name: preserves caller
order, de-duplicates first occurrences, raises ValueError on an unknown
name. available_samples and every decoded column match the caller's order
exactly, regardless of each sample's original VCF header position.
-
Exactly one of reference (a FASTA path, used to validate REF and left-align
indels) or no_reference=True (trusts pre-normalized input, skips
validation/left-align) is required — passing both or neither raises
ValueError.
-
The reference= FASTA may use a different contig naming scheme than the
variant source (e.g. source chr1, FASTA 1, or either side's mito contig
spelled as M/MT//); genoray resolves the source's contig
names against the FASTA's own naming (-prefix and mito aliases
included) before validating REF/left-aligning. The output store keeps the
source's contig spelling regardless of the FASTA's.
Conversion from PGEN
from genoray import SparseVar2
dropped = SparseVar2.from_pgen(
"out.svar2", "file.pgen", "ref.fa", # reference: validates REF + left-aligns indels
overwrite=True,
)
Signature: from_pgen(out, source, reference=None, *, regions=None, samples=None, merge_overlapping=False, regions_overlap="pos", no_reference=False, skip_out_of_scope=False, chunk_size=None, max_mem=None, threads=None, overwrite=False, long_allele_capacity=8*1024*1024, signatures=False, dosages=None, check_ref="e", progress=False, log_level="info") -> int
-
source — a .pgen file. Variant metadata is read from the sibling
.pvar/.pvar.zst, sample names from the sibling .psam.
reference/no_reference, skip_out_of_scope, overwrite,
long_allele_capacity, signatures, and check_ref all mean the same as
from_vcf (above), and return the same int (dropped out-of-scope ALTs).
-
Unlike from_vcf, PGEN sub-contig sharding is disabled (single reader
per contig) and threads never changes a single output byte. Reason
(measured, chr21c ~1M variants x 3202 samples): single-reader conversion is
already fast (~33s) and bound by the shared executor/writer + reference I/O,
not by pgenlib decode -- so sharding cannot beat that floor and measured
as slower (44.9s at threads=24 vs 32.6s serial). Bumping pgenlib to a
GIL-releasing build (>=0.94.x, which parallelizes decode via prange) does
not help either: the conversion is flat at ~33s across OMP_NUM_THREADS
1..32, so decode parallelism buys nothing. The sharding machinery exists and
is byte-identical (validated to 1M variants) for re-enablement only if a
future change shifts the bottleneck onto decode.
-
Diploid only — no ploidy= kwarg (from_vcf's default ploidy=2 is
implicit and fixed here).
-
chunk_size=None — unlike from_vcf's fixed 25_000 default, None here
derives a variant-count budget from sample count (a packed dense chunk costs
chunk_size * n_samples * 2 / 8 bytes), so a fixed constant that's fine at
200 samples doesn't blow memory at 500k. Pass an explicit int to override.
Warns if the derived value falls below 256 variants — see
from_vcf_list's chunk_size entry below for the details. dosages
counts as n_format_fields here.
-
max_mem: int | str | None = None — byte budget for the concurrency
planner: how many contigs convert at once, chosen so cohort-baseline
memory plus each concurrent contig's in-flight chunk buffers fit inside it
(in addition to the existing core-count bound, also capped at 8 concurrent
contigs regardless of budget). Same string forms as the module-level
convention above (, , , parsed by
), and (above) — both pipelines have a fitted concurrency planner and
spend the budget on concurrency the same way, just with
separately-fitted RAM-law coefficients (a PGEN chunk decodes both
haplotypes at once, so its per-variant cost is higher). 's
means the same whole-process budget too, but that path has no
concurrency planner to spend it on (its contigs run strictly
sequentially), so it derives its own per-chunk from the
budget instead — see its entry below. This is a deliberate default behavior
change from the pre- planner. If detection itself fails (no
cgroup limit and no readable — always true on macOS),
genoray warns and falls back to the old core-bound-only planning rather
than raising. Pass an explicit value to raise or lower the budget, or a
very large value to approximate unbounded planning.
the planner's RAM law has a fixed cohort-baseline term of roughly 2.7 GB
(PGEN's own fitted coefficients, higher than 's ~457 MB), so any
budget that can't cover baseline plus one concurrent contig's chunk
buffers is rejected with , even for a tiny cohort. That
baseline scales with cohort size (), so this isn't just
a small-cohort concern: at ~500k samples it alone predicts ~10.6 GB, so a
budget on a smaller host will reject the conversion — pass an
explicit sized to the host in that case.
Conversion from a list of single-sample VCFs
from genoray import SparseVar2
# Explicit list
dropped = SparseVar2.from_vcf_list("out.svar2", ["s1.vcf.gz", "s2.bcf"], "ref.fa")
# A directory of single-sample files
# (non-recursive: all *.vcf.gz/*.vcf.bgz, then all *.bcf)
dropped = SparseVar2.from_vcf_list("out.svar2", "vcfs/", "ref.fa")
# A manifest file (one path per line; blank/`#`-comment lines skipped;
# relative entries resolved against the manifest's directory)
dropped = SparseVar2.from_vcf_list("out.svar2", "manifest.txt", "ref.fa")
Signature: from_vcf_list(out, sources, reference=None, *, regions=None, merge_overlapping=False, regions_overlap="pos", no_reference=False, skip_out_of_scope=False, ploidy=2, chunk_size=None, max_mem=None, threads=None, overwrite=False, long_allele_capacity=8*1024*1024, signatures=False, info_fields=None, format_fields=None, check_ref="e", progress=False, log_level="info") -> int
Builds one SVAR2 store from N single-sample VCFs/BCFs with different
site lists, via a native k-way merge — no bcftools merge, no intermediate
multi-sample VCF.
-
regions=/merge_overlapping=/regions_overlap= — same convention,
semantics, and three overlap modes ("pos"/"record"/"variant") as
from_vcf, applied identically to every input file in the merge. As with
from_vcf, "variant" mode keeps a multiallelic record whole if ANY of its
alleles truly overlaps the region.
-
No samples= parameter — unlike from_vcf/from_pgen/from_svar1,
from_vcf_list has no cohort to subset by name: each input file is already
single-sample, and the cohort is exactly the file set passed via sources.
-
Each input file must be single-sample — exactly one sample column;
ValueError if any file has zero or more than one. That sample's VCF
header name becomes its sample name in the store; duplicate sample names
across input files raise ValueError.
-
sources — one of three forms, resolved by module-level
_resolve_vcf_sources:
- a
Sequence[str | Path] — explicit files, in the given order.
- a single directory
Path — every bgzipped VCF (*.vcf.gz/*.vcf.bgz)
then every *.bcf directly inside it (non-recursive), each group
natsort-ordered.
- a single file
Path — .vcf.gz/.vcf.bgz/.bcf is taken as one file; anything
else is a manifest (one path per line, blank/#-comment lines skipped,
relative entries resolved against the manifest's parent directory).
- Resolving to zero files raises
ValueError.
-
Absent site → hom-ref 0. A site called in file A but not present at
all in file B fills 0 (hom-ref) for B's sample at that site.
-
A within-file ./. is not observable after the merge. SVAR2's sparse
layout stores only ALT-carrying entries, so a missing hap and a hom-ref hap
both produce zero entries and cannot be told apart via or
. The missing sentinel is a dense
/ convention and is part of SVAR2's
decode. (The distinction is real inside the merge, but it is discarded when
genotypes are packed into the sparse carrier bit-grid — this matches
, so the two paths stay in parity.)
Conversion from SVAR1
from genoray import SparseVar2
dropped = SparseVar2.from_svar1(
"out.svar2", "old.svar", "ref.fa", # reference: validates REF + left-aligns indels
overwrite=True,
)
Signature: from_svar1(out, source, reference=None, *, regions=None, samples=None, merge_overlapping=False, regions_overlap="pos", no_reference=False, skip_out_of_scope=False, chunk_size=None, threads=None, overwrite=False, long_allele_capacity=8*1024*1024, signatures=False, fields=None, check_ref="e", progress=False, log_level="info") -> int
Migrates an existing SVAR 1.0 (SparseVar) store to SVAR2 natively — reads no
VCF and no htslib; SVAR1 is already sparse, so this reconstructs variant
records from SVAR1's arrays and reuses the same conversion spine as from_vcf.
source — a SparseVar store directory (SVAR1). reference/no_reference,
skip_out_of_scope, overwrite, long_allele_capacity, signatures, and
check_ref all mean the same as from_vcf (above), and return the same
int (dropped out-of-scope ALTs).
ploidy is read from SVAR1's metadata — no ploidy= kwarg.
chunk_size=None derives a variant-count budget from cohort size the same
way as from_pgen/from_vcf_list (_auto_chunk_size) and warns under the
same below-256-variant condition — see from_vcf_list's chunk_size entry
above for the details. Known gap: this call site always passes
n_format_fields=0, even though fields= (below) selects SVAR1 FORMAT
fields and defaults to carrying all of them — so unlike from_pgen, the
budget here does not account for staged FORMAT bytes and can under-size
the chunk when fields= carries a wide FORMAT set. Tracked in
#157.
- Biallelic SVAR1 only — raises
ValueError if the source store has
multiallelic variants (SVAR1's geno==1 model); re-create the SVAR1 store
biallelically first.
regions=/merge_overlapping=/regions_overlap= — same convention,
semantics, and three overlap modes ("pos"/"record"/"variant") as
from_vcf/from_pgen. "variant" mode keeps a record whole if ANY of its
alleles truly overlaps the region (though SVAR1 is itself biallelic-only, so
this only ever judges a single ALT). Unlike from_pgen, SVAR1 has no
on-disk covering-range index to narrow against up front — a selected
contig's local variants are still scanned in full; the per-record filter is
what actually restricts the output, so this costs a full-contig scan rather
than a range-restricted one.
samples= — selects and reorders SVAR1 samples by name (same convention
as /): preserves caller order, de-duplicates first
occurrences, raises on an unknown name. and
every decoded column match the caller's order exactly, regardless of each
sample's original SVAR1 position.
Range queries
Open a finished store, then query per contig. Construction reads meta.json and
opens one native reader per contig, exposing .available_samples (list; the
canonical sample-name accessor shared with VCF/PGEN/SparseVar),
.n_samples, .contigs, .ploidy, .format_version.
from genoray import SparseVar2
sv = SparseVar2("out.svar2")
regions = [(0, 40), (1_000, 2_000)] # 0-based half-open [start, end)
# Analysis path — decode to a seqpro Ragged record (one call per contig)
rag = sv.decode("chr1", regions) # fields pos (i32), ilen (i32), allele (ALT bytes)
# + one per selected field (see "Reading
# INFO/FORMAT fields" below); shape
# (R, S, P, None); pure-DEL ALT is empty
# Decode-free per-(region, sample, ploid) variant count — replaces SVAR 1.0's var_ranges
counts = sv.region_counts("chr1", regions) # np.ndarray, shape (R, S, P)
decode(contig, regions) returns a seqpro.rag.Ragged whose layout is
byte-identical to gvl's RaggedVariants (pos/ilen numeric,
allele opaque-string ALT, one shared variant-axis offsets object). ALT is
empty for a pure deletion (the reference base is not re-emitted). Requires
seqpro.
region_counts(contig, regions) is the decode-free count (offset diffs +
dense-mask popcount) — the simplified stand-in for SparseVar.var_ranges
(SVAR2 has no unified variant table, so variant indices no longer exist).
- Queries are per contig — cross-contig batching is the caller's job. Regions
are an iterable of
(start, end) pairs.
- The
contig argument to decode/region_counts/read_ranges accepts
alternate naming schemes — chr-prefixed vs unprefixed (chr1 ↔ 1) and
the mitochondrial aliases {M, MT, chrM, chrMT} — resolved via
ContigNormalizer to the store's own spelling. An unresolvable contig
raises ValueError.
The user-facing SVAR2 query API is decode / region_counts / read_ranges
(above). read_ranges(contig, starts, ends, samples=None) is a fused
search+gather; starts/ends are parallel 1D arrays (mirrors
SparseVar.read_ranges), samples selects/reorders a subset by name. It
returns the raw two-channel BatchResult → numpy dict, a TypedDict with a
fixed field set: vk_pos/vk_key/vk_off, dense_pos/dense_key/
dense_range/dense_present/dense_present_off, lut_bytes/lut_off, and
scalars n_regions/n_samples/ploidy.
SparseVar2 also has _overlap_batch/_find_ranges/_gather_ranges
(underscore-prefixed) — an internal, gvl-only numpy-dict wire contract for
the search/gather split used by a write-time overlap cache. They are not
part of the public API, are not covered by semver, and may change or
disappear without notice; don't call them from user code.
Reading INFO/FORMAT fields (SVAR2)
Fields written by from_vcf(info_fields=…, format_fields=…) (above) are read
back by opting in — they are not decoded by default (each one costs extra
I/O).
sv = SparseVar2("out.svar2")
sv.available_fields # {"AF": StoredField(...), "DS": StoredField(...)}
sv = sv.with_fields(["AF", "DS"]) # or SparseVar2("out.svar2", fields=["AF", "DS"])
rag = sv.decode("chr1", [(0, 10_000)])
rag["AF"] # Ragged, sharing offsets with pos/ilen/allele
SparseVar2(path, *, fields=None) / .with_fields(fields) — fields is a
Sequence[str] of canonical keys (see available_fields below).
with_fields returns a new SparseVar2 over the same store; it does
not mutate the original in place. fields=None (the constructor default)
selects nothing — fields are opt-in.
available_fields -> dict[str, StoredField] — every field declared in the
store's meta.json, keyed canonically: the bare field name when it is
unique across INFO and FORMAT, else bcftools-style INFO/DP / FORMAT/DP
when a name is used by both categories. StoredField (defined in
genoray._svar2_fields, not exported at top-level genoray) is a frozen
dataclass: name, category ("info"/"format"), dtype (np.dtype),
default (float | None), key.
decode(contig, regions) attaches one Ragged per selected field to the
returned record Ragged, alongside pos/ilen/allele — every one
sharing a single variant-axis offsets object, shape (R, S, P, None).
Access a field's data via rag["KEY"] (Ragged.__getitem__), not
rag.fields["KEY"] — Ragged.fields is just the list[str] of field
names on the record.
- Dtype is preserved as stored. SVAR2 losslessly auto-narrows integer
fields at write time, so e.g. an
AC field may come back as int8;
nothing is widened on read.
- Missing values are the field's
default if one was set at write time,
else a reserved sentinel (NaN for floats, iinfo.min/iinfo.max for
ints) — returned as-is, never translated.
- FORMAT fields are genotype-aligned (see
from_vcf's
above) — only ever emits carrier records, so the "non-carrier
values aren't stored" caveat from the write path is invisible on this
read surface.
Mutational signatures (SBS96 / DBS78 / ID83)
Same COSMIC workflow as SparseVar (see "Mutation catalogues" above), backed
by a Rust per-contig sidecar instead of a .gvi-attached field. Annotation is
required before mutation_matrix — either post-hoc, or by passing
signatures=True to from_vcf (above):
sv = SparseVar2("out.svar2")
ref = genoray.Reference.from_path("hg38.fa")
sv.annotate_mutations(ref) # post-hoc; writes the mutcat sidecar
sv.annotate_mutations(ref, contigs=["chr1"]) # restrict to a subset of contigs
df = sv.mutation_matrix("SBS96") # count="allele" (default)
df = sv.mutation_matrix("DBS78", count="sample")
act = sv.assign_signatures("SBS96") # mutation_matrix + fit_signatures
annotate_mutations(reference, *, gtf=None, contigs=None) -> None —
reference is a genoray.Reference or a FASTA path; contigs=None
(default) annotates every contig. contigs= accepts alternate naming —
chr-prefixed vs unprefixed and the mitochondrial aliases
{M, MT, chrM, chrMT} — resolved via ContigNormalizer to the store's own
spelling; raises ValueError if every requested contig fails to resolve.
Unlike SparseVar.annotate_mutations, there is no write_back= toggle
— SVAR2 always persists the sidecar to disk. gtf= optionally supplies a
GTF/GFF gene model path; when given, each
SNV is additionally classified by transcriptional-strand class (from
feature == "gene" footprints) and persisted to a strand.bin sidecar,
which unlocks the "SBS192"/"SBS384" catalogs below.
mutation_matrix(kind, *, count="allele"|"sample") -> pl.DataFrame — a
MutationType column (fixed COSMIC codebook order) plus one column per
sample. kind ∈ {"SBS96", "DBS78", "ID83", "SBS192", "SBS384"}.
count="allele" counts every non-ref allele copy; count="sample" counts
each category at most once per sample, OR-combined across contigs. Raises
ValueError if called before the store is annotated (no on-disk sidecar
for every contig) — annotate first, either via annotate_mutations or
from_vcf(..., signatures=True). "SBS192"/"SBS384" additionally require
strand annotation (annotate_mutations(..., gtf=...)) and raise
ValueError if the store lacks it. assign_signatures does not accept
"SBS192"/"SBS384" — see below.
assign_signatures(kind, *, reference=None, count="allele", max_delta=0.01, min_activity=0.005, n_jobs=1, backend="loky") -> pl.DataFrame
— mutation_matrix(kind, count=...) then genoray.fit_signatures(...).
reference accepts a pl.DataFrame, a TSV path, or None (defaults to
).
Strand-resolved catalogs (SBS192 / SBS384)
SparseVar2 also supports the transcriptional-strand-bias catalogs, which
require a gene model (GTF) at annotation time:
sv2.annotate_mutations(reference, gtf="gencode.v45.annotation.gtf.gz")
sbs384 = sv2.mutation_matrix("SBS384") # 384 rows: [T, U, N, B] x 96
sbs192 = sv2.mutation_matrix("SBS192") # 192 rows: the {T, U} sub-view = SBS384[:192]
- SBS384 = 96 trinucleotide channels x 4 strand categories, SigProfiler
order
[T, U, N, B]: Transcribed, Untranscribed, Nontranscribed
(intergenic), Bidirectional (position covered by genes on both strands).
- SBS192 is the
{T, U} sub-view (SBS384[:192]).
- Strand rule (pyrimidine-folded): a genic SNV is Untranscribed iff the
pyrimidine of its ref/alt pair sits on the gene's coding strand, else
Transcribed. Gene footprints come from
feature == "gene" rows (full gene
body); pre-filter the GTF to restrict biotypes.
- Without a
gtf=, mutation_matrix("SBS192"/"SBS384") raises. Write-time
from_vcf(..., signatures=True) stays strand-free; obtain strand catalogs via
a post-hoc annotate_mutations(reference, gtf=...).
assign_signatures("SBS192"/"SBS384") raises NotImplementedError: COSMIC
publishes no strand-resolved reference set. Use mutation_matrix for
strand-bias analysis.
Merge and split by contig
SVAR2 contigs are fully independent on disk, so recombining or subsetting
whole contigs is a cheap metadata-rewrite + file-copy operation — unlike
write_view (see the CLI section below), none of these methods re-run
conversion or the var_key/dense cost model.
from genoray import SparseVar2
sv = SparseVar2("out.svar2")
sv.subset_contigs("chr1.svar2", "chr1") # single contig
sv.subset_contigs("subset.svar2", ["chr1", "chr2"]) # multiple, source order preserved
paths = sv.split_by_contig("by_contig/") # one store per contig, out_dir/{contig}.svar2
SparseVar2.concat("merged.svar2", ["chr1.svar2", "chr2.svar2"]) # disjoint-contig merge
subset_contigs(output, contigs, *, mode="copy", overwrite=False) -> None —
write a new store containing only contigs (a single contig name or a
sequence of names). contigs accepts alternate naming — chr-prefixed vs
unprefixed and the mitochondrial aliases {M, MT, chrM, chrMT} — resolved
via ContigNormalizer to the store's own spelling. Pure metadata rewrite +
file copy of the kept contig directories, preserving the source store's
contig order. Raises ValueError if any name is unresolvable against
self.contigs, or if output resolves to this store's own path (in-place
subsetting is rejected, mirroring write_view's in-place guard). Raises
FileExistsError if output exists and overwrite=False.
split_by_contig(out_dir, *, mode="copy", overwrite=False) -> list[Path] —
explode into one single-contig store per contig at
out_dir/{contig}.svar2; returns the output paths in self.contigs
order. Implemented as one subset_contigs call per contig.
SparseVar2.concat(output, sources, *, mode="copy", overwrite=False) -> None
(classmethod) — concatenate stores with disjoint contig sets into one.
sources is a sequence of paths (or SparseVar2 instances); all sources
must agree on samples, ploidy, format_version, and fields —
disagreement on any of those, or a contig name appearing in more than one
source, raises ValueError. The merged contig list is natsorted,
independent of the order sources were passed in.
mode (all three methods) is the shared Mode literal —
"copy"|"hardlink"|"symlink"|"move" — controlling how each contig
directory is transplanted into the output store.
Errors
genoray raises standard Python builtins, by category:
ValueError — bad input content: contig/sample not found, REF disagrees with
the reference FASTA, or a symbolic/breakend ALT with skip_out_of_scope=False.
FileNotFoundError — a required input file is missing.
OSError — a corrupt/truncated store sidecar or an underlying disk I/O failure.
RuntimeError — an internal genoray bug (a worker thread panicked); please
report it.
CLI
genoray write has three subcommands — write vcf, write pgen, write svar1 — and all three target SVAR2. There is no bare auto-detecting
genoray write SOURCE OUT anymore; you must name the source kind. The
previous SVAR 1.0 (SparseVar) write path lives at the top-level
genoray write-svar1 command (hyphenated, not a write subcommand) — it
takes a VCF or PGEN source, same as before. genoray view still defaults
to SVAR2, with the previous SVAR 1.0 behavior under view svar1. genoray concat/genoray split are SVAR2-only (no SVAR1 equivalent).
genoray write vcf / genoray write pgen / genoray write svar1
# write vcf — VCF/BCF (or a directory/manifest of single-sample VCFs/BCFs) → SVAR2
genoray write vcf file.vcf.gz out.svar2 --reference ref.fa
genoray write vcf file.vcf.gz out.svar2 --no-reference
genoray write vcf file.vcf.gz out.svar2 --reference ref.fa --skip-symbolics-and-breakends --threads 4
genoray write vcf file.vcf.gz out.svar2 --reference ref.fa --fields INFO/AF --fields FORMAT/DP
genoray write vcf vcf_dir/ out.svar2 --no-reference --regions chr1:1-1000 # vcf-list form
# write pgen — PLINK2 PGEN → SVAR2 (no --ploidy; PGEN is diploid-only)
genoray write pgen file.pgen out.svar2 --reference ref.fa
genoray write pgen file.pgen out.svar2 --no-reference --regions chr1:1-1000 --samples A,B
genoray write pgen file.pgen out.svar2 --reference ref.fa --dosages DS=self
genoray write pgen hardcalls.pgen out.svar2 --reference ref.fa --dosages VAF=vaf.pgen
# write svar1 — SVAR1 (SparseVar) → SVAR2
genoray write svar1 store.svar out.svar2 --no-reference --samples A,B
genoray write svar1 store.svar out.svar2 --no-reference --fields dosages
genoray write svar1 store.svar out.svar2 --no-reference --empty-fields
# write-svar1 (legacy, top-level) — VCF or PGEN → SVAR 1.0, dosages, --haploid, --max-mem
genoray write-svar1 file.vcf.gz out.svar --max-mem 4g --haploid
All three write subcommands share --regions/-r, --regions-file/-R,
--samples/-s, --samples-file/-S, --merge-overlapping,
--regions-overlap (pos/record/variant), --reference XOR
--no-reference (required), --chunk-size, --threads/-@, --overwrite,
--long-allele-capacity (advanced), a single --skip-symbolics-and-breakends
flag (maps to skip_out_of_scope=; the SVAR2 core can't expand either
symbolic ALTs (<DEL>, <INS>, …) or breakends into nucleotides, so they're
dropped together and print a Dropped {n} out-of-scope (symbolic/breakend) ALT alleles. line when set), --check-ref {e,x} (default e, ignored with
--no-reference; e aborts on the first REF/FASTA disagreement, x drops
the offending record and continues — mirrors bcftools norm --check-ref), and
--progress/--no-progress + --log-level {off,warning,info,debug} (map to
progress=/log_level=; see "Conversion" above for behavior — default
--no-progress --log-level info). write vcf's vcf-list form forwards both
to from_vcf_list; its single-file form forwards both to from_vcf.
genoray write vcf (SparseVar2.from_vcf/from_vcf_list): source is a
single .vcf.gz/.vcf.bgz/.bcf → from_vcf; anything else (a directory,
or a file that isn't .vcf.gz/.vcf.bgz/.bcf) → the vcf-list form (a directory of
single-sample VCFs/BCFs, or a manifest listing them) → from_vcf_list — a
.svar (SVAR1) source belongs under write svar1 instead, not here.
--samples/--samples-file work only for the single-file form — they
raise for the vcf-list form (each input file already contributes exactly one sample, so
there's no cohort to subset). --fields (-f, repeatable) takes
bcftools-style INFO/x/FORMAT/x/FMT/x specs, parsed by
_parse_cli_field_specs and forwarded as info_fields=/format_fields=;
defaults to unset (no fields carried, genotypes only). --chunk-size
defaults to 25000. --ploidy (default 2) is accepted here.
genoray write pgen (SparseVar2.from_pgen): source is a .pgen. No
--ploidy (PGEN is diploid-only). --dosages (repeatable) takes
NAME=self (read dosage from source itself) or NAME=/path/to/vaf.pgen
(read from a separate PGEN), each becoming a DosageField(name=NAME, source=...) passed as dosages=. --chunk-size defaults to a
memory-derived value (None). --max-mem (default None = a DETECTED
budget, not unbounded) is the same whole-process concurrency-planner
budget as from_pgen(max_mem=) (above).
genoray write svar1 (SparseVar2.from_svar1): source is a *.svar
(SVAR1) directory. --fields (repeatable) selects which SVAR1 FORMAT
fields carry through (default: all); overrides
to carry none. defaults to a memory-derived value ().
genoray view
# SVAR2 (default) — thin CLI over SparseVar2.write_view
genoray view in.svar2 out.svar2 -r chr1:1-1000 -s A,B
genoray view in.svar2 out.svar2 -r chr1:1-1000 # all samples
genoray view in.svar2 out.svar2 -s A,B # all variants (one region per contig)
genoray view in.svar2 out.svar2 -r chr1:1-1000 --no-reroute # representation-preserving, low-memory view
genoray view in.svar2 out.svar2 -r chr1:1-1000 --reroute # force the size-optimal re-route
# SVAR 1.0 (previous default) — unchanged SparseVar.write_view CLI
genoray view svar1 in.svar out.svar -r chr1:1-1000 -s A,B --progress
Both subcommands share the same -r/--regions, -R/--regions-file,
-s/--samples, -S/--samples-file, -f/--fields, --merge-overlapping,
--regions-overlap, --overwrite, -@/--threads, --progress/
--no-progress options and the same no-op guard (at least one of
regions/samples is required) and mutex checks (--regions/--regions-file
and --samples/--samples-file are each mutually exclusive). genoray view
(SVAR2) additionally has --log-level {off,warning,info,debug}; genoray view svar1 does not — its SparseVar.write_view backend has no
log_level= kwarg.
-
genoray view (SVAR2, thin wrapper over SparseVar2.write_view): when
--regions/--regions-file is omitted, "all variants" defaults to one
region per contig (SparseVar2.contigs, since SVAR2 has no contig-length
metadata) spanning [0, 2**31 - 1) — every real POS is smaller. --fields
defaults to None, meaning no fields are carried through (genotypes
only) — this always succeeds, even on a store that has INFO/FORMAT fields.
Both --reroute and --no-reroute go through the same slicer backend and
carry --fields/--reference identically — there is no longer a
fields-carrying vs. genotypes-only split between them:
--reroute reruns the var_key/dense routing cost model over the subset —
size-optimal (each variant re-routed to whichever representation is
smaller for the subset's sample/carrier counts).
--no-reroute (reroute=False) slices each variant's existing
on-disk representation directly (no cost model, byte-level slice) —
representation-preserving regardless of the subset's sample/carrier
counts. Recommended for somatic/all-rare cohorts (nearly every variant
is already var_key-routed) or memory-constrained runs.
- Omitting both flags (the default) is
"auto": resolves to
--no-reroute's behavior when any FORMAT field is carried, to
--reroute's otherwise. WHY: a dense→var_key flip stores one value per
carrier call and has no slot for a non-carrier sample's FORMAT value,
so re-routing a source-dense variant under a FORMAT-carrying view would
silently drop it — "auto" prefers fidelity whenever FORMAT is in play
and takes the size-optimal re-route otherwise (genotype-only / INFO-only
views have no per-sample slot to lose).
Both --reference (recomputes mutcat from scratch on the subset) and
-@/--threads (caps contigs sliced concurrently; autodetected when
omitted) are real on both --reroute and --no-reroute — there is no
longer an "accepted but ignored/unused" caveat on either path. --progress
and --log-level are both real here — see "write_view progress bar"
below for the coarse, one-line-per-contig rendering and log-level
semantics. write_view's underlying kwarg only accepts
, , or — any other value (e.g. ) raises
rather than silently falling through to the
slicer.
genoray concat / genoray split
genoray concat merged.svar2 part1.svar2 part2.svar2 # disjoint-contig merge
genoray split in.svar2 out_dir/ # explode into out_dir/{contig}.svar2
genoray split in.svar2 subset.svar2 --contigs chr1,chr2 # subset into one store
Both accept --mode (Literal["copy", "hardlink", "symlink", "move"], default
"copy" — see SparseVar2.concat/split_by_contig/subset_contigs
docstrings) and --overwrite.
Filtering
VCF: pass a genoray.Filter(record=, expr=) value object to filter=.
record is a Callable[[cyvcf2.Variant], bool] applied during the genotype
scan; expr is the matching polars pl.Expr applied to the .gvi index —
VCF requires both halves, bundled together so they can never diverge.
To change a VCF's filter after construction, assign a Filter (or None to
clear it) to the vcf.filter setter; the in-memory index is invalidated.
The getter returns the Filter | None currently in effect, so vcf.filter = vcf.filter round-trips.
from genoray import VCF, Filter
vcf = VCF("file.vcf", filter=Filter(
record=lambda v: not v.INFO.get("SVTYPE"), # cyvcf2 record predicate
expr=~genoray.exprs.is_symbolic, # matching .gvi index predicate
))
vcf.filter = None # clear
f = vcf.filter # -> Filter | None
The former two-argument constructor (a separate polars-expression keyword
argument alongside filter=) and its tuple-valued vcf.filter getter/setter
are removed in 3.0.0 — migrate any code passing the record predicate and
polars expression separately to the single Filter(record=, expr=) object
shown above.
PGEN: pass a polars pl.Expr returning a boolean mask, operating on the
.gvi index columns. Built-in expressions in genoray.exprs (the
complete list):
is_snp (True if all ALT alleles have ILEN == 0; rows with any null ILEN → False)
is_indel (True if all ALT alleles have ILEN != 0; rows with any null ILEN → False)
is_biallelic
is_symbolic (True if any ALT is a VCF 4.x symbolic allele, i.e. starts with <)
is_breakend (True if any ALT is a VCF 4.x breakend in mate-pair / single-breakend notation, e.g. G[chr2:321[, ]chr2:321]G, .TGCA, TGCA.. A distinct ALT class from symbolic alleles — is_symbolic does not flag breakends)
is_imprecise (True if any ALT's ILEN is null — an un-sizable symbolic allele or a breakend)
ILEN (a List[Int32] expression — one value per ALT allele, not a boolean)
ILEN semantics for symbolic SVs. For precise <DEL>/<INS>/<DUP>,
ILEN is computed at index-build time from INFO fields: -|SVLEN| for <DEL>,
+|SVLEN| for <INS>/<DUP> (falls back to |END - POS| when SVLEN is absent).
For VCF, INFO fields are read from header-declared columns (via oxbow); for PGEN,
they are parsed from the PVAR INFO string. Non-symbolic ALTs use the literal
len(ALT) - len(REF).
Un-sizable symbolic alleles carry null ILEN. An allele is un-sizable when:
the IMPRECISE INFO flag is set, SVLEN/END are both missing, the symbolic
type is unsupported (<BND>, <CNV>, <INV>, <*>/<NON_REF>), or the ALT is
a breakend in mate-pair / single-breakend notation (e.g. G[chr2:321[). At NumPy
materialization, null ILEN is coerced to 0 (treated as a point variant).
Filtering guidance (use filter= — a bare pl.Expr for PGEN, a
genoray.Filter for VCF):
-
~genoray.exprs.is_symbolic — drops all symbolic alleles (precise or not).
Required for haplotype consumers (e.g. genvarloader) that cannot expand any
symbolic ALT into literal sequence:
# PGEN
pgen = genoray.PGEN("file.pgen", filter=~genoray.exprs.is_symbolic)
# VCF (both halves required, bundled in a Filter)
vcf = genoray.VCF(
"file.vcf.gz",
filter=genoray.Filter(
record=lambda rec: not any(a.startswith("<") for a in rec.ALT),
expr=~genoray.exprs.is_symbolic,
),
)
-
~genoray.exprs.is_imprecise — keeps precise symbolic SVs (correctly
sized/spanned) and drops only the un-sizable ones (including breakends, which
are always un-sizable). Suitable for range/overlap queries where precise SVs
are queryable:
pgen = genoray.PGEN("file.pgen", filter=~genoray.exprs.is_imprecise)
-
For haplotype consumers, drop all un-expandable ALTs (symbolic and
breakends) — breakends are not caught by ~is_symbolic:
hap_safe = ~genoray.exprs.is_symbolic & ~genoray.exprs.is_breakend
pgen = genoray.PGEN("file.pgen", filter=hap_safe)
For anything else, write pl.col(...) against the .gvi schema — read
genoray/exprs.py for the available columns. Combining two exprs
expressions with & / | works without importing polars; you only need
import polars as pl to build custom predicates.
Reference — quick reference
genoray.Reference is a pysam-backed indexed-FASTA reader used to supply
flanking context for mutation-catalogue classification.
ref = genoray.Reference.from_path("hg38.fa") # auto-creates .fai if absent
ref = genoray.Reference.from_path("hg38.fa", contigs=["chr1", "chr2"])
seq: np.ndarray = ref.fetch("chr1", start=1_000_000, end=1_000_010)
# returns uint8 NDArray, 0-based half-open [start, end)
# bytes(seq) gives the ASCII sequence
Key properties:
from_path(fasta, contigs=None) — fasta is a str | Path; auto-calls pysam.faidx if the .fai index is missing. contigs filters which contigs the caller cares about (defaults to all in the FASTA).
fetch(contig, start, end) — 0-based half-open [start, end). Positions outside the contig are N-padded. Returns NDArray[np.uint8].
contig_array(contig) — the full contig sequence as a cached NDArray[np.uint8]. Shares the one-contig-in-memory cache with fetch. Accepts chr-prefixed or unprefixed names.
- Contig-name agnostic:
"chr1" and "1" both resolve correctly (ContigNormalizer under the hood).
- One contig is cached in memory at a time; sequential per-contig access is efficient.
Mutation catalogues (SBS-96 / DBS-78 / ID-83)
write_view progress bar
SparseVar.write_view(..., progress=False) accepts an opt-in progress keyword.
When True, a phase-level rich progress bar is shown while the view is written
(one tick per major step: counting, genotypes, each carried field, the index
build, and mutation annotation when reference= is given). It defaults to
False — no bar and no overhead — so library and pipeline callers are
unaffected. The genoray view svar1 CLI exposes the same option as --progress
(also default off):
genoray view svar1 in.svar out.svar -r chr1:1-1000 -s A,B --progress
The bar is cosmetic: output bytes, schema, and dtypes are identical whether or
not it is enabled.
SparseVar2.write_view(..., progress=False, log_level="info") renders live
write progress the same way the from_* writers do (see "Conversion" above),
with one difference: unlike the from_* writers, write_view has no
per-record stream to sample from, so its progress is COARSE — one line per