Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
d7ca46b
Add option for sash sigrap individual process input to cancer report …
qclayssen Aug 27, 2025
f33c48c
Hypermutation Handling in PCGR (#9)
qclayssen Aug 29, 2025
cb5ee4d
Feature/pcgr v2.2.1 (#10)
qclayssen Sep 2, 2025
9d8e4d1
fix merge duplication and imports
qclayssen Sep 16, 2025
8db35b3
bump pcgr version
qclayssen Oct 2, 2025
c67b30a
bump gpgr version
qclayssen Oct 2, 2025
f9a7d3b
change print to logging
qclayssen Oct 16, 2025
b8ee4a0
add argument chunck size
qclayssen Oct 16, 2025
8223ef9
fix version software pcgr container
qclayssen Oct 17, 2025
f83c23a
add logging and filter fonction to read value and replace placeholder
qclayssen Oct 17, 2025
22b5c12
fix indentation
qclayssen Oct 17, 2025
529309c
read gz tsv from pcgr
qclayssen Oct 17, 2025
d3dd182
change PCGR_CLINVAR_CLASSIFICATION
qclayssen Oct 20, 2025
a5fc923
Change pcgr header check
qclayssen Oct 20, 2025
bf930e7
tyo
qclayssen Oct 24, 2025
8ea1edc
typo
qclayssen Oct 24, 2025
443c5be
add missing variable pcgr_output_dir
qclayssen Oct 24, 2025
966967f
Pcgr v2.2.5 (#22)
qclayssen Nov 19, 2025
6a93f6b
Uptade sage vcf header check (#24)
qclayssen Nov 24, 2025
ed3b11b
add missing HMF_HOTSPOT constant
qclayssen Nov 24, 2025
a3d16f7
Fix quotation
qclayssen Nov 24, 2025
9ddeac8
Fix test
qclayssen Nov 24, 2025
41b83bb
remove PCGR html report from chunk (hypermutated) to reduce memory use
qclayssen Dec 16, 2025
c42d725
change remove pcgr parallelisation of chunks, and fix pcgr internal p…
qclayssen Dec 17, 2025
03bba1d
free disk worker
qclayssen Dec 17, 2025
cb1c39b
remove exception to stop at fail process
qclayssen Dec 19, 2025
27403cb
make thread you by pcgr constant, so in case of OOM, on the retry don…
qclayssen Dec 19, 2025
2902428
Fix silent command failures by integrating error handling into execut…
qclayssen Dec 19, 2025
15db538
Extract PCGR errors from log files
qclayssen Dec 19, 2025
9bf22f5
Update bolt/workflows/other/cancer_report.py
qclayssen Jan 21, 2026
599849d
Update bolt/common/pcgr.py
qclayssen Jan 21, 2026
09a3a24
Apply suggestions from code review
qclayssen Feb 19, 2026
dbd500f
fix typo
qclayssen Feb 19, 2026
3d2d7b1
remove duplicate fonction
qclayssen Feb 19, 2026
15f0247
fix typo
qclayssen Feb 20, 2026
8fa85ef
add check pcgr_hotspot_present value
qclayssen Feb 20, 2026
31dabab
add is_hypermutated flag to variant counts process JSON
qclayssen May 7, 2026
f3d557c
update PCGR_TIERS_FILTERING
qclayssen May 12, 2026
184e170
Fix PCGR hypermutated filtering, add exhaustive tests
qclayssen May 12, 2026
5a28918
add back source comment
qclayssen May 12, 2026
48488d6
DEV: install r-gpgr from input-sigrap-data branch for is_hypermutated…
qclayssen May 12, 2026
b97d216
Add tests for split_vcf chunking and document PCGR 500k handling
qclayssen May 13, 2026
cc39768
Fix positional arg shift in run_somatic_chunk → run_somatic call
qclayssen May 13, 2026
7f01001
fix hypermutated tag
qclayssen May 14, 2026
97ad9f7
Add tests for is_hypermutated dragen count fix
qclayssen May 14, 2026
b4ab71f
Separate stdout/stderr streams in execute_command
qclayssen May 14, 2026
1b366fa
Pin r-gpgr ==2.3.0 in Dockerfile.gpgr
qclayssen May 15, 2026
f77deb4
fix doc and missing file closure
qclayssen May 15, 2026
1b6e28a
add test
qclayssen May 15, 2026
58987d1
change .with_suffix :
qclayssen May 15, 2026
5949539
chore: untrack local-only files
qclayssen May 15, 2026
e0103ef
update CHANGELOG for 0.3.0 release
qclayssen May 15, 2026
149db35
bump version to 0.3.1 and update changelog
qclayssen May 18, 2026
358290a
ci: add smoke tests to catch image startup failures before push
qclayssen May 18, 2026
dae66e5
fix missing build and pcgr docker file
qclayssen May 19, 2026
c53557a
Fix remove unused package and log pcgr limit
qclayssen May 19, 2026
8a35851
fix: tabix-index VCF chunks after split_vcf so PCGR can read them
qclayssen May 19, 2026
cfac848
test: assert split_vcf chunks have .tbi index (regression for PCGR v2…
qclayssen May 19, 2026
d2c390f
fix: bump r-gpgr to 2.3.1 in gpgr Docker image
qclayssen May 24, 2026
d34bc05
fix: skip PCGR when variant count exceeds MAX_SOMATIC_VARIANTS
qclayssen Jun 2, 2026
1c1d023
test: cover PCGR graceful skip on unresolvable variant count overflow
qclayssen Jun 2, 2026
162c689
fix: treat PCGR_MUTATION_HOTSPOT=. as missing in retention check
qclayssen Jun 3, 2026
eb19562
refactor: tighten retention-check comment to one line
qclayssen Jun 3, 2026
439e9e8
test: assert real PCGR_MUTATION_HOTSPOT value retains variant
qclayssen Jun 3, 2026
3c27cb0
change: drop --estimate_signatures from PCGR somatic invocation (#31)
qclayssen Jun 9, 2026
819fda8
docs: update CHANGELOG for fixes landed since v0.3.1 tag
qclayssen Jul 3, 2026
97446f7
Fix/52 pcgr skip high variant count (#32)
qclayssen Jul 3, 2026
05a9abc
Merge remote-tracking branch 'origin/release/0.3.0' into release/0.3.0
qclayssen Jul 3, 2026
76b71ac
fix: pin free-disk-space action, disable_estimates in chunked PCGR, l…
qclayssen Jul 3, 2026
f6d0f36
Merge branch 'fix/52-pcgr-skip-high-variant-count'
qclayssen Jul 3, 2026
0a3a825
Bump version: 0.3.1 → 0.3.2
qclayssen Jul 3, 2026
91c5d3c
style: remove obvious comments and docstrings from util.py
qclayssen Jul 3, 2026
7010606
docs: clean up CHANGELOG and document test coverage per 0.3.1/0.3.2
qclayssen Jul 3, 2026
dce49d0
test(quick-260703-n70-01): add unit tests for bolt/util.py header and…
qclayssen Jul 6, 2026
89ac805
test(quick-n70): add unit tests for bolt/common/pcgr.py pure annotati…
qclayssen Jul 6, 2026
e89b411
docs(quick-n70): add TESTING.md documenting test coverage
qclayssen Jul 6, 2026
93e7ec9
fix(quick-n70): correct annotate_record missing-key test expectation
qclayssen Jul 6, 2026
4dea6c0
fix(quick-n70): raise AssertionError on unresolvable annotate_record key
qclayssen Jul 6, 2026
3580ccd
test: add coverage for estimate flags and disable_estimates forwarding
qclayssen Jul 7, 2026
67ba921
test(quick-n70): split hypermutated tests by source module
qclayssen Jul 7, 2026
9ba6778
Merge remote-tracking branch 'origin/main' into release/0.3.0
qclayssen Jul 7, 2026
331ee1e
fix: address Copilot review findings on PR #33
qclayssen Jul 7, 2026
fb51f0e
docs(quick-n70): fix stale test-file references after pcgr test split
qclayssen Jul 7, 2026
863b36e
test(quick-j7e): add bcftools-guarded lossless regression test for me…
qclayssen Jul 13, 2026
7cd15af
fix(quick-p3k): handle duplicate PCGR VCF keys in get_annotations_vcf
qclayssen Jul 13, 2026
24b1206
docs: link get_annotations_vcf dup-key NOTE to bolt#34 instead of sam…
qclayssen Jul 13, 2026
39dc084
fix(quick-r8m): handle duplicate PCGR/CPSR TSV keys instead of asserting
qclayssen Jul 13, 2026
672763a
docs: add CHANGELOG entries for the duplicate-key assert fixes
qclayssen Jul 13, 2026
26ea34f
fix(quick-v4t): disable PCGR estimates for hypermutated tiered-select…
qclayssen Jul 13, 2026
b0e8f8e
docs: add CHANGELOG entry for hypermutated disable_estimates fix
qclayssen Jul 13, 2026
1afb683
fix(quick-w2n): check SAGE VCF headers against sage_vcf_fp, not input_fp
qclayssen Jul 13, 2026
2456ab7
fix: skip bcftools merge for single PCGR VCF chunk (#26) (#36)
qclayssen Jul 22, 2026
596d771
fix: merge main into release/0.3.0 (post-#35, preserving review comme…
qclayssen Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.3.1
current_version = 0.3.2
commit = True
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>[a-z0-9+]+)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4

- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
uses: jlumbroso/free-disk-space@v1.3.0
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
Expand Down
37 changes: 32 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
# bolt changelog

## 0.3.2

- Fix: `rescue.py`'s SAGE VCF header-consistency check compared the DRAGEN input VCF's headers against the expected SAGE header descriptions instead of the actual SAGE VCF's — the check ran but validated nothing (introduced [24](https://github.com/umccr/bolt/pull/24)). Now checks `sage_vcf_fp`. First-ever test coverage added for `rescue.py` and for `util.check_annotation_headers`
- Fix: hypermutated `report` samples (tiered-selection path) never disabled `--estimate_msi`/`--estimate_tmb` — TMB/MSI were computed on a trimmed variant subset with no indication in the report. Now passes `disable_estimates=True` when `select_pcgr_variants` trims input, matching `run_somatic_chunk`'s existing chunked-input behavior
- Fix: `get_annotations_vcf` crashed with `AssertionError` on a duplicate PCGR VCF key (variant mapping to multiple transcripts) — now logs a warning and keeps the first entry ([34](https://github.com/umccr/bolt/pull/34))
- Fix: `collect_pcgr_annotation_data` / `collect_cpsr_annotation_data` had the same duplicate-key `AssertionError` on their TSV paths — somatic path now keeps the most actionable tier on a duplicate, germline/CPSR path keeps the first entry; both warn instead of crashing
- Test: `TestGetAnnotationsVcf`, `TestCollectPcgrAnnotationData`, `TestCollectCpsrAnnotationData` — duplicate-key regression coverage for all three annotation-collection paths
- Test: `TestMergeVcfFiles` — bcftools-guarded integration test proving `merge_vcf_files` (`bcftools merge -m all`) losslessly reassembles PCGR sites-only chunk VCFs (no loss/dup, position-sorted, indexed output). Locks the sites-only invariant that keeps `bcftools merge` safe for the hypermutated chunk-merge path; documented with a `NOTE` in `merge_vcf_files`
- [32](https://github.com/umccr/bolt/pull/32) - Fix `PCGR_MUTATION_HOTSPOT=.` (dot placeholder) treated as truthy in retention check — was preventing tiered filtering from running for any sample with >450k PASS variants ([sash#52](https://github.com/umccr/sash/issues/52))
- [32](https://github.com/umccr/bolt/pull/32) - Graceful PCGR skip when `select_pcgr_variants` cannot cap variants to `MAX_SOMATIC_VARIANTS` — logs warning and continues without cancer report; non-PCGR outputs still publish ([sash#52](https://github.com/umccr/sash/issues/52))
- Fix: disable `--estimate_msi`/`--estimate_tmb` in chunked PCGR annotation runs — estimates on partial VCFs are not meaningful
- Fix: pin `jlumbroso/free-disk-space` CI action to `v1.3.0` (was `@main`)
- Test: `TestSelectPcgrVariants` — 8 integration tests covering tiered trimming, hotspot retention, PANEL retention, NONCODING-first drop order, and the `PCGR_MUTATION_HOTSPOT=.` regression
- Test: `TestEntrySkipsPcgrOnOverflow` — 2 tests: entry() skips PCGR on `RuntimeError` from unresolvable overflow; entry() calls PCGR normally when within limit
- Test: `TestSelectPcgrVariantsRaisesOnUnresolvableOverflow` — asserts `RuntimeError` when retained variants alone exceed `MAX_SOMATIC_VARIANTS`
- Test: `TestRunSomaticCommandArgs` — 2 tests: `--estimate_signatures` absent from all `run_somatic` commands; `disable_estimates=True` suppresses `--estimate_msi`/`--estimate_tmb`
- Test: `TestRunSomaticChunkArgMapping.test_disable_estimates_passed_to_run_somatic` — `run_somatic_chunk` passes `disable_estimates=True` to every `run_somatic` call

## 0.3.1

- Fix `ModuleNotFoundError: No module named 'pkg_resources'` in `bolt:0.3.0-multiqc` — add `setuptools <81` to conda env
- Fix `merge_vcf_files` producing wrong output filename — `Path.with_suffix()` was stripping `.pass` component; use explicit path concatenation instead
- Fix VCF writers not closed in `transfer_annotations_somatic` and `transfer_annotations_germline` — BGZip output could be truncated
- Fix `split_vcf` writing uncompressed plain `.vcf` chunks — now uses `.vcf.gz` with `wz` mode
- Fix `PCGR_ACTIONABILITY_TIER` VCF header description — updated to match stored short-form values (`1`,`2`,`3`,`4`,`N`)
- Add regression test for chunk file compression (`test_chunks_are_gzipped`)
- Fix `split_vcf` chunks not tabix-indexed, causing PCGR to fail reading them
- Fix `build.yaml` and `Dockerfile.pcgr` build issues
- Remove unused `logging` import and fix `PCGR_MAX_SOMATIC_VARIANTS` header description in `constants.py`
- Bump `r-gpgr` to 2.3.1 in `Dockerfile.gpgr`
- Add CI smoke tests to catch Docker image startup failures before push
- [31](https://github.com/umccr/bolt/pull/31) - Drop `--estimate_signatures` from PCGR somatic invocation — signature analysis comes from gpgr/sigrap downstream; keep `--estimate_msi`/`--estimate_tmb` ([sash#57](https://github.com/umccr/sash/issues/57))
- Test: regression test for chunk file compression (`test_chunks_are_gzipped`)
- Test: regression test for chunk tabix indexing (`test_chunks_are_tabix_indexed`)
- Test: `TestTierOrdering` — 3 tests verifying `PCGR_TIERS_FILTERING` uses short forms (`N`,`4`,`3`,`2`,`1`) and NONCODING precedes TIER_1 in `get_ordering()`
- Test: `TestSplitVcf` — 4 tests: chunking above/below limit, `.vcf.gz` compression, `.tbi` indexing
- Test: `TestSelectPcgrVariants` — initial 6 integration tests for tiered trimming logic
- Test: `TestGetVariantFilterData`, `TestDetermineFilter`, `TestGetImpacts` — 14 unit tests covering variant attribute extraction and filter-category determination
- Test: `TestCountVariantProcess` — 4 tests: `is_hypermutated` flag, DRAGEN count, SAGE_NOVEL exclusion, annotation-filter exclusion
- Test: `TestRunSomaticChunkArgMapping` — asserts `pcgr_conda` is not shifted into `pcgr_threads` position on positional arg mapping

## 0.3.0

- [28](https://github.com/umccr/bolt/pull/28) - gpgr version bump to 2.2.12 for cancer report hypermutated flag fix

- [17](https://github.com/umccr/bolt/pull/17) - change dragen HRD file optional

- [14](https://github.com/umccr/bolt/pull/14) - gpgr version bump to 2.2.0

- [3](https://github.com/scwatts/bolt/pull/3) - Improve PCGR / CPSR argument handling

- [6](https://github.com/umccr/bolt/pull/6) - Change oncoanalyser v2.0.0 update, with switch sv caller from GRIPSS to eSVee

- [9](https://github.com/umccr/bolt/pull/9) Add hypermutation sample handling
74 changes: 74 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Testing

## Running tests

Canonical (matches CI):

```bash
python -m unittest discover tests/ --buffer
```

Also supported:

```bash
python -m pytest tests/ -v
```

Single test:

```bash
python -m pytest tests/test_smlv_somatic_filter.py::TestSmlvSomaticFilter::test_min_af_filter -v
```

Tests in `tests/` are pure Python + in-memory `cyvcf2` logic and must run
without any bioinformatics binary installed (no `bcftools`, `pcgr`, `cpsr`,
`vcfanno`, `snpEff`, `gpgr`, VEP). Functions that shell out to those tools are
either mocked/patched in tests or are not unit-tested (see below). The one
exception is `TestMergeVcfFiles`, an integration test that exercises the real
`merge_vcf_files` → `bcftools merge` path; it is guarded with
`@unittest.skipUnless(shutil.which('bcftools'), ...)`, so it runs in the conda
CI env and skips cleanly (never fails) where `bcftools` is absent.

## Test Coverage

| Module | Status | Functions covered | Test file |
|---|---|---|---|
| `bolt/util.py` | Partial | `get_vcf_header_entry`, `get_vcf_header_line`, `get_qualified_vcf_annotation`, `add_vcf_header_entry`, `merge_tsv_files`, `merge_vcf_files` (bcftools-guarded lossless/sorted integration test), `check_annotation_headers` | `tests/test_util.py` |
| `bolt/common/pcgr.py` | Partial | `get_ordering`, `get_impacts`, `determine_filter`, `get_variant_filter_data`, `split_vcf`, `run_somatic_chunk` (arg-mapping regression) | `tests/test_pcgr.py` |
| `bolt/common/pcgr.py` | Partial | `parse_genomic_change`, `get_impacts_higher`, `get_annotation_entry_tsv`, `compile_annotation_data`, `annotate_record`, `get_annotations_vcf` (duplicate-key regression), `collect_pcgr_annotation_data` (duplicate-key tier resolution), `collect_cpsr_annotation_data` (duplicate-key regression) | `tests/test_pcgr_annotation.py` |
| `bolt/workflows/smlv_somatic/filter.py` | Partial | `set_filter_data` | `tests/test_smlv_somatic_filter.py` |
| `bolt/workflows/smlv_somatic/report.py` | Partial | `select_pcgr_variants`, `count_variant_process`, `entry` overflow handling, `entry` `disable_estimates` branching | `tests/test_smlv_somatic_report.py` |
| `bolt/workflows/smlv_somatic/rescue.py` | Partial | `annotate_existing_sage_calls` (SAGE VCF header-consistency check only) | `tests/test_smlv_somatic_rescue.py` |

"Partial" means the module has meaningful test coverage for its pure/testable logic,
but not every function in the file is tested (see below for what is excluded and why).

## Untested / not unit-testable in CI

These require live bioinformatics binaries (bcftools, PCGR/CPSR, vcfanno, snpEff,
gpgr) or full end-to-end subprocess orchestration, so they are excluded from the
unit test suite:

| Function/module | Reason |
|---|---|
| `bolt/util.py: count_vcf_records` | Shells out to `bcftools view` |
| `bolt/util.py: execute_command` | Spawns real subprocesses via `/bin/bash` |
| `bolt/common/pcgr.py: prepare_vcf_somatic` / `prepare_vcf_germline` | Shells out to `bcftools index`/`bcftools view`/`bcftools annotate` |
| `bolt/common/pcgr.py: run_somatic` / `run_somatic_chunk` (execution path) / `run_germline` | Invoke `pcgr`/`cpsr` CLI directly |
| `bolt/common/pcgr.py: transfer_annotations_somatic` / `transfer_annotations_germline` | Depend on real PCGR/CPSR TSV+VCF output files |
| `bolt/common/pcgr.py: merging_pcgr_files` | Wraps `merge_vcf_files`/`merge_tsv_files` (bcftools-dependent) |
| `bolt/workflows/smlv_somatic/annotate.py` | Orchestrates vcfanno + PON + PCGR subprocess pipeline; no unit tests |
| `bolt/workflows/smlv_somatic/rescue.py` (all functions except the header check) | SAGE hotspot recall orchestrates `bcftools isec`/`concat`/`annotate` subprocesses end-to-end; no unit tests |
| `bolt/workflows/smlv_somatic/prepare.py` | bcftools-based VCF prep; no unit tests |
| `bolt/workflows/smlv_germline/prepare.py` | bcftools-based panel region selection; no unit tests |
| `bolt/workflows/smlv_germline/report.py` | bcftools stats + CPSR report generation; no unit tests |
| `bolt/workflows/sv_somatic/annotate.py` | snpEff subprocess annotation; no unit tests |
| `bolt/workflows/sv_somatic/prioritise.py` | Wraps `external/prioritize_sv.py`; no unit tests |
| `bolt/workflows/other/cancer_report.py` | Invokes `gpgr.R canrep` subprocess; no unit tests |
| `bolt/workflows/other/multiqc_report.py` | Invokes `multiqc` subprocess; no unit tests |
| `bolt/workflows/other/purple_baf_plot.py` | Invokes `circos` subprocess; no unit tests |
| `bolt/external/prioritize_sv.py` | Vendored third-party AstraZeneca SV annotation logic; no unit tests |

Contributions raising coverage for these are welcome, but will require mocking
subprocess calls (as done in `tests/test_smlv_somatic_report.py` for
`select_pcgr_variants`) or dedicated fixture VCFs/TSVs under `tests/fixtures/`.
96 changes: 58 additions & 38 deletions bolt/common/pcgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ def run_somatic(input_fp, pcgr_refdata_dir, vep_dir, output_dir, chunk_nbr=None,
f'--control_af_tag NORMAL_AF',
f'--genome_assembly grch38',
f'--assay WGS',
*([] if disable_estimates else ['--estimate_signatures', '--estimate_msi', '--estimate_tmb']),
# NOTE: --estimate_signatures dropped (umccr/sash#57); keep msi/tmb.
*([] if disable_estimates else ['--estimate_msi', '--estimate_tmb']),
f'--vcfanno_n_proc {vcfanno_threads}',
f'--vep_n_forks {vep_forks}',
f'--vep_pick_order biotype,rank,appris,tsl,ccds,canonical,length,mane_plus_clinical,mane_select',
Expand Down Expand Up @@ -352,6 +353,25 @@ def transfer_annotations_germline(input_fp, normal_name, cpsr_dir, output_dir):
output_fh.close()


# Lower value = more clinically actionable. Used to resolve duplicate PCGR TSV
# rows for the same variant (PCGR can emit one row per transcript mapping).
_TIER_ORDER = {'1': 0, '2': 1, '3': 2, '4': 3, 'N': 4}


def _normalise_tier(raw_tier):
# Normalize PCGR actionability tier to simple values: '1','2','3','4','N'
tier_norm = (raw_tier or '').strip().replace('_', ' ').upper()
if tier_norm in ('TIER 1', 'TIER1', '1'):
return '1'
elif tier_norm in ('TIER 2', 'TIER2', '2'):
return '2'
elif tier_norm in ('TIER 3', 'TIER3', '3'):
return '3'
elif tier_norm in ('TIER 4', 'TIER4', '4'):
return '4'
return 'N'


def collect_pcgr_annotation_data(tsv_fp, vcf_fp, info_field_map):
# Gather all annotations from TSV
data_tsv = dict()
Expand All @@ -360,23 +380,19 @@ def collect_pcgr_annotation_data(tsv_fp, vcf_fp, info_field_map):
with open_fn(tsv_fp, 'rt') as tsv_fh:
for record in csv.DictReader(tsv_fh, delimiter='\t'):
key, record_ann = get_annotation_entry_tsv(record, info_field_map)
assert key not in data_tsv

# Normalize PCGR actionability tier to simple values: '1','2','3','4','N'
raw_tier = (record.get('ACTIONABILITY_TIER') or '').strip()
tier_norm = raw_tier.replace('_', ' ').upper()
if tier_norm in ('TIER 1','TIER1','1'):
tier_val = '1'
elif tier_norm in ('TIER 2','TIER2','2'):
tier_val = '2'
elif tier_norm in ('TIER 3','TIER3','3'):
tier_val = '3'
elif tier_norm in ('TIER 4','TIER4','4'):
tier_val = '4'
else:
tier_val = 'N'
tier_val = _normalise_tier(record.get('ACTIONABILITY_TIER'))
record_ann[constants.VcfInfo.PCGR_ACTIONABILITY_TIER] = tier_val

# NOTE(QC): PCGR can emit multiple TSV rows for the same variant when it
# maps to multiple transcripts. Keep the most actionable entry (lowest
# tier number) rather than asserting. See https://github.com/umccr/bolt/pull/34
if key in data_tsv:
existing_tier = data_tsv[key][constants.VcfInfo.PCGR_ACTIONABILITY_TIER]
if _TIER_ORDER[tier_val] >= _TIER_ORDER[existing_tier]:
logger.warning(f'Duplicate PCGR TSV key {key}: keeping tier {existing_tier}, skipping tier {tier_val}')
continue
logger.warning(f'Duplicate PCGR TSV key {key}: replacing tier {existing_tier} with more actionable tier {tier_val}')

# Store annotation data
data_tsv[key] = record_ann

Expand Down Expand Up @@ -404,7 +420,15 @@ def collect_cpsr_annotation_data(tsv_fp, vcf_fp, info_field_map):
record['ALT'] = re_result.group('alt')

key, record_ann = get_annotation_entry_tsv(record, info_field_map)
assert key not in data_tsv

# NOTE(QC): CPSR can emit multiple TSV rows for the same variant when it
# maps to multiple transcripts (same class of issue as the PCGR somatic
# TSV path above). CPSR rows carry no actionability tier to break ties
# on, so keep the first entry and warn. See https://github.com/umccr/bolt/pull/34
if key in data_tsv:
logger.warning(f'Duplicate CPSR TSV key {key}: keeping first entry')
continue

data_tsv[key] = record_ann

# Gather annotations from VCF
Expand All @@ -414,18 +438,14 @@ def collect_cpsr_annotation_data(tsv_fp, vcf_fp, info_field_map):
return compile_annotation_data(data_tsv, data_vcf)

def parse_genomic_change(genomic_change):
"""
Parse a genomic change string, e.g., "3:g.41224645T>C"
Returns a tuple: (chrom, pos, ref, alt)
"""
# Regular expression for the format "chrom:g.posRef>Alt"
# Format: "chrom:g.posRef>Alt" e.g. "3:g.41224645T>C"
pattern = r'^(?P<chrom>\w+):g\.(?P<pos>\d+)(?P<ref>\w+)>(?P<alt>\w+)$'
match = re.match(pattern, genomic_change)
if not match:
raise ValueError(f"Format not recognized: {genomic_change}")

# Get values and format as needed
chrom = f"chr{match.group('chrom')}"
chrom = match.group('chrom')
if not chrom.startswith('chr'):
chrom = f'chr{chrom}'
pos = int(match.group('pos'))
ref = match.group('ref')
alt = match.group('alt')
Expand All @@ -439,7 +459,14 @@ def get_annotations_vcf(vcf_fp, info_field_map):
assert len(record.ALT) == 1
[alt] = record.ALT
key = (f'chr{record.CHROM}', record.POS, record.REF, alt)
assert key not in data_vcf

# NOTE(QC): PCGR can emit duplicate variant entries in its output VCF when a
# variant maps to multiple transcripts. Keep the first entry and warn rather
# than asserting, consistent with the TSV-side dedup.
# https://github.com/umccr/bolt/pull/34
if key in data_vcf:
logger.warning(f'Duplicate PCGR VCF key {key}: keeping first entry')
continue

data_vcf[key] = dict()
for info_dst, info_src in info_field_map.items():
Expand Down Expand Up @@ -505,7 +532,7 @@ def annotate_record(record, annotations, *, allow_missing=False):
if allow_missing:
return record
else:
assert key not in annotations
assert False, f'Missing annotation key: {key}'

# Transfer annotations
for info_enum, v in annotations[key].items():
Expand All @@ -514,11 +541,6 @@ def annotate_record(record, annotations, *, allow_missing=False):
return record

def split_vcf(input_vcf, output_dir, *, max_variants=None):
"""
Splits a VCF file into multiple chunks, each containing up to max_variants variants.
Each chunk includes the VCF header.
Ensures no overlapping positions between chunks.
"""
if max_variants is None:
max_variants = constants.MAX_SOMATIC_VARIANTS
elif max_variants <= 0:
Expand Down Expand Up @@ -566,27 +588,25 @@ def split_vcf(input_vcf, output_dir, *, max_variants=None):
def run_somatic_chunk(vcf_chunks, pcgr_data_dir, vep_dir, output_dir, pcgr_output_dir, max_threads, pcgr_conda, pcgrr_conda):
pcgr_tsv_files = []
pcgr_vcf_files = []

# Process each chunk sequentially

for chunk_number, vcf_file in enumerate(vcf_chunks, start=1):
pcgr_tsv_fp, pcgr_vcf_fp = run_somatic(vcf_file, pcgr_data_dir, vep_dir, pcgr_output_dir, chunk_nbr=chunk_number, threads=max_threads, pcgr_conda=pcgr_conda, pcgrr_conda=pcgrr_conda)
pcgr_tsv_fp, pcgr_vcf_fp = run_somatic(vcf_file, pcgr_data_dir, vep_dir, pcgr_output_dir, chunk_nbr=chunk_number, threads=max_threads, pcgr_conda=pcgr_conda, pcgrr_conda=pcgrr_conda, disable_estimates=True)
if pcgr_tsv_fp:
pcgr_tsv_files.append(pcgr_tsv_fp)
if pcgr_vcf_fp:
pcgr_vcf_files.append(pcgr_vcf_fp)

merged_vcf_fp, merged_tsv_fp = merging_pcgr_files(output_dir, pcgr_vcf_files, pcgr_tsv_files)
return merged_tsv_fp, merged_vcf_fp


def merging_pcgr_files(output_dir, pcgr_vcf_files, pcgr_tsv_files):
pcgr_dir = pathlib.Path(output_dir) / 'pcgr'
pcgr_dir.mkdir(exist_ok=True)

# Merge all TSV files into a single file in the pcgr directory
merged_tsv_fp = pcgr_dir / "nosampleset.pcgr_acmg.grch38.snvs_indels.tiers.tsv.gz"
util.merge_tsv_files(pcgr_tsv_files, merged_tsv_fp)

# Step 5: Merge all VCF files into a single file in the pcgr directory
merged_vcf_path = pcgr_dir / "nosampleset.pcgr.grch38.pass"
if len(pcgr_vcf_files) == 1:
# NOTE(QC): bcftools merge requires 2+ inputs; with a single chunk there is
Expand Down
Loading
Loading