strkit 0.12.0__tar.gz → 0.12.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {strkit-0.12.0/strkit.egg-info → strkit-0.12.0a1}/PKG-INFO +2 -9
- {strkit-0.12.0 → strkit-0.12.0a1}/README.md +1 -7
- {strkit-0.12.0 → strkit-0.12.0a1}/setup.py +0 -1
- strkit-0.12.0a1/strkit/VERSION +1 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/call_locus.py +8 -20
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/call_sample.py +66 -32
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/types.py +4 -6
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/entry.py +0 -21
- {strkit-0.12.0 → strkit-0.12.0a1/strkit.egg-info}/PKG-INFO +2 -9
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit.egg-info/SOURCES.txt +0 -5
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit.egg-info/requires.txt +0 -1
- strkit-0.12.0/strkit/VERSION +0 -1
- strkit-0.12.0/strkit/call/consensus.py +0 -26
- strkit-0.12.0/strkit/call/output/__init__.py +0 -9
- strkit-0.12.0/strkit/call/output/json_report.py +0 -46
- strkit-0.12.0/strkit/call/output/tsv.py +0 -34
- strkit-0.12.0/strkit/call/output/vcf.py +0 -170
- {strkit-0.12.0 → strkit-0.12.0a1}/LICENSE +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/MANIFEST.in +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/pyproject.toml +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/setup.cfg +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/__init__.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/__init__.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/align_matrix.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/allele.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/cigar.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/non_daemonic_pool.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/realign.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/repeats.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/snvs.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/call/utils.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/catalog/__init__.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/catalog/combine.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/constants.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/__init__.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/_bed_4.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/converter.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/expansionhunter.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/gangstr.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/hipstr.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/straglr.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/convert/tandem_genotypes.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/exceptions.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/json.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/logger.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/__init__.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/base.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/expansionhunter.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/gangstr.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/repeathmm.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/result.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/straglr.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/strkit.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/tandem_genotypes.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/mi/vcf_utils.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/utils.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/viz/__init__.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/viz/server.py +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/viz/static/logo.png +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit/viz/templates/browser.html +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit.egg-info/dependency_links.txt +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit.egg-info/entry_points.txt +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/strkit.egg-info/top_level.txt +0 -0
- {strkit-0.12.0 → strkit-0.12.0a1}/tests/test_caller_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: strkit
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.0a1
|
|
4
4
|
Summary: A toolkit for analyzing variation in short(ish) tandem repeats.
|
|
5
5
|
Home-page: https://github.com/davidlougheed/strkit
|
|
6
6
|
Author: David Lougheed
|
|
@@ -16,7 +16,6 @@ Requires-Dist: Flask<3.1,>=2.2.5
|
|
|
16
16
|
Requires-Dist: pysam<0.23,>=0.19
|
|
17
17
|
Requires-Dist: numpy<1.27,>=1.23.4
|
|
18
18
|
Requires-Dist: parasail<1.4,>=1.2.4
|
|
19
|
-
Requires-Dist: pyfamsa<0.4,>=0.3.0
|
|
20
19
|
Requires-Dist: scikit-learn<1.4,>=1.2.1
|
|
21
20
|
Requires-Dist: scipy<1.12,>=1.10
|
|
22
21
|
Requires-Dist: statsmodels<0.15,>=0.14.0
|
|
@@ -116,8 +115,6 @@ long read data should still work.
|
|
|
116
115
|
* Whole-genome and targeted genotyping modes to adjust this re-weighting.
|
|
117
116
|
* Incorporation of single-nucleotide variation (SNVs) for better and faster calling plus
|
|
118
117
|
additional downstream analysis possibilities.
|
|
119
|
-
* Recommended for **HiFi data only**. In my testing, this worsens runtime and call quality for
|
|
120
|
-
ONT-UL data, but speeds up the tool and improves call quality for HiFi data.
|
|
121
118
|
* Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
|
|
122
119
|
* 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
|
|
123
120
|
|
|
@@ -131,11 +128,10 @@ strkit call \
|
|
|
131
128
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
132
129
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
133
130
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
134
|
-
--incorporate-snvs path/to/dbsnp.vcf.gz \ #
|
|
131
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
|
|
135
132
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
136
133
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
137
134
|
--flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
|
|
138
|
-
--consensus \ # If this flag is set, consensus sequences are calculated for alleles. This increases runtime!
|
|
139
135
|
--seed 183 \ # Fixed random number generator seed for replicability
|
|
140
136
|
--processes 10 # Number of parallel processes to use; DEFAULT: 1
|
|
141
137
|
```
|
|
@@ -164,9 +160,6 @@ specify a path to output a more detailed JSON document to. This document contain
|
|
|
164
160
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
165
161
|
file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
|
|
166
162
|
|
|
167
|
-
If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
|
|
168
|
-
`--vcf ...` argument.
|
|
169
|
-
|
|
170
163
|
See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
|
|
171
164
|
how to format a locus catalog or choose from existing available catalogs.
|
|
172
165
|
|
|
@@ -90,8 +90,6 @@ long read data should still work.
|
|
|
90
90
|
* Whole-genome and targeted genotyping modes to adjust this re-weighting.
|
|
91
91
|
* Incorporation of single-nucleotide variation (SNVs) for better and faster calling plus
|
|
92
92
|
additional downstream analysis possibilities.
|
|
93
|
-
* Recommended for **HiFi data only**. In my testing, this worsens runtime and call quality for
|
|
94
|
-
ONT-UL data, but speeds up the tool and improves call quality for HiFi data.
|
|
95
93
|
* Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
|
|
96
94
|
* 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
|
|
97
95
|
|
|
@@ -105,11 +103,10 @@ strkit call \
|
|
|
105
103
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
106
104
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
107
105
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
108
|
-
--incorporate-snvs path/to/dbsnp.vcf.gz \ #
|
|
106
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
|
|
109
107
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
110
108
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
111
109
|
--flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
|
|
112
|
-
--consensus \ # If this flag is set, consensus sequences are calculated for alleles. This increases runtime!
|
|
113
110
|
--seed 183 \ # Fixed random number generator seed for replicability
|
|
114
111
|
--processes 10 # Number of parallel processes to use; DEFAULT: 1
|
|
115
112
|
```
|
|
@@ -138,9 +135,6 @@ specify a path to output a more detailed JSON document to. This document contain
|
|
|
138
135
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
139
136
|
file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
|
|
140
137
|
|
|
141
|
-
If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
|
|
142
|
-
`--vcf ...` argument.
|
|
143
|
-
|
|
144
138
|
See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
|
|
145
139
|
how to format a locus catalog or choose from existing available catalogs.
|
|
146
140
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.12.0a1
|
|
@@ -18,11 +18,10 @@ from sklearn.cluster import AgglomerativeClustering
|
|
|
18
18
|
from numpy.typing import NDArray
|
|
19
19
|
from typing import Iterable, Literal, Optional, Union
|
|
20
20
|
|
|
21
|
-
from strkit.call.allele import CallDict, call_alleles
|
|
21
|
+
from strkit.call.allele import CallDict, get_n_alleles, call_alleles
|
|
22
22
|
from strkit.utils import apply_or_none
|
|
23
23
|
|
|
24
24
|
from .align_matrix import match_score
|
|
25
|
-
from .consensus import consensus_seq
|
|
26
25
|
from .realign import realign_read
|
|
27
26
|
from .repeats import get_repeat_count, get_ref_repeat_count
|
|
28
27
|
from .snvs import (
|
|
@@ -110,10 +109,6 @@ def get_read_coords_from_matched_pairs(
|
|
|
110
109
|
left_flank_start, _ = matched_pairs[lhs]
|
|
111
110
|
# ------------------------------------------------------------------------------------------------------------------
|
|
112
111
|
|
|
113
|
-
# Binary search for left flank end ---------------------------------------------------------------------------------
|
|
114
|
-
# TODO
|
|
115
|
-
# ------------------------------------------------------------------------------------------------------------------
|
|
116
|
-
|
|
117
112
|
for query_coord, ref_coord in matched_pairs[lhs+1:]:
|
|
118
113
|
# Skip gaps on either side to find mapped flank indices
|
|
119
114
|
if ref_coord < left_coord:
|
|
@@ -141,7 +136,7 @@ def get_read_coords_from_matched_pairs(
|
|
|
141
136
|
|
|
142
137
|
|
|
143
138
|
def get_overlapping_segments_and_related_data(
|
|
144
|
-
bfs: tuple[pysam.AlignmentFile
|
|
139
|
+
bfs: tuple[pysam.AlignmentFile],
|
|
145
140
|
read_contig: str,
|
|
146
141
|
left_flank_coord: int,
|
|
147
142
|
right_flank_coord: int,
|
|
@@ -556,7 +551,6 @@ def debug_log_flanking_seq(logger_: logging.Logger, locus_log_str: str, rn: str,
|
|
|
556
551
|
def call_locus(
|
|
557
552
|
t_idx: int,
|
|
558
553
|
t: tuple,
|
|
559
|
-
n_alleles: int,
|
|
560
554
|
bfs: tuple[AlignmentFile, ...],
|
|
561
555
|
ref: FastaFile,
|
|
562
556
|
min_reads: int,
|
|
@@ -567,6 +561,7 @@ def call_locus(
|
|
|
567
561
|
seed: int,
|
|
568
562
|
logger_: logging.Logger,
|
|
569
563
|
sample_id: Optional[str] = None,
|
|
564
|
+
sex_chroms: Optional[str] = None,
|
|
570
565
|
realign: bool = False,
|
|
571
566
|
hq: bool = False,
|
|
572
567
|
# incorporate_snvs: bool = False,
|
|
@@ -577,7 +572,6 @@ def call_locus(
|
|
|
577
572
|
fractional: bool = False,
|
|
578
573
|
respect_ref: bool = False,
|
|
579
574
|
count_kmers: str = "none", # "none" | "peak" | "read"
|
|
580
|
-
consensus: bool = False,
|
|
581
575
|
log_level: int = logging.WARNING,
|
|
582
576
|
read_file_has_chr: bool = True,
|
|
583
577
|
ref_file_has_chr: bool = True,
|
|
@@ -605,6 +599,10 @@ def call_locus(
|
|
|
605
599
|
ref_seq: str = ""
|
|
606
600
|
raised: bool = False
|
|
607
601
|
|
|
602
|
+
n_alleles: Optional[int] = get_n_alleles(2, sex_chroms, contig)
|
|
603
|
+
if n_alleles is None: # Sex chromosome, but we don't have a specified sex chromosome karyotype
|
|
604
|
+
return None
|
|
605
|
+
|
|
608
606
|
# Currently, only support diploid use of SNVs. There's not much of a point with haploid loci,
|
|
609
607
|
# and polyploidy is hard.
|
|
610
608
|
# should_incorporate_snvs: bool = incorporate_snvs and n_alleles == 2
|
|
@@ -801,7 +799,6 @@ def call_locus(
|
|
|
801
799
|
flank_len: int = len(flank_left_seq) + len(flank_right_seq)
|
|
802
800
|
tr_len_w_flank: int = tr_len + flank_len
|
|
803
801
|
|
|
804
|
-
tr_read_seq = qs[left_flank_end:right_flank_start]
|
|
805
802
|
tr_read_seq_wc = calculate_seq_with_wildcards(qs[left_flank_end:right_flank_start], qqs)
|
|
806
803
|
|
|
807
804
|
if count_kmers != "none":
|
|
@@ -851,7 +848,6 @@ def call_locus(
|
|
|
851
848
|
read_dict_extra[rn] = {
|
|
852
849
|
"_ref_start": segment_start,
|
|
853
850
|
"_ref_end": segment_end,
|
|
854
|
-
**({"_tr_seq": tr_read_seq} if consensus else {}),
|
|
855
851
|
}
|
|
856
852
|
|
|
857
853
|
# Reads can show up more than once - TODO - cache this information across loci
|
|
@@ -890,7 +886,6 @@ def call_locus(
|
|
|
890
886
|
}),
|
|
891
887
|
"motif": motif,
|
|
892
888
|
"ref_cn": ref_cn,
|
|
893
|
-
**({"ref_seq": ref_seq} if consensus else {}),
|
|
894
889
|
"reads": read_dict,
|
|
895
890
|
}
|
|
896
891
|
|
|
@@ -1013,13 +1008,12 @@ def call_locus(
|
|
|
1013
1008
|
call_stdevs = call_data.get("peak_stdevs")
|
|
1014
1009
|
call_modal_n = call_data.get("modal_n_peaks")
|
|
1015
1010
|
|
|
1016
|
-
# Assign reads to peaks and compute peak k-mers
|
|
1011
|
+
# Assign reads to peaks and compute peak k-mers --------------------------------------------------------------------
|
|
1017
1012
|
|
|
1018
1013
|
# We cannot call read-level cluster labels with >2 peaks using distance alone;
|
|
1019
1014
|
# don't know how re-sampling has occurred.
|
|
1020
1015
|
call_peak_n_reads: list[int] = []
|
|
1021
1016
|
peak_kmers: list[Counter] = [Counter() for _ in range(call_modal_n or 0)]
|
|
1022
|
-
call_seqs: list[str] = []
|
|
1023
1017
|
if read_peaks_called := call_modal_n and call_modal_n <= 2:
|
|
1024
1018
|
peaks: NDArray[np.float_] = call_peaks[:call_modal_n]
|
|
1025
1019
|
stdevs: NDArray[np.float_] = call_stdevs[:call_modal_n]
|
|
@@ -1065,11 +1059,6 @@ def call_locus(
|
|
|
1065
1059
|
|
|
1066
1060
|
call_peak_n_reads = list(map(len, allele_reads))
|
|
1067
1061
|
|
|
1068
|
-
if consensus:
|
|
1069
|
-
call_seqs = list(
|
|
1070
|
-
map(lambda a: consensus_seq(map(lambda rr: read_dict_extra[rr]["_tr_seq"], a)), allele_reads)
|
|
1071
|
-
)
|
|
1072
|
-
|
|
1073
1062
|
peak_data = {
|
|
1074
1063
|
"means": call_peaks.tolist(), # from np.ndarray
|
|
1075
1064
|
"weights": call_weights.tolist(), # from np.ndarray
|
|
@@ -1077,7 +1066,6 @@ def call_locus(
|
|
|
1077
1066
|
"modal_n": call_modal_n,
|
|
1078
1067
|
"n_reads": call_peak_n_reads,
|
|
1079
1068
|
**({"kmers": list(map(dict, peak_kmers))} if count_kmers in ("peak", "both") else {}),
|
|
1080
|
-
**({"seqs": call_seqs} if consensus else {}),
|
|
1081
1069
|
} if call_data else None
|
|
1082
1070
|
|
|
1083
1071
|
# Calculate call time ----------------------------------------------------------------------------------------------
|
|
@@ -9,19 +9,22 @@ import pathlib
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
|
+
import sys
|
|
12
13
|
import time
|
|
13
14
|
|
|
14
15
|
from datetime import datetime
|
|
15
16
|
from multiprocessing.synchronize import Event as EventClass # For type hinting
|
|
16
17
|
from pysam import AlignmentFile
|
|
17
18
|
|
|
18
|
-
from typing import Literal, Optional
|
|
19
|
+
from typing import Literal, Optional, Union
|
|
20
|
+
|
|
21
|
+
from strkit import __version__
|
|
22
|
+
|
|
23
|
+
from strkit.json import dumps_indented, json
|
|
19
24
|
from strkit.logger import logger
|
|
20
25
|
|
|
21
|
-
from .allele import get_n_alleles
|
|
22
26
|
from .call_locus import call_locus
|
|
23
27
|
from .non_daemonic_pool import NonDaemonicPool
|
|
24
|
-
from .output import output_json_report, output_tsv as output_tsv_fn, output_vcf
|
|
25
28
|
from .utils import get_new_seed
|
|
26
29
|
|
|
27
30
|
__all__ = [
|
|
@@ -45,6 +48,7 @@ def locus_worker(
|
|
|
45
48
|
num_bootstrap: int,
|
|
46
49
|
flank_size: int,
|
|
47
50
|
sample_id: Optional[str],
|
|
51
|
+
sex_chroms: Optional[str],
|
|
48
52
|
realign: bool,
|
|
49
53
|
hq: bool,
|
|
50
54
|
# incorporate_snvs: bool,
|
|
@@ -53,7 +57,6 @@ def locus_worker(
|
|
|
53
57
|
fractional: bool,
|
|
54
58
|
respect_ref: bool,
|
|
55
59
|
count_kmers: str,
|
|
56
|
-
consensus: bool,
|
|
57
60
|
log_level: int,
|
|
58
61
|
locus_queue: mp.Queue,
|
|
59
62
|
is_single_processed: bool,
|
|
@@ -99,9 +102,9 @@ def locus_worker(
|
|
|
99
102
|
if td is None: # Kill signal
|
|
100
103
|
break
|
|
101
104
|
|
|
102
|
-
t_idx, t,
|
|
105
|
+
t_idx, t, locus_seed = td
|
|
103
106
|
res = call_locus(
|
|
104
|
-
t_idx, t,
|
|
107
|
+
t_idx, t, bfs, ref,
|
|
105
108
|
min_reads=min_reads,
|
|
106
109
|
min_allele_reads=min_allele_reads,
|
|
107
110
|
min_avg_phred=min_avg_phred,
|
|
@@ -110,6 +113,7 @@ def locus_worker(
|
|
|
110
113
|
seed=locus_seed,
|
|
111
114
|
logger_=lg,
|
|
112
115
|
sample_id=sample_id,
|
|
116
|
+
sex_chroms=sex_chroms,
|
|
113
117
|
realign=realign,
|
|
114
118
|
hq=hq,
|
|
115
119
|
# incorporate_snvs=incorporate_snvs,
|
|
@@ -120,7 +124,6 @@ def locus_worker(
|
|
|
120
124
|
fractional=fractional,
|
|
121
125
|
respect_ref=respect_ref,
|
|
122
126
|
count_kmers=count_kmers,
|
|
123
|
-
consensus=consensus,
|
|
124
127
|
log_level=log_level,
|
|
125
128
|
read_file_has_chr=read_file_has_chr,
|
|
126
129
|
ref_file_has_chr=ref_file_has_chr,
|
|
@@ -179,6 +182,10 @@ def parse_loci_bed(loci_file: str):
|
|
|
179
182
|
yield from (tuple(line.split("\t")) for line in (s.strip() for s in tf) if line and not line.startswith("#"))
|
|
180
183
|
|
|
181
184
|
|
|
185
|
+
def _cn_to_str(cn: Union[int, float]) -> str:
|
|
186
|
+
return f"{cn:.1f}" if isinstance(cn, float) else str(cn)
|
|
187
|
+
|
|
188
|
+
|
|
182
189
|
def call_sample(
|
|
183
190
|
read_files: tuple[str, ...],
|
|
184
191
|
reference_file: str,
|
|
@@ -198,10 +205,8 @@ def call_sample(
|
|
|
198
205
|
fractional: bool = False,
|
|
199
206
|
respect_ref: bool = False,
|
|
200
207
|
count_kmers: str = "none", # "none" | "peak" | "read"
|
|
201
|
-
consensus: bool = False,
|
|
202
208
|
log_level: int = logging.WARNING,
|
|
203
209
|
json_path: Optional[str] = None,
|
|
204
|
-
vcf_path: Optional[str] = None,
|
|
205
210
|
indent_json: bool = False,
|
|
206
211
|
output_tsv: bool = True,
|
|
207
212
|
processes: int = 1,
|
|
@@ -248,17 +253,11 @@ def call_sample(
|
|
|
248
253
|
# Keep track of all contigs we are processing to speed up downstream Mendelian inheritance analysis.
|
|
249
254
|
contig_set: set[str] = set()
|
|
250
255
|
for t_idx, t in enumerate(parse_loci_bed(loci_file), 1):
|
|
251
|
-
contig = t[0]
|
|
252
|
-
|
|
253
|
-
n_alleles: Optional[int] = get_n_alleles(2, sex_chroms, contig)
|
|
254
|
-
if n_alleles is None: # Sex chromosome, but we don't have a specified sex chromosome karyotype
|
|
255
|
-
continue # --> skip the locus
|
|
256
|
-
|
|
257
256
|
# We use locus-specific random seeds for replicability, no matter which order
|
|
258
257
|
# the loci are yanked out of the queue / how many processes we have.
|
|
259
258
|
# Tuple of (1-indexed locus index, locus data, locus-specific random seed)
|
|
260
|
-
locus_queue.put((t_idx, t,
|
|
261
|
-
contig_set.add(
|
|
259
|
+
locus_queue.put((t_idx, t, get_new_seed(rng)))
|
|
260
|
+
contig_set.add(t[0])
|
|
262
261
|
num_loci += 1
|
|
263
262
|
|
|
264
263
|
# At the end of the queue, add a None value (* the # of processes).
|
|
@@ -276,6 +275,7 @@ def call_sample(
|
|
|
276
275
|
"num_bootstrap": num_bootstrap,
|
|
277
276
|
"flank_size": flank_size,
|
|
278
277
|
"sample_id": sample_id_final,
|
|
278
|
+
"sex_chroms": sex_chroms,
|
|
279
279
|
"realign": realign,
|
|
280
280
|
"hq": hq,
|
|
281
281
|
# "incorporate_snvs": incorporate_snvs,
|
|
@@ -284,7 +284,6 @@ def call_sample(
|
|
|
284
284
|
"fractional": fractional,
|
|
285
285
|
"respect_ref": respect_ref,
|
|
286
286
|
"count_kmers": count_kmers,
|
|
287
|
-
"consensus": consensus,
|
|
288
287
|
"log_level": log_level,
|
|
289
288
|
}
|
|
290
289
|
|
|
@@ -318,19 +317,54 @@ def call_sample(
|
|
|
318
317
|
time_taken = datetime.now() - start_time
|
|
319
318
|
|
|
320
319
|
if output_tsv:
|
|
321
|
-
|
|
320
|
+
for res in results:
|
|
321
|
+
has_call = res["call"] is not None
|
|
322
|
+
# n_peaks = res["peaks"]["modal_n"]
|
|
323
|
+
|
|
324
|
+
sys.stdout.write("\t".join((
|
|
325
|
+
res["contig"],
|
|
326
|
+
str(res["start"]),
|
|
327
|
+
str(res["end"]),
|
|
328
|
+
res["motif"],
|
|
329
|
+
_cn_to_str(res["ref_cn"]),
|
|
330
|
+
",".join(map(_cn_to_str, sorted(r["cn"] for r in res["reads"].values()))) if res["reads"] else ".",
|
|
331
|
+
"|".join(map(_cn_to_str, res["call"])) if has_call else ".",
|
|
332
|
+
("|".join("-".join(map(_cn_to_str, gc)) for gc in res["call_95_cis"]) if has_call else "."),
|
|
333
|
+
# *((res["assign_method"] if has_call else ".",) if incorporate_snvs else ()),
|
|
334
|
+
*((res["assign_method"] if has_call else ".",) if snv_vcf is not None else ()),
|
|
335
|
+
|
|
336
|
+
# ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["means"][:n_peaks]))
|
|
337
|
+
# if has_call and n_peaks <= 2 else "."),
|
|
338
|
+
# ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["weights"][:n_peaks]))
|
|
339
|
+
# if has_call and n_peaks <= 2 else "."),
|
|
340
|
+
# ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["stdevs"][:n_peaks]))
|
|
341
|
+
# if has_call and n_peaks <= 2 else "."),
|
|
342
|
+
)) + "\n")
|
|
322
343
|
|
|
323
344
|
if json_path:
|
|
324
|
-
|
|
325
|
-
sample_id_final,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
json_report = {
|
|
346
|
+
"sample_id": sample_id_final,
|
|
347
|
+
"caller": {
|
|
348
|
+
"name": "strkit",
|
|
349
|
+
"version": __version__,
|
|
350
|
+
},
|
|
351
|
+
"parameters": {
|
|
352
|
+
**job_params,
|
|
353
|
+
"processes": processes,
|
|
354
|
+
},
|
|
355
|
+
"runtime": time_taken.total_seconds(),
|
|
356
|
+
"contigs": tuple(contig_set),
|
|
357
|
+
"results": results,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
dfn = dumps_indented if indent_json else json.dumps
|
|
361
|
+
report_data = dfn(json_report)
|
|
362
|
+
|
|
363
|
+
if json_path == "stdout":
|
|
364
|
+
sys.stdout.buffer.write(report_data)
|
|
365
|
+
sys.stdout.write("\n")
|
|
366
|
+
sys.stdout.flush()
|
|
367
|
+
else:
|
|
368
|
+
with open(json_path, "wb") as jf:
|
|
369
|
+
jf.write(report_data)
|
|
370
|
+
jf.write(b"\n")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# import pysam
|
|
2
|
-
from typing import Literal, TypedDict, Union
|
|
2
|
+
from typing import Literal, Optional, TypedDict, Union
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
@@ -34,14 +34,12 @@ class ReadDict(_ReadDictBase, total=False):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class ReadDictExtra(TypedDict, total=False):
|
|
37
|
+
# Below are only added if SNVs are being incorporated:
|
|
38
|
+
|
|
37
39
|
_ref_start: int # Read start in ref coordinates
|
|
38
40
|
_ref_end: int # Read end in ref coordinates
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Below are only added if SNVs are being incorporated:
|
|
43
|
-
|
|
44
|
-
_qs: str # Query (read) sequence
|
|
42
|
+
_qs: str # Query (read) sequence... only added if SNVs are being incorporated
|
|
45
43
|
|
|
46
44
|
sig_clip_left: bool # Significant amounts of clipping (5' of read)
|
|
47
45
|
sig_clip_right: bool # Significant amounts of clipping (3' of read)
|
|
@@ -92,11 +92,6 @@ def add_call_parser_args(call_parser):
|
|
|
92
92
|
"expansion patterns. If set to 'peak', read-level counts will be aggregated into peak-level counts for "
|
|
93
93
|
"more efficient space and processing. This is the default if the flag is passed by itself.")
|
|
94
94
|
|
|
95
|
-
call_parser.add_argument(
|
|
96
|
-
"--consensus", "-c",
|
|
97
|
-
action="store_true",
|
|
98
|
-
help="Calculate consensus sequences for alleles. This flag increases runtime.")
|
|
99
|
-
|
|
100
95
|
call_parser.add_argument(
|
|
101
96
|
"--min-reads",
|
|
102
97
|
type=int,
|
|
@@ -142,9 +137,6 @@ def add_call_parser_args(call_parser):
|
|
|
142
137
|
help="Sex chromosome configuration to use for this sample (XX, XY, etc.) If left out, sex chromosomes will not "
|
|
143
138
|
"be genotyped.")
|
|
144
139
|
|
|
145
|
-
# BEGIN FILE OUTPUT ARGUMENTS ======================================================================================
|
|
146
|
-
|
|
147
|
-
# Begin JSON output arguments --------------------------------------------------------------------------------------
|
|
148
140
|
call_parser.add_argument(
|
|
149
141
|
"--json", "-j",
|
|
150
142
|
type=str,
|
|
@@ -156,23 +148,12 @@ def add_call_parser_args(call_parser):
|
|
|
156
148
|
action="store_true",
|
|
157
149
|
help="If passed alongside --json [x], the JSON output will be indented to be more human readable but "
|
|
158
150
|
"less compact.")
|
|
159
|
-
# End JSON output arguments ----------------------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
# Begin VCF output arguments ---------------------------------------------------------------------------------------
|
|
162
|
-
call_parser.add_argument(
|
|
163
|
-
"--vcf",
|
|
164
|
-
type=str,
|
|
165
|
-
help="Path to write VCF-formatted calls to. STR alleles are currently formatted symbolically rather than as "
|
|
166
|
-
"consensus sequences.")
|
|
167
|
-
# End VCF output arguments -----------------------------------------------------------------------------------------
|
|
168
151
|
|
|
169
152
|
call_parser.add_argument(
|
|
170
153
|
"--no-tsv",
|
|
171
154
|
action="store_true",
|
|
172
155
|
help="If passed, no TSV call output will be written to stdout.")
|
|
173
156
|
|
|
174
|
-
# END FILE OUTPUT ARGUMENTS ========================================================================================
|
|
175
|
-
|
|
176
157
|
call_parser.add_argument(
|
|
177
158
|
"--seed",
|
|
178
159
|
type=int,
|
|
@@ -370,11 +351,9 @@ def _exec_call(p_args) -> None:
|
|
|
370
351
|
fractional=p_args.fractional,
|
|
371
352
|
respect_ref=p_args.respect_ref,
|
|
372
353
|
count_kmers=p_args.count_kmers,
|
|
373
|
-
consensus=p_args.consensus or not (not p_args.vcf), # Consensus calculation is required for VCF output.
|
|
374
354
|
log_level=log_levels[p_args.log_level],
|
|
375
355
|
json_path=p_args.json,
|
|
376
356
|
indent_json=p_args.indent_json,
|
|
377
|
-
vcf_path=p_args.vcf,
|
|
378
357
|
output_tsv=not p_args.no_tsv,
|
|
379
358
|
processes=p_args.processes,
|
|
380
359
|
seed=p_args.seed,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: strkit
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.0a1
|
|
4
4
|
Summary: A toolkit for analyzing variation in short(ish) tandem repeats.
|
|
5
5
|
Home-page: https://github.com/davidlougheed/strkit
|
|
6
6
|
Author: David Lougheed
|
|
@@ -16,7 +16,6 @@ Requires-Dist: Flask<3.1,>=2.2.5
|
|
|
16
16
|
Requires-Dist: pysam<0.23,>=0.19
|
|
17
17
|
Requires-Dist: numpy<1.27,>=1.23.4
|
|
18
18
|
Requires-Dist: parasail<1.4,>=1.2.4
|
|
19
|
-
Requires-Dist: pyfamsa<0.4,>=0.3.0
|
|
20
19
|
Requires-Dist: scikit-learn<1.4,>=1.2.1
|
|
21
20
|
Requires-Dist: scipy<1.12,>=1.10
|
|
22
21
|
Requires-Dist: statsmodels<0.15,>=0.14.0
|
|
@@ -116,8 +115,6 @@ long read data should still work.
|
|
|
116
115
|
* Whole-genome and targeted genotyping modes to adjust this re-weighting.
|
|
117
116
|
* Incorporation of single-nucleotide variation (SNVs) for better and faster calling plus
|
|
118
117
|
additional downstream analysis possibilities.
|
|
119
|
-
* Recommended for **HiFi data only**. In my testing, this worsens runtime and call quality for
|
|
120
|
-
ONT-UL data, but speeds up the tool and improves call quality for HiFi data.
|
|
121
118
|
* Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
|
|
122
119
|
* 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
|
|
123
120
|
|
|
@@ -131,11 +128,10 @@ strkit call \
|
|
|
131
128
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
132
129
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
133
130
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
134
|
-
--incorporate-snvs path/to/dbsnp.vcf.gz \ #
|
|
131
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
|
|
135
132
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
136
133
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
137
134
|
--flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
|
|
138
|
-
--consensus \ # If this flag is set, consensus sequences are calculated for alleles. This increases runtime!
|
|
139
135
|
--seed 183 \ # Fixed random number generator seed for replicability
|
|
140
136
|
--processes 10 # Number of parallel processes to use; DEFAULT: 1
|
|
141
137
|
```
|
|
@@ -164,9 +160,6 @@ specify a path to output a more detailed JSON document to. This document contain
|
|
|
164
160
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
165
161
|
file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
|
|
166
162
|
|
|
167
|
-
If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
|
|
168
|
-
`--vcf ...` argument.
|
|
169
|
-
|
|
170
163
|
See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
|
|
171
164
|
how to format a locus catalog or choose from existing available catalogs.
|
|
172
165
|
|
|
@@ -23,17 +23,12 @@ strkit/call/allele.py
|
|
|
23
23
|
strkit/call/call_locus.py
|
|
24
24
|
strkit/call/call_sample.py
|
|
25
25
|
strkit/call/cigar.py
|
|
26
|
-
strkit/call/consensus.py
|
|
27
26
|
strkit/call/non_daemonic_pool.py
|
|
28
27
|
strkit/call/realign.py
|
|
29
28
|
strkit/call/repeats.py
|
|
30
29
|
strkit/call/snvs.py
|
|
31
30
|
strkit/call/types.py
|
|
32
31
|
strkit/call/utils.py
|
|
33
|
-
strkit/call/output/__init__.py
|
|
34
|
-
strkit/call/output/json_report.py
|
|
35
|
-
strkit/call/output/tsv.py
|
|
36
|
-
strkit/call/output/vcf.py
|
|
37
32
|
strkit/catalog/__init__.py
|
|
38
33
|
strkit/catalog/combine.py
|
|
39
34
|
strkit/convert/__init__.py
|
strkit-0.12.0/strkit/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.12.0
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pyfamsa
|
|
3
|
-
from scipy.stats import mode
|
|
4
|
-
from typing import Iterable
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"consensus_seq",
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
GAP_ORD = ord("-")
|
|
11
|
-
|
|
12
|
-
aligner = pyfamsa.Aligner(threads=1, guide_tree="upgma", keep_duplicates=True)
|
|
13
|
-
join_str = "".join
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _not_gap(x: int) -> bool:
|
|
17
|
-
return x != GAP_ORD
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def consensus_seq(seqs: Iterable[str]) -> str:
|
|
21
|
-
# Align all the allele tandem repeat sequences together into a multiple sequence alignment
|
|
22
|
-
msa = aligner.align(pyfamsa.Sequence(f"s{i}".encode("ascii"), seq.encode("ascii")) for i, seq in enumerate(seqs))
|
|
23
|
-
# Turn the MSA into a numpy matrix as setup for calculating the modal value at each position
|
|
24
|
-
msa_grid = np.array(list(map(lambda aseq: list(map(int, aseq.sequence)), msa)))
|
|
25
|
-
# Return a stringified, gapless version of the column-wise mode for the MSA
|
|
26
|
-
return join_str(map(chr, filter(_not_gap, getattr(mode(msa_grid), "mode", ()))))
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from datetime import timedelta
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
from strkit import __version__
|
|
6
|
-
from strkit.json import dumps_indented, json
|
|
7
|
-
|
|
8
|
-
__all__ = ["output_json_report"]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def output_json_report(
|
|
12
|
-
sample_id: Optional[str],
|
|
13
|
-
job_params: dict,
|
|
14
|
-
processes: int,
|
|
15
|
-
time_taken: timedelta,
|
|
16
|
-
contig_set: set[str],
|
|
17
|
-
results: tuple[dict, ...],
|
|
18
|
-
json_path: str,
|
|
19
|
-
indent_json: bool,
|
|
20
|
-
):
|
|
21
|
-
json_report = {
|
|
22
|
-
"sample_id": sample_id,
|
|
23
|
-
"caller": {
|
|
24
|
-
"name": "strkit",
|
|
25
|
-
"version": __version__,
|
|
26
|
-
},
|
|
27
|
-
"parameters": {
|
|
28
|
-
**job_params,
|
|
29
|
-
"processes": processes,
|
|
30
|
-
},
|
|
31
|
-
"runtime": time_taken.total_seconds(),
|
|
32
|
-
"contigs": tuple(contig_set),
|
|
33
|
-
"results": results,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
dfn = dumps_indented if indent_json else json.dumps
|
|
37
|
-
report_data = dfn(json_report)
|
|
38
|
-
|
|
39
|
-
if json_path == "stdout":
|
|
40
|
-
sys.stdout.buffer.write(report_data)
|
|
41
|
-
sys.stdout.write("\n")
|
|
42
|
-
sys.stdout.flush()
|
|
43
|
-
else:
|
|
44
|
-
with open(json_path, "wb") as jf:
|
|
45
|
-
jf.write(report_data)
|
|
46
|
-
jf.write(b"\n")
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from typing import Union
|
|
3
|
-
|
|
4
|
-
__all__ = ["output_tsv"]
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def _cn_to_str(cn: Union[int, float]) -> str:
|
|
8
|
-
return f"{cn:.1f}" if isinstance(cn, float) else str(cn)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def output_tsv(results: tuple[dict, ...], has_snv_vcf: bool):
|
|
12
|
-
for res in results:
|
|
13
|
-
has_call = res["call"] is not None
|
|
14
|
-
# n_peaks = res["peaks"]["modal_n"]
|
|
15
|
-
|
|
16
|
-
sys.stdout.write("\t".join((
|
|
17
|
-
res["contig"],
|
|
18
|
-
str(res["start"]),
|
|
19
|
-
str(res["end"]),
|
|
20
|
-
res["motif"],
|
|
21
|
-
_cn_to_str(res["ref_cn"]),
|
|
22
|
-
",".join(map(_cn_to_str, sorted(r["cn"] for r in res["reads"].values()))) if res["reads"] else ".",
|
|
23
|
-
"|".join(map(_cn_to_str, res["call"])) if has_call else ".",
|
|
24
|
-
("|".join("-".join(map(_cn_to_str, gc)) for gc in res["call_95_cis"]) if has_call else "."),
|
|
25
|
-
# *((res["assign_method"] if has_call else ".",) if incorporate_snvs else ()),
|
|
26
|
-
*((res["assign_method"] if has_call else ".",) if has_snv_vcf else ()),
|
|
27
|
-
|
|
28
|
-
# ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["means"][:n_peaks]))
|
|
29
|
-
# if has_call and n_peaks <= 2 else "."),
|
|
30
|
-
# ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["weights"][:n_peaks]))
|
|
31
|
-
# if has_call and n_peaks <= 2 else "."),
|
|
32
|
-
# ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["stdevs"][:n_peaks]))
|
|
33
|
-
# if has_call and n_peaks <= 2 else "."),
|
|
34
|
-
)) + "\n")
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
import pysam
|
|
3
|
-
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
__all__ = ["output_vcf"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# VCF_ALLELE_CNV_TR = "<CNV:TR>"
|
|
10
|
-
|
|
11
|
-
# VCF_TR_INFO_RECORDS: tuple[tuple[str, str, str, str], ...] = (
|
|
12
|
-
# ("SVLEN", "A", "Integer", "Length of the structural variant"),
|
|
13
|
-
# ("CN", "A", "Float", "Copy number of allele"),
|
|
14
|
-
# ("RN", "A", "Integer", "Total number of repeat sequences in this allele"),
|
|
15
|
-
# ("RUS", ".", "String", "Repeat unit sequence of the corresponding repeat sequence"),
|
|
16
|
-
# ("RUL", ".", "Integer", "Repeat unit length of the corresponding repeat sequence"),
|
|
17
|
-
# ("RB", ".", "Integer", "Total number of bases in the corresponding repeat sequence"),
|
|
18
|
-
# ("CIRUC", ".", "Float", "Confidence interval around RUC"),
|
|
19
|
-
# ("CIRB", ".", "Integer", "Confidence interval around RB"),
|
|
20
|
-
# )
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _build_variant_header(sample_id: str, reference_file: str) -> pysam.VariantHeader:
|
|
24
|
-
vh = pysam.VariantHeader() # automatically sets VCF version to 4.2
|
|
25
|
-
|
|
26
|
-
# Add an absolute path to the reference genome
|
|
27
|
-
vh.add_meta("reference", f"file://{str(pathlib.Path(reference_file).absolute())}")
|
|
28
|
-
|
|
29
|
-
# Add all contigs from the reference genome file + lengths
|
|
30
|
-
rf = pysam.FastaFile(reference_file)
|
|
31
|
-
try:
|
|
32
|
-
for contig in rf.references:
|
|
33
|
-
vh.contigs.add(contig, length=rf.get_reference_length(contig))
|
|
34
|
-
finally:
|
|
35
|
-
rf.close()
|
|
36
|
-
|
|
37
|
-
# Add CNV:TR alt type (symbolic allele: tandem repeat)
|
|
38
|
-
vh.add_meta("ALT", "<ID=CNV:TR,Description=\"Tandem repeat\">")
|
|
39
|
-
|
|
40
|
-
# Set up basic VCF formats
|
|
41
|
-
vh.formats.add("AD", "R", "Integer", "Read depth for each allele")
|
|
42
|
-
vh.formats.add("DP", 1, "Integer", "Read depth")
|
|
43
|
-
vh.formats.add("GT", 1, "String", "Genotype")
|
|
44
|
-
# vh.formats.add("PS", 1, "String", "Phase set") TODO
|
|
45
|
-
|
|
46
|
-
# Add INFO records for tandem repeat copies - these are new to VCF4.4! TODO
|
|
47
|
-
# for iv in VCF_TR_INFO_RECORDS:
|
|
48
|
-
# vh.info.add(*iv)
|
|
49
|
-
|
|
50
|
-
# Add the sample
|
|
51
|
-
vh.add_sample(sample_id)
|
|
52
|
-
|
|
53
|
-
return vh
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _vr_pos_key(vr: pysam.VariantRecord) -> int:
|
|
57
|
-
return vr.pos
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def output_vcf(
|
|
61
|
-
sample_id: Optional[str],
|
|
62
|
-
reference_file: str,
|
|
63
|
-
results: tuple[dict, ...],
|
|
64
|
-
vcf_path: str,
|
|
65
|
-
):
|
|
66
|
-
sample_id_str: str = sample_id or "sample"
|
|
67
|
-
|
|
68
|
-
vh = _build_variant_header(sample_id_str, reference_file)
|
|
69
|
-
vf = pysam.VariantFile(vcf_path, "w", header=vh)
|
|
70
|
-
|
|
71
|
-
contig_vrs: list[pysam.VariantRecord] = []
|
|
72
|
-
|
|
73
|
-
def _write_contig_vrs():
|
|
74
|
-
# sort the variant records by position
|
|
75
|
-
contig_vrs.sort(key=_vr_pos_key)
|
|
76
|
-
|
|
77
|
-
# write them to the VCF
|
|
78
|
-
for contig_vr in contig_vrs:
|
|
79
|
-
vf.write(contig_vr)
|
|
80
|
-
|
|
81
|
-
# clear the contig variant record list for the new contig
|
|
82
|
-
contig_vrs.clear()
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
last_contig = results[0]["contig"] if results else ""
|
|
86
|
-
|
|
87
|
-
has_at_least_one_snv_set = next((r.get("snvs") is not None for r in results), None) is not None
|
|
88
|
-
|
|
89
|
-
for result_idx, result in enumerate(results, 1):
|
|
90
|
-
contig = result["contig"]
|
|
91
|
-
|
|
92
|
-
if contig != last_contig:
|
|
93
|
-
# we moved on from the last contig, so write the last batch of variant records to the VCF
|
|
94
|
-
_write_contig_vrs()
|
|
95
|
-
|
|
96
|
-
# TODO: fix-up coordinates / note positioning
|
|
97
|
-
start = result["start_adj"]
|
|
98
|
-
end = result["end_adj"]
|
|
99
|
-
call = result["call"]
|
|
100
|
-
|
|
101
|
-
# ref_cn = result["ref_cn"]
|
|
102
|
-
ref_seq = result["ref_seq"].lower()
|
|
103
|
-
|
|
104
|
-
seqs = tuple(map(str.lower, (result["peaks"] or {}).get("seqs", ())))
|
|
105
|
-
|
|
106
|
-
# cn_alts = sorted(set(c for c in call if c != ref_cn)) if call is not None else ()
|
|
107
|
-
seq_alts = sorted(set(filter(lambda c: c != ref_seq, seqs)))
|
|
108
|
-
|
|
109
|
-
# cn_alleles = (ref_cn, *cn_alts) if call is not None else (".",)
|
|
110
|
-
seq_alleles = (ref_seq, *(seq_alts or (".",))) if call is not None else (".",)
|
|
111
|
-
|
|
112
|
-
vr: pysam.VariantRecord = vf.new_record(
|
|
113
|
-
contig=contig,
|
|
114
|
-
start=start,
|
|
115
|
-
stop=end,
|
|
116
|
-
alleles=seq_alleles,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# TODO: right now we only call one repeat in one allele and use the canonical motif sequence, even though
|
|
120
|
-
# there may be multiple motifs present.
|
|
121
|
-
# motif = result["motif"]
|
|
122
|
-
# vr.info["SVLEN"] = math.ceil(end - start)
|
|
123
|
-
# vr.info["CN"] = tuple(a / ref_cn for a in alts) if ref_cn else tuple([1] * len(alts))
|
|
124
|
-
# vr.info["RN"] = tuple([1] * len(alts))
|
|
125
|
-
# vr.info["RUS"] = tuple([motif] * len(alts))
|
|
126
|
-
# vr.info["RUL"] = tuple([len(motif)] * len(alts))
|
|
127
|
-
|
|
128
|
-
# TODO: set up tandem repeat info fields
|
|
129
|
-
|
|
130
|
-
vr.samples[sample_id_str]["GT"] = tuple(map(seq_alleles.index, seqs)) if seqs else (".",)
|
|
131
|
-
vr.samples[sample_id_str]["DP"] = sum(result["peaks"]["n_reads"])
|
|
132
|
-
|
|
133
|
-
# TODO: output SNVs
|
|
134
|
-
|
|
135
|
-
# if has_at_least_one_snv_set:
|
|
136
|
-
# vr.samples[sample_id_str].phased = True
|
|
137
|
-
# vr.samples[sample_id_str]["PS"] = str(result_idx)
|
|
138
|
-
|
|
139
|
-
# for snv in result.get("snvs", ()):
|
|
140
|
-
# print(snv)
|
|
141
|
-
#
|
|
142
|
-
# ref = snv["ref"]
|
|
143
|
-
# snv_alts = tuple(filter(lambda v: v != ref, snv["call"]))
|
|
144
|
-
# snv_alleles = (ref, *snv_alts)
|
|
145
|
-
#
|
|
146
|
-
# snv_vr = vf.new_record(
|
|
147
|
-
# contig=contig,
|
|
148
|
-
# id=snv["id"],
|
|
149
|
-
# # TODO: fix-up coordinates
|
|
150
|
-
# start=snv["pos"],
|
|
151
|
-
# stop=snv["pos"] + 1,
|
|
152
|
-
# alleles=snv_alleles,
|
|
153
|
-
# )
|
|
154
|
-
#
|
|
155
|
-
# # TODO: write "rcs" for sample SNV genotypes - list of #reads per allele
|
|
156
|
-
#
|
|
157
|
-
# snv_vr.samples[sample_id_str]["GT"] = tuple(map(snv_alleles.index, snv["call"]))
|
|
158
|
-
# # snv_vr.samples[sample_id_str].phased = True
|
|
159
|
-
# snv_vr.samples[sample_id_str]["PS"] = str(result_idx)
|
|
160
|
-
#
|
|
161
|
-
# contig_vrs.append(snv_vr)
|
|
162
|
-
#
|
|
163
|
-
# # TODO: now is the time to figure out phasing SNVs across different TRs
|
|
164
|
-
|
|
165
|
-
contig_vrs.append(vr)
|
|
166
|
-
|
|
167
|
-
_write_contig_vrs() # write the final contig's worth of variant records to the VCF at the end
|
|
168
|
-
|
|
169
|
-
finally:
|
|
170
|
-
vf.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|