Skip to main content

rfmix_reader

Tests Documentation DOI

Fast, memory-light access to local-ancestry output — RFMix (.msp.tsv, .fb.tsv), FLARE (.anc.vcf.gz) and haptools simulations — as one lazily-evaluated xarray.Dataset backed by a per-chromosome Zarr cache.

  • One streaming pass over the source (constant memory), then instant reopen.
  • Haplotype ancestry codes stored as int8 (2 bytes per sample-locus, a few MB per chromosome on disk); posteriors kept on request.
  • The same Dataset for every format, with position queries, BED intervals, Parquet export, interpolation onto a variant grid, and gnomix-style phasing.

Installation

pip install rfmix-reader                 # core: numpy, pandas, dask, xarray, zarr, cyvcf2
pip install "rfmix-reader[viz,io]"       # + matplotlib/seaborn/cairosvg, pyarrow
Extra Adds Needed for
viz matplotlib, seaborn, cairosvg plot_*, ds.la.to_tagore
io pyarrow ds.la.to_parquet
reference bio2zarr prepare-reference (VCF to VCF-Zarr for method="reference" phasing)
gpu torch, cupy-cuda12x, cudf-cu12, dask-cudf-cu12 optional CuPy compute backend
all everything above

Parsing is CPU-only by design (it is I/O bound). When CuPy is importable the imputation and plotting helpers use it as the array backend (rfmix_reader.backends.use_gpu()).

Quickstart

from rfmix_reader import open_rfmix, open_local_ancestry

# parse once (one streaming pass), cache as la_cache/<chrom>.zarr
ds = open_rfmix("two_pops/out/", cache_dir="la_cache/")

# later sessions: instant, lazy
ds = open_local_ancestry("la_cache/")
ds = open_local_ancestry("la_cache/", chrom="21")

ds.la.counts            # (variant, sample, ancestry) int8 diploid counts, lazy dask
ds.la.haplotypes        # (variant, sample, ploidy) int8 ancestry codes
ds.la.global_ancestry   # DataFrame: sample_id, <ancestries>, chrom
ds.la.samples, ds.la.ancestries, ds.la.chromosomes
rfmix-reader convert fb two_pops/out/ la_cache/ --keep-posteriors
rfmix-reader info la_cache/

Readers

Source Call Notes
RFMix .msp.tsv (+ .rfmix.Q) open_rfmix(path) default; segment-level hard calls, segment_end coordinate
RFMix .fb.tsv (+ .rfmix.Q) open_rfmix(path, source="fb", keep_posteriors=True) one pass over the text; hard calls are the per-haplotype argmax, posteriors optional
FLARE .anc.vcf.gz (+ .global.anc.gz) open_flare(path) AN1/AN2 codes in ##ANCESTRY order
haptools simgenotype --pop_field open_simu(path) tabix regions in parallel; sorted labels (haptools defines no order)
existing cache open_local_ancestry(cache_dir, chrom=None) concatenates <chrom>.zarr stores
write cache only convert(path, fmt, cache_dir) fmt in msp, fb, flare, haptools

path may be a directory, a single file, or a path prefix (/out/run_ matches /out/run_chr1.msp.tsv, ...). chrom="21" restricts to one chromosome. Without cache_dir the Dataset is built in memory, which is fine for .msp.tsv; for .fb.tsv with posteriors use a cache (a chr1-sized cohort of 500 samples is ~11 GB of float32 posteriors).

The Dataset

dims:   variant, sample, ploidy (=2), ancestry, contig
vars:   haplotype_ancestry (variant, sample, ploidy)            int8  code into `ancestry`, -1 missing
        posterior          (variant, sample, ploidy, ancestry)  float32  optional
        global_ancestry    (contig, sample, ancestry)           float32
coords: chromosome, variant_position, segment_end (variant); sample_id; ancestry; contig
attrs:  source_format, source_files, rfmix_reader_version

Conventions that hold for every reader:

  • Ancestries are in the tool's own order (RFMix #reference_panel_population / #Subpopulation order/codes, FLARE ##ANCESTRY), and global_ancestry uses the same order.
  • A sample/locus with no call (e.g. an all-zero RFMix posterior) is -1 in the codes and in every count of that row.
  • Counts are derived lazily from the codes (ds.la.counts), so they are never stored twice.

