v2 CN/LOH (WIP): cn build-model and cn call
ci / test (3.12) (push) Failing after 10s
ci / test (3.13) (push) Failing after 11s

Cyto model (.simurg-cyto: GC 1 kb/100 kb/1 Mb, PFB from the cluster file, optional
sex-aware batch reference from normal GTCs; IN-051, CYTO-038); GC-wave correction
(CYTO-003); smoothing (CYTO-004); HMM with constitutional, gain-LOH, mosaic and haploid
states, forward-backward QUAL and Viterbi segments (CYTO-005/012); event statistics,
effective size, post-filters, per-type thresholds, promotion/LowFraction, built-in
centromere mask from UCSC cytoBand (CYTO-006..021); sex resolution (CYTO-024), ploidy;
CNV VCF with bgzip+tabix (CYTO-028..031), segments parquet, cn_sample_summary.tsv and
the cn_call_manifest.json sidecar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01814GKVzZVL3FuF3DeiZXhv
This commit is contained in:
kemalandClaude Opus 5.5 committed 2026-09-29 01:13:51 +01:00
1 parent cb3f5fac58
commit 3068e84566
8 files changed
+1452

No files matched your search

+101
View File
@@ -666,6 +666,107 @@ def qc_controls_cmd(
run_guarded(rep, lambda: run_qc_controls(opts, rep))
# --------------------------------------------------------------------------------------------------------
cn_app = typer.Typer(help="Genome-wide copy number, LOH and mosaicism (CYTO-001).")
app.add_typer(cn_app, name="cn")
@cn_app.command("build-model")
def cn_build_model_cmd(
bpm: Annotated[Path, typer.Option("--bpm", help="BPM manifest (required).")],
fasta: Annotated[Path, typer.Option("--fasta", help="Reference FASTA of the manifest's build (required).")],
out: Annotated[Path, typer.Option("--out", help="Output model file (.simurg-cyto).")],
egt: Annotated[Path | None, typer.Option("--egt", help="Cluster file: population B-allele frequencies.")] = None,
reference_gtcs: Annotated[
Path | None, typer.Option("--reference-gtcs", help="GTCs of normal samples (>= 24) for the batch reference.")
] = None,
max_reference: Annotated[int, typer.Option("--max-reference", min=1, help="Random subset size.")] = 100,
log_json: LogJson = False,
debug: Debug = False,
quiet: Quiet = False,
) -> None:
"""Cyto model: GC covariates, PFB and an optional batch reference per probe (CYTO-038, IN-051)."""
from simurg.pipeline.cn import run_build_model
rep = _reporter("cn build-model", out.parent, log_json, debug, quiet)
run_guarded(rep, lambda: run_build_model(bpm, fasta, out, rep, egt, "auto", reference_gtcs, max_reference))
def _kb(v: int) -> int:
if not 0 <= v <= 250_000:
raise typer.BadParameter("sizes are 0-250000 kb")
return v * 1000
@cn_app.command("call")
def cn_call_cmd(
bpm: Annotated[Path, typer.Option("--bpm", help="BPM manifest (required).")],
model: Annotated[Path, typer.Option("--model", help="Cyto model from cn build-model (required).")],
out: Annotated[Path, typer.Option("--out", help="Output folder (required).")],
gtcs: Annotated[Path | None, typer.Option("--gtcs", help="Folder searched recursively for GTC files.")] = None,
summary: Annotated[
Path | None, typer.Option("--summary", help="sample_summary.tsv (default: next to GTCs).")
] = None,
fasta: Annotated[Path | None, typer.Option("--fasta", help="FASTA for REF padding bases.")] = None,
min_del_probes: Annotated[int, typer.Option("--min-del-probes", min=0, max=250000)] = 10,
min_del_size: Annotated[int, typer.Option("--min-del-size", help="kb")] = 25,
min_dup_probes: Annotated[int, typer.Option("--min-dup-probes", min=0, max=250000)] = 10,
min_dup_size: Annotated[int, typer.Option("--min-dup-size", help="kb")] = 50,
min_loh_probes: Annotated[int, typer.Option("--min-loh-probes", min=0, max=250000)] = 500,
min_loh_size: Annotated[int, typer.Option("--min-loh-size", help="kb")] = 3000,
min_gainloh_probes: Annotated[int, typer.Option("--min-gainloh-probes", min=0, max=250000)] = 300,
min_gainloh_size: Annotated[int, typer.Option("--min-gainloh-size", help="kb")] = 500,
min_mdel_probes: Annotated[int, typer.Option("--min-mdel-probes", min=0, max=250000)] = 10,
min_mdel_size: Annotated[int, typer.Option("--min-mdel-size", help="kb")] = 25,
min_mdup_probes: Annotated[int, typer.Option("--min-mdup-probes", min=0, max=250000)] = 10,
min_mdup_size: Annotated[int, typer.Option("--min-mdup-size", help="kb")] = 50,
min_mloh_probes: Annotated[int, typer.Option("--min-mloh-probes", min=0, max=250000)] = 500,
min_mloh_size: Annotated[int, typer.Option("--min-mloh-size", help="kb")] = 3000,
smoothing: Annotated[int, typer.Option("--smoothing", min=0, max=1000)] = 5,
max_mosaic_fraction: Annotated[float, typer.Option("--max-mosaic-fraction")] = 1.0,
min_mosaic_fraction: Annotated[float, typer.Option("--min-mosaic-fraction")] = 0.10,
filter_qual: Annotated[int, typer.Option("--filter-qual", min=0, max=60)] = 10,
cn_cap: Annotated[int, typer.Option("--cn-cap", min=3, max=8)] = 4,
gc_correct: Annotated[bool, typer.Option("--gc-correct/--no-gc-correct")] = True,
unknown_sex_policy: Annotated[str, typer.Option("--unknown-sex-policy")] = "skip-sex-chromosomes",
gt_mode: Annotated[str, typer.Option("--gt-mode", help="vcf|compat")] = "vcf",
svlen_mode: Annotated[str, typer.Option("--svlen-mode", help="signed|positive")] = "signed",
mask_bed: Annotated[Path | None, typer.Option("--mask-bed", help="Mask BED (default: built-in).")] = None,
no_bgzip: Annotated[bool, typer.Option("--no-bgzip", help="Plain VCF (cn annotate then refuses it).")] = False,
threads: ThreadsOpt = 0,
force: ForceOpt = False,
log_json: LogJson = False,
debug: Debug = False,
quiet: Quiet = False,
) -> None:
"""GTCs -> per-sample CNV/LOH/mosaic VCF, segments and summary (CLI-028)."""
from simurg.cn.events import Thresholds
from simurg.pipeline.cn import CnCallOptions, run_cn_call
if unknown_sex_policy not in ("skip-sex-chromosomes", "female", "male", "error"):
raise typer.BadParameter("--unknown-sex-policy: skip-sex-chromosomes|female|male|error")
if gt_mode not in ("vcf", "compat") or svlen_mode not in ("signed", "positive"):
raise typer.BadParameter("--gt-mode vcf|compat, --svlen-mode signed|positive")
if not 0 < max_mosaic_fraction <= 1 or not 0 <= min_mosaic_fraction < 1:
raise typer.BadParameter("mosaic fractions: 0 < max <= 1, 0 <= min < 1")
th = Thresholds(
probes={"del": min_del_probes, "dup": min_dup_probes, "loh": min_loh_probes, "gainloh": min_gainloh_probes,
"mdel": min_mdel_probes, "mdup": min_mdup_probes, "mloh": min_mloh_probes},
size_bp={"del": _kb(min_del_size), "dup": _kb(min_dup_size), "loh": _kb(min_loh_size),
"gainloh": _kb(min_gainloh_size), "mdel": _kb(min_mdel_size), "mdup": _kb(min_mdup_size),
"mloh": _kb(min_mloh_size)},
max_mosaic_fraction=max_mosaic_fraction, min_mosaic_fraction=min_mosaic_fraction, filter_qual=filter_qual,
) # fmt: skip
rep = _reporter("cn call", out, log_json, debug, quiet)
opts = CnCallOptions(
out=out, bpm=bpm, model=model, gtcs=gtcs, summary=summary, fasta=fasta, thresholds=th, smoothing=smoothing,
cn_cap=cn_cap, gc_correct=gc_correct, unknown_sex_policy=unknown_sex_policy, gt_mode=gt_mode,
svlen_mode=svlen_mode, mask_bed=mask_bed, bgzip=not no_bgzip, threads=threads, force=force,
argv=sys.argv[1:],
) # fmt: skip
run_guarded(rep, lambda: run_cn_call(opts, rep))
# --------------------------------------------------------------------------------------------------------
methyl_app = typer.Typer(help="Methylation arrays: control QC, detection, beta values (MET-001).")
app.add_typer(methyl_app, name="methyl")