From 51db244cc8a5e330a1ee7190491678809e211cbb Mon Sep 17 00:00:00 2001 From: pieetie <129987442+pieetie@users.noreply.github.com> Date: Mon, 25 May 2026 18:23:34 +0200 Subject: [PATCH 1/5] docs: update README (scope + limitations) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 69d625c..dccb47a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,17 @@ pip install -e ".[dev,test]" For ready-to-run command lines (single-sample `gen`, paired somatic `gen-pair`, validation, banks, and dev checks), see [`docs/ready-to-use.md`](docs/ready-to-use.md). +## Scope & limitations + +svForge ships the full header of each target caller verbatim, but for now it does not populate every INFO/FORMAT field a real run would emit. +The fields below are declared in the header for parser compatibility but left empty in records — support may land in a later release. + +**Manta** — not yet populated: `CIPOS`, `CIEND` (alignment-derived breakpoint CIs), `HOMSEQ` (microhomology sequence; `HOMLEN` is populated), `BND_DEPTH`, `MATE_BND_DEPTH` (per-breakend coverage). + +**DELLY** — not yet populated: `RDRATIO` (tumor/normal read-depth ratio), `INSLEN`, `SRMAPQ`, `CONSENSUS`, `CONSBP` (split-read consensus); also, records are currently always tagged `PRECISE` rather than alternating with `IMPRECISE`. + +In the meantime, if your downstream pipeline hard-requires any of these fields, you can supply your own `--header-template` that drops the unused declarations, or post-process the VCF to fill them with sentinel values. + ## Typical use cases - Validate downstream filters (for example, `SVFORGE_SOURCE=gnomad` records should disappear after your gnomAD filtering step). From 41de4bd65cbbb929374e3b7b3c6612a2d82ea8db Mon Sep 17 00:00:00 2001 From: pieetie <129987442+pieetie@users.noreply.github.com> Date: Mon, 25 May 2026 18:26:28 +0200 Subject: [PATCH 2/5] fix(sampler): draw REF from A/C/G/T instead of N --- src/svforge/core/sampler.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/svforge/core/sampler.py b/src/svforge/core/sampler.py index 680f6e8..da699be 100644 --- a/src/svforge/core/sampler.py +++ b/src/svforge/core/sampler.py @@ -38,6 +38,19 @@ _CHROM_SAMPLE_MARGIN = 10_000 _BLACKLIST_SVTYPES = ("DEL", "DUP", "INV") +_REF_BASES = ("A", "C", "G", "T") + + +def _random_ref_base(rng: random.Random) -> str: + """ + Return a plausible REF base for an SV anchor (random A/C/G/T) + + Real callers always emit a base extracted from the reference fasta. svforge + has no fasta access, so it draws uniformly from the four canonical bases. + This keeps the BND ALT (which embeds REF) looking like e.g. ``G]chr1:42]`` + instead of ``N]chr1:42]`` + """ + return rng.choice(_REF_BASES) _T = TypeVar("_T") @@ -294,6 +307,7 @@ def _gnomad_entry_to_sv(entry: GnomadEntry, cfg: SamplerConfig, rng: random.Rand strands="+-", vaf=vaf, genotype=genotype, + ref_base=_random_ref_base(rng), origin=cfg.origin, source="gnomad", info_extra={"SVFORGE_SOURCE_ID": entry.source_id}, @@ -310,6 +324,7 @@ def _gnomad_entry_to_sv(entry: GnomadEntry, cfg: SamplerConfig, rng: random.Rand strands=_default_strands(entry.svtype), vaf=vaf, genotype=genotype, + ref_base=_random_ref_base(rng), origin=cfg.origin, source="gnomad", info_extra={"SVFORGE_SOURCE_ID": entry.source_id}, @@ -338,6 +353,7 @@ def _blacklist_entry_to_sv(entry: BlacklistEntry, cfg: SamplerConfig, rng: rando strands=_default_strands(svtype), vaf=vaf, genotype=genotype, + ref_base=_random_ref_base(rng), origin=cfg.origin, source="blacklist", info_extra={"SVFORGE_SOURCE_ID": entry.source_id}, @@ -395,6 +411,7 @@ def _materialize( homlen=homlen, vaf=vaf, genotype=genotype, + ref_base=_random_ref_base(rng), origin=cfg.origin, source="bank", ) @@ -418,6 +435,7 @@ def _materialize( homlen=homlen, vaf=vaf, genotype=genotype, + ref_base=_random_ref_base(rng), origin=cfg.origin, source="bank", ) From 7f04b18b6e177bc55298d26e9e3d02bfec23a8a7 Mon Sep 17 00:00:00 2001 From: pieetie <129987442+pieetie@users.noreply.github.com> Date: Mon, 25 May 2026 18:26:58 +0200 Subject: [PATCH 3/5] fix(delly): match format order and BND brkend ALT --- src/svforge/writers/delly.py | 41 ++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/svforge/writers/delly.py b/src/svforge/writers/delly.py index 683a280..2add1ca 100644 --- a/src/svforge/writers/delly.py +++ b/src/svforge/writers/delly.py @@ -68,17 +68,17 @@ def _paired_sample_columns(sv: SV) -> tuple[str, str, str]: if sv.genotype == "1/1" else ("0,-5,-50" if sv.genotype == "0/0" else "-10,-1,-10") ) - fmt = "GT:GL:GQ:FT:RC:RCL:RCR:RDCN:DR:DV:RR:RV" + fmt = "GT:GL:GQ:FT:RCL:RC:RCR:RDCN:DR:DV:RR:RV" tumor_col = ( - f"{sv.genotype}:{gl_alt}:{gq}:PASS:40:20:20:2:" + f"{sv.genotype}:{gl_alt}:{gq}:PASS:20:40:20:2:" f"{ref}:{alt}:{junction_ref}:{junction_alt}" ) if sv.origin == "germline": normal_col = tumor_col else: - normal_col = f"0/0:0,-5,-50:{gq}:PASS:40:20:20:2:30:0:15:0" + normal_col = f"0/0:0,-5,-50:{gq}:PASS:20:40:20:2:30:0:15:0" return fmt, tumor_col, normal_col @@ -117,8 +117,8 @@ def _sample_column(sv: SV) -> tuple[str, str]: if sv.genotype == "1/1" else ("0,-5,-50" if sv.genotype == "0/0" else "-10,-1,-10") ) - fmt = "GT:GL:GQ:FT:RC:RCL:RCR:RDCN:DR:DV:RR:RV" - sample = f"{sv.genotype}:{gl}:{gq}:PASS:40:20:20:2:{ref}:{alt}:{junction_ref}:{junction_alt}" + fmt = "GT:GL:GQ:FT:RCL:RC:RCR:RDCN:DR:DV:RR:RV" + sample = f"{sv.genotype}:{gl}:{gq}:PASS:20:40:20:2:{ref}:{alt}:{junction_ref}:{junction_alt}" return fmt, sample @@ -155,8 +155,35 @@ def _base_info(sv: SV) -> list[str]: return info +def _bnd_alt(ref_base: str, mate_chrom: str, mate_pos: int, strands: str) -> str: + """ + Return the VCF 4.2 breakend ALT string used by real DELLY for BND records + + Real DELLY emits the standard breakend notation (e.g. ``]chr3:151368767]C``) + rather than the ```` symbolic allele. The ```` declaration in the + bundled header is kept verbatim from the reference template for header + fidelity but is never actually consumed by Delly's record format + """ + loc = f"{mate_chrom}:{mate_pos}" + s1, s2 = strands[0], strands[1] + if s1 == "+" and s2 == "+": + return f"{ref_base}[{loc}[" + if s1 == "+" and s2 == "-": + return f"{ref_base}]{loc}]" + if s1 == "-" and s2 == "+": + return f"[{loc}[{ref_base}" + return f"]{loc}]{ref_base}" + + +def _delly_alt(sv: SV) -> str: + if sv.svtype == "BND": + assert sv.mate_chrom is not None and sv.mate_pos is not None + return _bnd_alt(sv.ref_base, sv.mate_chrom, sv.mate_pos, sv.strands) + return f"<{sv.svtype}>" + + def _delly_record(sv: SV) -> VCFRecord: - alt = f"<{sv.svtype}>" + alt = _delly_alt(sv) info = ";".join(_base_info(sv)) fmt, sample = _sample_column(sv) line = "\t".join( @@ -177,7 +204,7 @@ def _delly_record(sv: SV) -> VCFRecord: def _delly_record_paired(sv: SV) -> VCFRecord: - alt = f"<{sv.svtype}>" + alt = _delly_alt(sv) info = ";".join(_base_info(sv)) fmt, tumor_col, normal_col = _paired_sample_columns(sv) line = "\t".join( From 480063c775fcb9d0363beee0072d8312bdc70ad1 Mon Sep 17 00:00:00 2001 From: pieetie <129987442+pieetie@users.noreply.github.com> Date: Mon, 25 May 2026 18:37:18 +0200 Subject: [PATCH 4/5] fix(manta): emit instead of --- src/svforge/writers/manta.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/svforge/writers/manta.py b/src/svforge/writers/manta.py index bc888e9..1465a39 100644 --- a/src/svforge/writers/manta.py +++ b/src/svforge/writers/manta.py @@ -124,8 +124,20 @@ def _base_info(sv: SV) -> list[str]: return info +def _symbolic_alt(svtype: str) -> str: + """ + Return the symbolic ALT used by Manta for ``svtype`` + + Manta declares ```` (not ````) in its header ALT meta and + every DUP record in the wild carries that exact symbolic value + """ + if svtype == "DUP": + return "" + return f"<{svtype}>" + + def _symbolic_record(sv: SV) -> VCFRecord: - alt = f"<{sv.svtype}>" + alt = _symbolic_alt(sv.svtype) info = ";".join(_base_info(sv)) fmt, sample = _sample_column(sv) line = "\t".join( @@ -146,7 +158,7 @@ def _symbolic_record(sv: SV) -> VCFRecord: def _symbolic_record_paired(sv: SV) -> VCFRecord: - alt = f"<{sv.svtype}>" + alt = _symbolic_alt(sv.svtype) info = ";".join(_base_info(sv)) fmt, normal_col, tumor_col = _paired_sample_columns(sv) line = "\t".join( From c66761e555f6e67ff61815e30934ce963934057b Mon Sep 17 00:00:00 2001 From: pieetie <129987442+pieetie@users.noreply.github.com> Date: Mon, 25 May 2026 18:38:30 +0200 Subject: [PATCH 5/5] chore(release): v1.0.2 --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a4bc4..a5381a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.0.2] — 2026-05-25 + +### Fixed + +- **DELLY writer**: `FORMAT` field order corrected from `…FT:RC:RCL:RCR…` to `…FT:RCL:RC:RCR…` to match real DELLY 1.x output; sample column values reordered accordingly so `RC` stays the SV-region count and `RCL`/`RCR` the left/right control regions. +- **Manta writer**: DUP records now emit `` as ALT (matching the `##ALT` declaration in the Manta header) instead of the undeclared ``. +- **DELLY writer**: BND records now use the standard VCF 4.2 breakend notation (e.g. `]chr3:151368767]C`) instead of the symbolic `` allele, matching real DELLY output. +- **Sampler**: SV anchor `REF` base is now drawn uniformly from A/C/G/T per record instead of being hardcoded to `N`. This makes BND ALT strings look like `G]chr1:42]` instead of `N]chr1:42]`. + +### Added + +- README "Scope & limitations" section documenting which INFO/FORMAT fields are declared in the bundled headers but not (yet) populated, split by caller (Manta vs DELLY). + ## [1.0.1] — 2026-04-28 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index f606f12..fc484a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "svforge" -version = "1.0.1" +version = "1.0.2" description = "Synthetic VCF generator for structural variants (Manta, DELLY) with controlled variability and realistic artefact injection" readme = "README.md" requires-python = ">=3.10"