ds.la is the accessor (registered on import of rfmix_reader.core, which every open_* call does):

Accessor Returns
ds.la.counts / .haplotypes / .posterior lazy DataArrays (posterior may be None)
ds.la.global_ancestry long DataFrame (sample_id, ancestries, chrom)
ds.la.samples, .ancestries, .chromosomes, .n_variants, .n_samples labels / sizes
ds.la.sel_chrom("chr21"), ds.la.sel_region("chr21", start, end) contiguous Dataset slices (binary search on the sorted positions)
ds.la.to_legacy() the legacy (0.5 and earlier) (loci_df, g_anc, local_array) triple

Operations

All operations are lazy or streaming; sample is an index or a sample ID.

Operation Result
ds.la.at_positions(loci_df, method="stepwise"|"nearest", samples=None, aggregate=True) ancestry at listed chrom/pos (haplotype counts and fractions, or per-sample copies with aggregate=False)
ds.la.locus_index(chrom, positions, method="stepwise"|"nearest", tolerance=None) index of the variant/segment covering each position (-1 = none)
ds.la.counts_at(chrom, positions, ...) (n, sample, ancestry) int8 counts at those positions
ds.la.to_bed(sample, min_segment=1) constant-ancestry intervals for one sample
ds.la.to_tagore(sample, palette="tab10") the BED annotated for TAGORE; plot with plot_local_ancestry_tagore
ds.la.to_parquet(outdir, prefix=..., rows_per_file=...) <prefix>.<chrom>-<k>.parquet (chrom, pos, hap, then <sample>_<ancestry> int8 columns), one dask block at a time
ds.la.interpolate(variants_df, zarr_outdir, method="linear"|"nearest"|"stepwise") counts on a denser variant grid, Zarr-backed per chromosome; missing calls are filled
ds.la.phase(config=PhasingConfig()) phase-corrected haplotype codes (single chromosome)
import pandas as pd

loci = pd.DataFrame({"chrom": ["chr21", "chr21"], "pos": [15_000_000, 30_000_000]})
ds.la.at_positions(loci)                       # AFR_haplotypes, AFR_fraction, ...
ds.la.at_positions(loci, samples=["NA19700"], aggregate=False)

# QTL-style access: one chromosome in memory, then index by genotype position
chrom = ds.la.sel_chrom("chr21")                        # contiguous slice, still lazy
counts = chrom.la.counts.values                          # (variants, samples, ancestries) int8
idx = chrom.la.locus_index("chr21", genotype_positions)  # segment per SNP, -1 = uncovered
H = counts[idx[idx >= 0]]

bed = ds.la.to_bed("NA19700", min_segment=3)
ds.la.to_parquet("out/", prefix="la", rows_per_file=100_000)

variants = pd.read_parquet("genotypes/variants.parquet")   # chrom, pos
dense = ds.la.interpolate(variants, "imputed/", method="stepwise")

Phasing

ds.la.phase() corrects switch errors between the two haplotypes per sample the way gnomix does: heterozygous blocks are found from the haplotype codes, each window inside a block is scored by how well the two posterior tracks match the block-start orientation versus the swapped one, uninformative windows inherit the previous state, and the two haplotypes (codes and posteriors) are exchanged wherever the track says "switched". It needs no reference panel; open .fb.tsv output with keep_posteriors=True so the posteriors are available (without them the hard calls are used). The result adds a phase_swapped (variant, sample) mask.

from rfmix_reader import open_rfmix
from rfmix_reader.processing.phase import PhasingConfig, merge_phased_zarrs

ds = open_rfmix("two_pops/out/", source="fb", keep_posteriors=True,
                cache_dir="la_cache/", chrom="21")
phased = ds.la.phase(config=PhasingConfig(window_size=50, min_block_len=20, posterior_margin=0.2))
phased["phase_swapped"].sum("variant")         # exchanged loci per sample

Whole chromosomes phase in seconds (chr21, 175k loci by 500 samples: about 4 s). phase_rfmix_chromosome_to_zarr(prefix, None, None, "phased_chr21.zarr", chrom="21") does read, phase and write in one call, and merge_phased_zarrs concatenates per-chromosome stores; both outputs reopen with open_local_ancestry.

