deepgenopix / tests /test_variant_simulate.py
vedatonuryilmaz's picture
test simulator skipped loci
b380516 verified
Raw
History Blame Contribute Delete
6.01 kB
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
from deepgenopix.variant.simulate import (
SimulatorConfig,
VariantManifest,
inject_variants,
materialize_corpus,
reverse_complement,
simulate_reads_with_truth,
token_position_for,
)
def _det_seq(length: int, seed: int) -> str:
rng = np.random.default_rng(seed)
return "".join("ACGT"[int(b)] for b in rng.integers(0, 4, size=length))
def test_inject_snp_changes_only_target_position():
seq = _det_seq(120, seed=11)
pos = 37
ref_base = seq[pos]
alt_base = "ACGT"[("ACGT".index(ref_base) + 1) % 4]
manifest = VariantManifest("locus_a", pos, "snp", ref_base, alt_base, 0.5)
out = inject_variants(seq, [manifest])
assert out[pos] == alt_base
assert out[:pos] == seq[:pos]
assert out[pos + 1 :] == seq[pos + 1 :]
def test_inject_insertion_grows_sequence():
seq = _det_seq(96, seed=22)
insert = "GGGAAACCC"
manifest = VariantManifest("locus_a", 40, "ins", "", insert, 0.5)
out = inject_variants(seq, [manifest])
assert len(out) == len(seq) + len(insert)
assert out[40 : 40 + len(insert)] == insert
assert out[: 40] == seq[: 40]
assert out[40 + len(insert) :] == seq[40:]
def test_inject_deletion_removes_window():
seq = _det_seq(96, seed=33)
start = 50
deletion = seq[start : start + 4]
manifest = VariantManifest("locus_a", start, "del", deletion, "", 0.5)
out = inject_variants(seq, [manifest])
assert len(out) == len(seq) - 4
assert out[: start] == seq[: start]
assert out[start :] == seq[start + 4 :]
def test_inject_inversion_reverse_complements_window():
seq = _det_seq(96, seed=44)
start = 24
length = 12
segment = seq[start : start + length]
manifest = VariantManifest("locus_a", start, "inv", segment, reverse_complement(segment), 0.5)
out = inject_variants(seq, [manifest])
assert out[start : start + length] == reverse_complement(segment)
assert out[: start] == seq[: start]
assert out[start + length :] == seq[start + length :]
def test_simulate_reads_truth_round_trip_for_snp():
seq = _det_seq(300, seed=55)
pos = 64
ref_base = seq[pos]
alt_base = "ACGT"[("ACGT".index(ref_base) + 1) % 4]
manifest = VariantManifest("locus_a", pos, "snp", ref_base, alt_base, 0.5)
reads, truth = simulate_reads_with_truth(
seq,
[manifest],
coverage=40,
read_length=120,
error_rate=0.0,
seed=7,
haplotype_frequency=0.5,
bp_per_token=24,
)
assert len(reads) == len(truth)
assert set(truth["haplotype"]) == {"variant", "reference"}
# Reads that cover the variant position from the variant haplotype must
# show the alt base; reference reads must show the ref base.
variant_rows = truth[truth["haplotype"] == "variant"]
for _, row in variant_rows.iterrows():
start = int(row["read_offset_in_haplotype"])
end = start + 120
if not (start <= pos < end):
continue
offset_in_read = pos - start
assert reads[int(row["read_idx"])][offset_in_read] == alt_base
assert row["manifest_idxs"] == (0,)
assert row["variant_token_positions"] == (token_position_for(pos, bp_per_token=24),)
reference_rows = truth[truth["haplotype"] == "reference"]
for _, row in reference_rows.iterrows():
start = int(row["read_offset_in_haplotype"])
end = start + 120
if not (start <= pos < end):
continue
offset_in_read = pos - start
assert reads[int(row["read_idx"])][offset_in_read] == ref_base
def test_token_position_for_respects_bp_per_token():
assert token_position_for(0, bp_per_token=24) == 0
assert token_position_for(23, bp_per_token=24) == 0
assert token_position_for(24, bp_per_token=24) == 1
assert token_position_for(60, bp_per_token=24, locus_offset=12) == 2
def test_materialize_corpus_round_trips(tmp_path: Path):
sequences = [_det_seq(300, seed=100 + i) for i in range(3)]
reference_parquet = tmp_path / "reference.parquet"
pd.DataFrame(
{
"locus_id": [f"locus_{i}" for i in range(len(sequences))],
"sequence": sequences,
}
).to_parquet(reference_parquet, index=False)
output_dir = tmp_path / "corpus"
cfg = SimulatorConfig(
coverage=20,
read_length=150,
error_rate=0.0,
bp_per_token=24,
seed=42,
variants_per_locus=2,
variant_types=("snp",),
frequency_levels=(0.5,),
)
paths = materialize_corpus(reference_parquet, cfg, output_dir)
assert paths.reads.exists()
assert paths.truth.exists()
assert paths.manifest.exists()
assert paths.summary.exists()
reads_df = pd.read_parquet(paths.reads)
truth_df = pd.read_parquet(paths.truth)
manifests_df = pd.read_parquet(paths.manifest)
assert len(reads_df) == len(truth_df)
assert len(manifests_df) == cfg.variants_per_locus * len(sequences)
assert truth_df["haplotype"].isin({"reference", "variant"}).all()
def test_materialize_corpus_skips_loci_that_become_too_short(tmp_path: Path):
reference_parquet = tmp_path / "reference.parquet"
pd.DataFrame(
{
"locus_id": ["too_short_after_deletion", "long_enough"],
"sequence": [_det_seq(151, seed=11), _det_seq(220, seed=12)],
}
).to_parquet(reference_parquet, index=False)
cfg = SimulatorConfig(
coverage=5,
read_length=150,
error_rate=0.0,
seed=0,
variant_types=("del",),
indel_size_bp=3,
)
paths = materialize_corpus(reference_parquet, cfg, tmp_path / "corpus")
summary = json.loads(paths.summary.read_text())
assert summary["n_loci"] == 1
assert summary["n_skipped_loci"] == 1
assert "haplotype shorter than read_length" in summary["skipped_loci"][0]["reason"]