strkit 0.11.7__tar.gz → 0.12.0__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.11.7/strkit.egg-info → strkit-0.12.0}/PKG-INFO +9 -2
- {strkit-0.11.7 → strkit-0.12.0}/README.md +7 -1
- {strkit-0.11.7 → strkit-0.12.0}/setup.py +1 -0
- strkit-0.12.0/strkit/VERSION +1 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/call_locus.py +20 -8
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/call_sample.py +32 -66
- strkit-0.12.0/strkit/call/consensus.py +26 -0
- strkit-0.12.0/strkit/call/output/__init__.py +9 -0
- strkit-0.12.0/strkit/call/output/json_report.py +46 -0
- strkit-0.12.0/strkit/call/output/tsv.py +34 -0
- strkit-0.12.0/strkit/call/output/vcf.py +170 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/types.py +6 -4
- {strkit-0.11.7 → strkit-0.12.0}/strkit/constants.py +0 -13
- {strkit-0.11.7 → strkit-0.12.0}/strkit/entry.py +25 -7
- strkit-0.12.0/strkit/mi/repeathmm.py +92 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/result.py +6 -6
- strkit-0.12.0/strkit/mi/straglr.py +98 -0
- strkit-0.12.0/strkit/mi/tandem_genotypes.py +88 -0
- {strkit-0.11.7 → strkit-0.12.0/strkit.egg-info}/PKG-INFO +9 -2
- {strkit-0.11.7 → strkit-0.12.0}/strkit.egg-info/SOURCES.txt +5 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit.egg-info/requires.txt +1 -0
- strkit-0.11.7/strkit/VERSION +0 -1
- strkit-0.11.7/strkit/mi/repeathmm.py +0 -192
- strkit-0.11.7/strkit/mi/straglr.py +0 -201
- strkit-0.11.7/strkit/mi/tandem_genotypes.py +0 -172
- {strkit-0.11.7 → strkit-0.12.0}/LICENSE +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/MANIFEST.in +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/pyproject.toml +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/setup.cfg +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/__init__.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/__init__.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/align_matrix.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/allele.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/cigar.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/non_daemonic_pool.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/realign.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/repeats.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/snvs.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/call/utils.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/catalog/__init__.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/catalog/combine.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/__init__.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/_bed_4.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/converter.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/expansionhunter.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/gangstr.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/hipstr.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/straglr.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/convert/tandem_genotypes.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/exceptions.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/json.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/logger.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/__init__.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/base.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/expansionhunter.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/gangstr.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/strkit.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/mi/vcf_utils.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/utils.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/viz/__init__.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/viz/server.py +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/viz/static/logo.png +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit/viz/templates/browser.html +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit.egg-info/dependency_links.txt +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit.egg-info/entry_points.txt +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/strkit.egg-info/top_level.txt +0 -0
- {strkit-0.11.7 → strkit-0.12.0}/tests/test_caller_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: strkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
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,6 +16,7 @@ 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
|
|
19
20
|
Requires-Dist: scikit-learn<1.4,>=1.2.1
|
|
20
21
|
Requires-Dist: scipy<1.12,>=1.10
|
|
21
22
|
Requires-Dist: statsmodels<0.15,>=0.14.0
|
|
@@ -115,6 +116,8 @@ long read data should still work.
|
|
|
115
116
|
* Whole-genome and targeted genotyping modes to adjust this re-weighting.
|
|
116
117
|
* Incorporation of single-nucleotide variation (SNVs) for better and faster calling plus
|
|
117
118
|
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.
|
|
118
121
|
* Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
|
|
119
122
|
* 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
|
|
120
123
|
|
|
@@ -128,10 +131,11 @@ strkit call \
|
|
|
128
131
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
129
132
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
130
133
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
131
|
-
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up
|
|
134
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # [RECOMMENDED FOR HIFI ONLY] If you want SNV calls to help phase STRs & speed up calling
|
|
132
135
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
133
136
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
134
137
|
--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!
|
|
135
139
|
--seed 183 \ # Fixed random number generator seed for replicability
|
|
136
140
|
--processes 10 # Number of parallel processes to use; DEFAULT: 1
|
|
137
141
|
```
|
|
@@ -160,6 +164,9 @@ specify a path to output a more detailed JSON document to. This document contain
|
|
|
160
164
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
161
165
|
file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
|
|
162
166
|
|
|
167
|
+
If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
|
|
168
|
+
`--vcf ...` argument.
|
|
169
|
+
|
|
163
170
|
See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
|
|
164
171
|
how to format a locus catalog or choose from existing available catalogs.
|
|
165
172
|
|
|
@@ -90,6 +90,8 @@ 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.
|
|
93
95
|
* Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
|
|
94
96
|
* 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
|
|
95
97
|
|
|
@@ -103,10 +105,11 @@ strkit call \
|
|
|
103
105
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
104
106
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
105
107
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
106
|
-
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up
|
|
108
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # [RECOMMENDED FOR HIFI ONLY] If you want SNV calls to help phase STRs & speed up calling
|
|
107
109
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
108
110
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
109
111
|
--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!
|
|
110
113
|
--seed 183 \ # Fixed random number generator seed for replicability
|
|
111
114
|
--processes 10 # Number of parallel processes to use; DEFAULT: 1
|
|
112
115
|
```
|
|
@@ -135,6 +138,9 @@ specify a path to output a more detailed JSON document to. This document contain
|
|
|
135
138
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
136
139
|
file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
|
|
137
140
|
|
|
141
|
+
If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
|
|
142
|
+
`--vcf ...` argument.
|
|
143
|
+
|
|
138
144
|
See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
|
|
139
145
|
how to format a locus catalog or choose from existing available catalogs.
|
|
140
146
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.12.0
|
|
@@ -18,10 +18,11 @@ 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,
|
|
21
|
+
from strkit.call.allele import CallDict, 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
|
|
25
26
|
from .realign import realign_read
|
|
26
27
|
from .repeats import get_repeat_count, get_ref_repeat_count
|
|
27
28
|
from .snvs import (
|
|
@@ -109,6 +110,10 @@ def get_read_coords_from_matched_pairs(
|
|
|
109
110
|
left_flank_start, _ = matched_pairs[lhs]
|
|
110
111
|
# ------------------------------------------------------------------------------------------------------------------
|
|
111
112
|
|
|
113
|
+
# Binary search for left flank end ---------------------------------------------------------------------------------
|
|
114
|
+
# TODO
|
|
115
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
116
|
+
|
|
112
117
|
for query_coord, ref_coord in matched_pairs[lhs+1:]:
|
|
113
118
|
# Skip gaps on either side to find mapped flank indices
|
|
114
119
|
if ref_coord < left_coord:
|
|
@@ -136,7 +141,7 @@ def get_read_coords_from_matched_pairs(
|
|
|
136
141
|
|
|
137
142
|
|
|
138
143
|
def get_overlapping_segments_and_related_data(
|
|
139
|
-
bfs: tuple[pysam.AlignmentFile],
|
|
144
|
+
bfs: tuple[pysam.AlignmentFile, ...],
|
|
140
145
|
read_contig: str,
|
|
141
146
|
left_flank_coord: int,
|
|
142
147
|
right_flank_coord: int,
|
|
@@ -551,6 +556,7 @@ def debug_log_flanking_seq(logger_: logging.Logger, locus_log_str: str, rn: str,
|
|
|
551
556
|
def call_locus(
|
|
552
557
|
t_idx: int,
|
|
553
558
|
t: tuple,
|
|
559
|
+
n_alleles: int,
|
|
554
560
|
bfs: tuple[AlignmentFile, ...],
|
|
555
561
|
ref: FastaFile,
|
|
556
562
|
min_reads: int,
|
|
@@ -561,7 +567,6 @@ def call_locus(
|
|
|
561
567
|
seed: int,
|
|
562
568
|
logger_: logging.Logger,
|
|
563
569
|
sample_id: Optional[str] = None,
|
|
564
|
-
sex_chroms: Optional[str] = None,
|
|
565
570
|
realign: bool = False,
|
|
566
571
|
hq: bool = False,
|
|
567
572
|
# incorporate_snvs: bool = False,
|
|
@@ -572,6 +577,7 @@ def call_locus(
|
|
|
572
577
|
fractional: bool = False,
|
|
573
578
|
respect_ref: bool = False,
|
|
574
579
|
count_kmers: str = "none", # "none" | "peak" | "read"
|
|
580
|
+
consensus: bool = False,
|
|
575
581
|
log_level: int = logging.WARNING,
|
|
576
582
|
read_file_has_chr: bool = True,
|
|
577
583
|
ref_file_has_chr: bool = True,
|
|
@@ -599,10 +605,6 @@ def call_locus(
|
|
|
599
605
|
ref_seq: str = ""
|
|
600
606
|
raised: bool = False
|
|
601
607
|
|
|
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
|
-
|
|
606
608
|
# Currently, only support diploid use of SNVs. There's not much of a point with haploid loci,
|
|
607
609
|
# and polyploidy is hard.
|
|
608
610
|
# should_incorporate_snvs: bool = incorporate_snvs and n_alleles == 2
|
|
@@ -799,6 +801,7 @@ def call_locus(
|
|
|
799
801
|
flank_len: int = len(flank_left_seq) + len(flank_right_seq)
|
|
800
802
|
tr_len_w_flank: int = tr_len + flank_len
|
|
801
803
|
|
|
804
|
+
tr_read_seq = qs[left_flank_end:right_flank_start]
|
|
802
805
|
tr_read_seq_wc = calculate_seq_with_wildcards(qs[left_flank_end:right_flank_start], qqs)
|
|
803
806
|
|
|
804
807
|
if count_kmers != "none":
|
|
@@ -848,6 +851,7 @@ def call_locus(
|
|
|
848
851
|
read_dict_extra[rn] = {
|
|
849
852
|
"_ref_start": segment_start,
|
|
850
853
|
"_ref_end": segment_end,
|
|
854
|
+
**({"_tr_seq": tr_read_seq} if consensus else {}),
|
|
851
855
|
}
|
|
852
856
|
|
|
853
857
|
# Reads can show up more than once - TODO - cache this information across loci
|
|
@@ -886,6 +890,7 @@ def call_locus(
|
|
|
886
890
|
}),
|
|
887
891
|
"motif": motif,
|
|
888
892
|
"ref_cn": ref_cn,
|
|
893
|
+
**({"ref_seq": ref_seq} if consensus else {}),
|
|
889
894
|
"reads": read_dict,
|
|
890
895
|
}
|
|
891
896
|
|
|
@@ -1008,12 +1013,13 @@ def call_locus(
|
|
|
1008
1013
|
call_stdevs = call_data.get("peak_stdevs")
|
|
1009
1014
|
call_modal_n = call_data.get("modal_n_peaks")
|
|
1010
1015
|
|
|
1011
|
-
# Assign reads to peaks and compute peak k-mers
|
|
1016
|
+
# Assign reads to peaks and compute peak k-mers (and optionally consensus sequences) -------------------------------
|
|
1012
1017
|
|
|
1013
1018
|
# We cannot call read-level cluster labels with >2 peaks using distance alone;
|
|
1014
1019
|
# don't know how re-sampling has occurred.
|
|
1015
1020
|
call_peak_n_reads: list[int] = []
|
|
1016
1021
|
peak_kmers: list[Counter] = [Counter() for _ in range(call_modal_n or 0)]
|
|
1022
|
+
call_seqs: list[str] = []
|
|
1017
1023
|
if read_peaks_called := call_modal_n and call_modal_n <= 2:
|
|
1018
1024
|
peaks: NDArray[np.float_] = call_peaks[:call_modal_n]
|
|
1019
1025
|
stdevs: NDArray[np.float_] = call_stdevs[:call_modal_n]
|
|
@@ -1059,6 +1065,11 @@ def call_locus(
|
|
|
1059
1065
|
|
|
1060
1066
|
call_peak_n_reads = list(map(len, allele_reads))
|
|
1061
1067
|
|
|
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
|
+
|
|
1062
1073
|
peak_data = {
|
|
1063
1074
|
"means": call_peaks.tolist(), # from np.ndarray
|
|
1064
1075
|
"weights": call_weights.tolist(), # from np.ndarray
|
|
@@ -1066,6 +1077,7 @@ def call_locus(
|
|
|
1066
1077
|
"modal_n": call_modal_n,
|
|
1067
1078
|
"n_reads": call_peak_n_reads,
|
|
1068
1079
|
**({"kmers": list(map(dict, peak_kmers))} if count_kmers in ("peak", "both") else {}),
|
|
1080
|
+
**({"seqs": call_seqs} if consensus else {}),
|
|
1069
1081
|
} if call_data else None
|
|
1070
1082
|
|
|
1071
1083
|
# Calculate call time ----------------------------------------------------------------------------------------------
|
|
@@ -9,22 +9,19 @@ import pathlib
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
|
-
import sys
|
|
13
12
|
import time
|
|
14
13
|
|
|
15
14
|
from datetime import datetime
|
|
16
15
|
from multiprocessing.synchronize import Event as EventClass # For type hinting
|
|
17
16
|
from pysam import AlignmentFile
|
|
18
17
|
|
|
19
|
-
from typing import Literal, Optional
|
|
20
|
-
|
|
21
|
-
from strkit import __version__
|
|
22
|
-
|
|
23
|
-
from strkit.json import dumps_indented, json
|
|
18
|
+
from typing import Literal, Optional
|
|
24
19
|
from strkit.logger import logger
|
|
25
20
|
|
|
21
|
+
from .allele import get_n_alleles
|
|
26
22
|
from .call_locus import call_locus
|
|
27
23
|
from .non_daemonic_pool import NonDaemonicPool
|
|
24
|
+
from .output import output_json_report, output_tsv as output_tsv_fn, output_vcf
|
|
28
25
|
from .utils import get_new_seed
|
|
29
26
|
|
|
30
27
|
__all__ = [
|
|
@@ -48,7 +45,6 @@ def locus_worker(
|
|
|
48
45
|
num_bootstrap: int,
|
|
49
46
|
flank_size: int,
|
|
50
47
|
sample_id: Optional[str],
|
|
51
|
-
sex_chroms: Optional[str],
|
|
52
48
|
realign: bool,
|
|
53
49
|
hq: bool,
|
|
54
50
|
# incorporate_snvs: bool,
|
|
@@ -57,6 +53,7 @@ def locus_worker(
|
|
|
57
53
|
fractional: bool,
|
|
58
54
|
respect_ref: bool,
|
|
59
55
|
count_kmers: str,
|
|
56
|
+
consensus: bool,
|
|
60
57
|
log_level: int,
|
|
61
58
|
locus_queue: mp.Queue,
|
|
62
59
|
is_single_processed: bool,
|
|
@@ -102,9 +99,9 @@ def locus_worker(
|
|
|
102
99
|
if td is None: # Kill signal
|
|
103
100
|
break
|
|
104
101
|
|
|
105
|
-
t_idx, t, locus_seed = td
|
|
102
|
+
t_idx, t, n_alleles, locus_seed = td
|
|
106
103
|
res = call_locus(
|
|
107
|
-
t_idx, t, bfs, ref,
|
|
104
|
+
t_idx, t, n_alleles, bfs, ref,
|
|
108
105
|
min_reads=min_reads,
|
|
109
106
|
min_allele_reads=min_allele_reads,
|
|
110
107
|
min_avg_phred=min_avg_phred,
|
|
@@ -113,7 +110,6 @@ def locus_worker(
|
|
|
113
110
|
seed=locus_seed,
|
|
114
111
|
logger_=lg,
|
|
115
112
|
sample_id=sample_id,
|
|
116
|
-
sex_chroms=sex_chroms,
|
|
117
113
|
realign=realign,
|
|
118
114
|
hq=hq,
|
|
119
115
|
# incorporate_snvs=incorporate_snvs,
|
|
@@ -124,6 +120,7 @@ def locus_worker(
|
|
|
124
120
|
fractional=fractional,
|
|
125
121
|
respect_ref=respect_ref,
|
|
126
122
|
count_kmers=count_kmers,
|
|
123
|
+
consensus=consensus,
|
|
127
124
|
log_level=log_level,
|
|
128
125
|
read_file_has_chr=read_file_has_chr,
|
|
129
126
|
ref_file_has_chr=ref_file_has_chr,
|
|
@@ -182,10 +179,6 @@ def parse_loci_bed(loci_file: str):
|
|
|
182
179
|
yield from (tuple(line.split("\t")) for line in (s.strip() for s in tf) if line and not line.startswith("#"))
|
|
183
180
|
|
|
184
181
|
|
|
185
|
-
def _cn_to_str(cn: Union[int, float]) -> str:
|
|
186
|
-
return f"{cn:.1f}" if isinstance(cn, float) else str(cn)
|
|
187
|
-
|
|
188
|
-
|
|
189
182
|
def call_sample(
|
|
190
183
|
read_files: tuple[str, ...],
|
|
191
184
|
reference_file: str,
|
|
@@ -205,8 +198,10 @@ def call_sample(
|
|
|
205
198
|
fractional: bool = False,
|
|
206
199
|
respect_ref: bool = False,
|
|
207
200
|
count_kmers: str = "none", # "none" | "peak" | "read"
|
|
201
|
+
consensus: bool = False,
|
|
208
202
|
log_level: int = logging.WARNING,
|
|
209
203
|
json_path: Optional[str] = None,
|
|
204
|
+
vcf_path: Optional[str] = None,
|
|
210
205
|
indent_json: bool = False,
|
|
211
206
|
output_tsv: bool = True,
|
|
212
207
|
processes: int = 1,
|
|
@@ -253,11 +248,17 @@ def call_sample(
|
|
|
253
248
|
# Keep track of all contigs we are processing to speed up downstream Mendelian inheritance analysis.
|
|
254
249
|
contig_set: set[str] = set()
|
|
255
250
|
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
|
+
|
|
256
257
|
# We use locus-specific random seeds for replicability, no matter which order
|
|
257
258
|
# the loci are yanked out of the queue / how many processes we have.
|
|
258
259
|
# Tuple of (1-indexed locus index, locus data, locus-specific random seed)
|
|
259
|
-
locus_queue.put((t_idx, t, get_new_seed(rng)))
|
|
260
|
-
contig_set.add(
|
|
260
|
+
locus_queue.put((t_idx, t, n_alleles, get_new_seed(rng)))
|
|
261
|
+
contig_set.add(contig)
|
|
261
262
|
num_loci += 1
|
|
262
263
|
|
|
263
264
|
# At the end of the queue, add a None value (* the # of processes).
|
|
@@ -275,7 +276,6 @@ def call_sample(
|
|
|
275
276
|
"num_bootstrap": num_bootstrap,
|
|
276
277
|
"flank_size": flank_size,
|
|
277
278
|
"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,6 +284,7 @@ def call_sample(
|
|
|
284
284
|
"fractional": fractional,
|
|
285
285
|
"respect_ref": respect_ref,
|
|
286
286
|
"count_kmers": count_kmers,
|
|
287
|
+
"consensus": consensus,
|
|
287
288
|
"log_level": log_level,
|
|
288
289
|
}
|
|
289
290
|
|
|
@@ -317,54 +318,19 @@ def call_sample(
|
|
|
317
318
|
time_taken = datetime.now() - start_time
|
|
318
319
|
|
|
319
320
|
if output_tsv:
|
|
320
|
-
|
|
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")
|
|
321
|
+
output_tsv_fn(results, has_snv_vcf=snv_vcf is not None)
|
|
343
322
|
|
|
344
323
|
if json_path:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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")
|
|
324
|
+
output_json_report(
|
|
325
|
+
sample_id_final,
|
|
326
|
+
job_params,
|
|
327
|
+
processes,
|
|
328
|
+
time_taken,
|
|
329
|
+
contig_set,
|
|
330
|
+
results,
|
|
331
|
+
json_path,
|
|
332
|
+
indent_json,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if vcf_path:
|
|
336
|
+
output_vcf(sample_id_final, reference_file, results, vcf_path)
|
|
@@ -0,0 +1,26 @@
|
|
|
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", ()))))
|
|
@@ -0,0 +1,46 @@
|
|
|
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")
|
|
@@ -0,0 +1,34 @@
|
|
|
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")
|