The previous reference-panel matcher remains as method="reference" (with ref_zarr_root / sample_annot_path) for comparison only: it compares ancestry labels against reference allele codes, which is not a sound test of phase. To build its inputs, convert bgzipped, indexed reference VCFs with prepare-reference refs/ 1kg_chr21.vcf.gz (needs the reference extra) and give a two-column sample_id<TAB>group annotation table.

Haptools simulations

haptools does not write the chromosome length into the ##contig header, but the tabix region pulls need it. Reheader each file with the contig entry from the contigs.txt haptools produces, for example:

CONTIG_LINE=$(grep -w "ID=${CHR}" contigs.txt)
bcftools view -h "$IN" | sed "s/^##contig=<ID=${CHR}>.*/${CONTIG_LINE}/" > header.${CHR}.tmp
bcftools reheader -h header.${CHR}.tmp -o "$OUT" "$IN"
tabix -p vcf "$OUT"

Visualization

from rfmix_reader import plot_global_ancestry, plot_ancestry_by_chromosome, plot_local_ancestry_tagore

g_anc = ds.la.global_ancestry
plot_global_ancestry(g_anc, save_path="global")            # writes global.png and global.pdf
plot_ancestry_by_chromosome(g_anc, save_path="by_chrom")
plot_local_ancestry_tagore(ds.la.to_tagore("NA19700"), prefix="NA19700", build="hg38", oformat="png")

Migrating from 0.x

See MIGRATION.md. Every legacy (0.5 and earlier) name maps to one call on the Dataset; ds.la.to_legacy() returns the old triple and rfmix_reader.from_legacy(loci_df, g_anc, admix) builds a Dataset from one.

Development

git clone https://github.com/heart-gen/rfmix_reader.git
cd rfmix_reader
poetry install --with test --extras "viz io"
poetry run pytest             # fast suite (seconds)
poetry run pytest --run-slow  # also the chr21 tests (needs git-LFS data)

Test fixtures for every format live under tests/data/ (regenerate with python tests/data/make_fixtures.py).

Citation

If you use this software, please cite:

Benjamin, K. J. M. (2024). RFMix-reader [Computer software]. https://github.com/heart-gen/rfmix_reader

Kynon JM Benjamin. "RFMix-reader: Accelerated reading and processing for local ancestry studies." bioRxiv (2024). DOI: 10.1101/2024.07.13.603370.

Funding

This work was supported by the National Institutes of Health, National Institute on Minority Health and Health Disparities (NIMHD) K99MD016964 / R00MD016964.

Metadata

Release files for rfmix-reader 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rfmix-reader 0.7.0
File Size Uploaded
rfmix_reader-0.7.0.tar.gz 87.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rfmix-reader 0.7.0
File Interpreter ABI Platform
rfmix_reader-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 190.4 kB

Release files / rfmix_reader-0.7.0.tar.gz

Download URL rfmix_reader-0.7.0.tar.gz
Size 87.8 kB
Tags Source
SHA-256 checksum
How to use checksums
826d6b813310ea52462a14a479756c9cac2ff40c234c7ed3e3e244064121cfaf
BLAKE2b-256 checksum
How to use checksums
3506b0845e07ee30d78654f5f0eb6b04f56de80931ea1c4ce7964b3afa7ded36
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.3.3 CPython/3.14.7 Linux/7.1.6-arch1-1

Release files / rfmix_reader-0.7.0-py3-none-any.whl

Download URL rfmix_reader-0.7.0-py3-none-any.whl
Size 102.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2e85ec3e9abf6b38245ef099d7a9f5d16c64ce03f2182c3ec5a4cb3b4275b5e
BLAKE2b-256 checksum
How to use checksums
9d483f5987d06c638044a00cdc992f79f68727e47bdf1b2a381ebe03984d9627
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.3.3 CPython/3.14.7 Linux/7.1.6-arch1-1

Release history Release notifications | RSS feed

0.7.3

2 release files

0.7.2

2 release files

0.7.1

2 release files

This release

0.7.0 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.20

2 release files

0.1.19

2 release files

0.1.18

2 release files

0.1.17

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

3 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page