strkit 0.8.0a25__tar.gz → 0.8.1__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.8.0a25/strkit.egg-info → strkit-0.8.1}/PKG-INFO +12 -25
- {strkit-0.8.0a25 → strkit-0.8.1}/README.md +11 -24
- {strkit-0.8.0a25 → strkit-0.8.1}/setup.py +7 -7
- strkit-0.8.1/strkit/VERSION +1 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/call/__init__.py +1 -3
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/call/allele.py +12 -6
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/call_locus.py +101 -114
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/call_sample.py +3 -2
- strkit-0.8.1/strkit/call/cigar.py +91 -0
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/realign.py +2 -2
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/repeats.py +6 -3
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/snvs.py +121 -39
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/types.py +9 -2
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/utils.py +10 -4
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/entry.py +1 -85
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/templates/browser.html +3 -2
- {strkit-0.8.0a25 → strkit-0.8.1/strkit.egg-info}/PKG-INFO +12 -25
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/SOURCES.txt +10 -15
- strkit-0.8.1/strkit.egg-info/requires.txt +11 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/tests/test_caller_utils.py +1 -1
- strkit-0.8.0a25/strkit/VERSION +0 -1
- strkit-0.8.0a25/strkit/call/caller/__init__.py +0 -5
- strkit-0.8.0a25/strkit/call/caller/cigar.py +0 -73
- strkit-0.8.0a25/strkit/call/re_caller.py +0 -91
- strkit-0.8.0a25/strkit/call/repeathmm.py +0 -95
- strkit-0.8.0a25/strkit/call/straglr.py +0 -94
- strkit-0.8.0a25/strkit/call/tandem_genotypes.py +0 -64
- strkit-0.8.0a25/strkit.egg-info/requires.txt +0 -11
- {strkit-0.8.0a25 → strkit-0.8.1}/LICENSE +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/MANIFEST.in +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/pyproject.toml +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/setup.cfg +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/__init__.py +0 -0
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/align_matrix.py +0 -0
- {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/non_daemonic_pool.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/catalog/__init__.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/catalog/combine.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/constants.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/__init__.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/_bed_4.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/converter.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/expansionhunter.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/gangstr.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/hipstr.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/straglr.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/tandem_genotypes.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/exceptions.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/json.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/logger.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/__init__.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/base.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/expansionhunter.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/gangstr.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/repeathmm.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/result.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/straglr.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/strkit.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/tandem_genotypes.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/vcf_utils.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/utils.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/__init__.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/server.py +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/static/logo.png +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/dependency_links.txt +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/entry_points.txt +0 -0
- {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: strkit
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
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
|
|
@@ -80,6 +80,7 @@ strkit call \
|
|
|
80
80
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
81
81
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
82
82
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
83
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
|
|
83
84
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
84
85
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
85
86
|
--flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
|
|
@@ -91,14 +92,20 @@ PacBio provides a
|
|
|
91
92
|
[recommended workflow](https://github.com/PacificBiosciences/apps-scripts/tree/master/RepeatAnalysisTools)
|
|
92
93
|
for CCS alignment in this scenario.
|
|
93
94
|
|
|
94
|
-
If you're using HiFi reads as input, **use the `--hq` and `--realign` options** to get better
|
|
95
|
-
and a greater proportion of reads incorporated into the computed genotypes,
|
|
96
|
-
These should not add much performance overhead.
|
|
95
|
+
If you're using HiFi reads as input, **use the `--hq` and `--realign` options** to get better
|
|
96
|
+
genotype calculation and a greater proportion of reads incorporated into the computed genotypes,
|
|
97
|
+
respectively. These should not add much performance overhead.
|
|
97
98
|
|
|
98
99
|
If more than one read file is specified, the reads will be pooled. This can come in handy if you
|
|
99
100
|
have e.g. multiple flow cells of the same sample split into different BAM files, or the reads are
|
|
100
101
|
split by chromosome.
|
|
101
102
|
|
|
103
|
+
If you want to incorporate SNV calling into the process, which speeds up runtime and gives
|
|
104
|
+
marginally better calling results, you must provide an indexed, `bgzip`-compressed SNV catalog
|
|
105
|
+
VCF which matches your reference genome. You can find dbSNP VCFs at
|
|
106
|
+
[`https://ftp.ncbi.nih.gov/snp/organisms/human_9606/VCF/`](https://ftp.ncbi.nih.gov/snp/organisms/human_9606/VCF/).
|
|
107
|
+
The file for GRCh38 is called `00-common_all.vcf.gz` as of time of writing.
|
|
108
|
+
|
|
102
109
|
If you want to output a full call report, you can use the `--json output-file.json` argument to
|
|
103
110
|
specify a path to output a more detailed JSON document to. This document contains 99% CIs, peak
|
|
104
111
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
@@ -117,7 +124,7 @@ samtools faidx my-reference.fa.gz # Generates a .fai index file
|
|
|
117
124
|
|
|
118
125
|
##### Note on OpenMP Threading
|
|
119
126
|
|
|
120
|
-
Slow performance can result from running `strkit call`
|
|
127
|
+
Slow performance can result from running `strkit call` on a system with OpenMP,
|
|
121
128
|
due to a misguided attempt at multithreading under the hood somewhere in Numpy/Scipy (which doesn't work
|
|
122
129
|
here due to repeated initializations of the Gaussian mixture model.) To fix this, the following
|
|
123
130
|
environment variable is auto-set (hardcoded) before running:
|
|
@@ -178,26 +185,6 @@ To exit the tool, press `Ctrl-C` in your command line window as mentioned in
|
|
|
178
185
|
the start-up instructions.
|
|
179
186
|
|
|
180
187
|
|
|
181
|
-
### `strkit re-call`: Genotype re-caller
|
|
182
|
-
|
|
183
|
-
This command has a similar feature-set as `strkit call`, but is designed to
|
|
184
|
-
be used with the output of other long-read STR genotyping methods to refine
|
|
185
|
-
the genotype estimates when calling from HiFi reads.\
|
|
186
|
-
|
|
187
|
-
#### Features:
|
|
188
|
-
|
|
189
|
-
* Support for re-calling output from `tandem-genotypes`, `RepeatHMM`, and `Straglr`
|
|
190
|
-
* Strand resampling / bias correction (for use with the `tandem-genotypes` program)
|
|
191
|
-
* 95% confidence intervals on calls via user-configurable bootstrapping
|
|
192
|
-
|
|
193
|
-
#### Notes:
|
|
194
|
-
|
|
195
|
-
* `--min-allele-reads` will affect the confidence intervals given by the bootstrap process,
|
|
196
|
-
especially in low-coverage loci. This should be set depending on the read technology being used;
|
|
197
|
-
something like a single PacBio HiFi read generally contains higher-quality information than a single
|
|
198
|
-
PacBio CLR read, for example.
|
|
199
|
-
|
|
200
|
-
|
|
201
188
|
|
|
202
189
|
### `strkit mi`: Mendelian inheritance analysis
|
|
203
190
|
|
|
@@ -64,6 +64,7 @@ strkit call \
|
|
|
64
64
|
--realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
|
|
65
65
|
--ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
|
|
66
66
|
--loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
|
|
67
|
+
--incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
|
|
67
68
|
--min-reads 4 \ # Minimum number of supporting reads needed to make a call
|
|
68
69
|
--min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
|
|
69
70
|
--flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
|
|
@@ -75,14 +76,20 @@ PacBio provides a
|
|
|
75
76
|
[recommended workflow](https://github.com/PacificBiosciences/apps-scripts/tree/master/RepeatAnalysisTools)
|
|
76
77
|
for CCS alignment in this scenario.
|
|
77
78
|
|
|
78
|
-
If you're using HiFi reads as input, **use the `--hq` and `--realign` options** to get better
|
|
79
|
-
and a greater proportion of reads incorporated into the computed genotypes,
|
|
80
|
-
These should not add much performance overhead.
|
|
79
|
+
If you're using HiFi reads as input, **use the `--hq` and `--realign` options** to get better
|
|
80
|
+
genotype calculation and a greater proportion of reads incorporated into the computed genotypes,
|
|
81
|
+
respectively. These should not add much performance overhead.
|
|
81
82
|
|
|
82
83
|
If more than one read file is specified, the reads will be pooled. This can come in handy if you
|
|
83
84
|
have e.g. multiple flow cells of the same sample split into different BAM files, or the reads are
|
|
84
85
|
split by chromosome.
|
|
85
86
|
|
|
87
|
+
If you want to incorporate SNV calling into the process, which speeds up runtime and gives
|
|
88
|
+
marginally better calling results, you must provide an indexed, `bgzip`-compressed SNV catalog
|
|
89
|
+
VCF which matches your reference genome. You can find dbSNP VCFs at
|
|
90
|
+
[`https://ftp.ncbi.nih.gov/snp/organisms/human_9606/VCF/`](https://ftp.ncbi.nih.gov/snp/organisms/human_9606/VCF/).
|
|
91
|
+
The file for GRCh38 is called `00-common_all.vcf.gz` as of time of writing.
|
|
92
|
+
|
|
86
93
|
If you want to output a full call report, you can use the `--json output-file.json` argument to
|
|
87
94
|
specify a path to output a more detailed JSON document to. This document contains 99% CIs, peak
|
|
88
95
|
labels, and some other information that isn't included in the normal TSV file. If you want this
|
|
@@ -101,7 +108,7 @@ samtools faidx my-reference.fa.gz # Generates a .fai index file
|
|
|
101
108
|
|
|
102
109
|
##### Note on OpenMP Threading
|
|
103
110
|
|
|
104
|
-
Slow performance can result from running `strkit call`
|
|
111
|
+
Slow performance can result from running `strkit call` on a system with OpenMP,
|
|
105
112
|
due to a misguided attempt at multithreading under the hood somewhere in Numpy/Scipy (which doesn't work
|
|
106
113
|
here due to repeated initializations of the Gaussian mixture model.) To fix this, the following
|
|
107
114
|
environment variable is auto-set (hardcoded) before running:
|
|
@@ -162,26 +169,6 @@ To exit the tool, press `Ctrl-C` in your command line window as mentioned in
|
|
|
162
169
|
the start-up instructions.
|
|
163
170
|
|
|
164
171
|
|
|
165
|
-
### `strkit re-call`: Genotype re-caller
|
|
166
|
-
|
|
167
|
-
This command has a similar feature-set as `strkit call`, but is designed to
|
|
168
|
-
be used with the output of other long-read STR genotyping methods to refine
|
|
169
|
-
the genotype estimates when calling from HiFi reads.\
|
|
170
|
-
|
|
171
|
-
#### Features:
|
|
172
|
-
|
|
173
|
-
* Support for re-calling output from `tandem-genotypes`, `RepeatHMM`, and `Straglr`
|
|
174
|
-
* Strand resampling / bias correction (for use with the `tandem-genotypes` program)
|
|
175
|
-
* 95% confidence intervals on calls via user-configurable bootstrapping
|
|
176
|
-
|
|
177
|
-
#### Notes:
|
|
178
|
-
|
|
179
|
-
* `--min-allele-reads` will affect the confidence intervals given by the bootstrap process,
|
|
180
|
-
especially in low-coverage loci. This should be set depending on the read technology being used;
|
|
181
|
-
something like a single PacBio HiFi read generally contains higher-quality information than a single
|
|
182
|
-
PacBio CLR read, for example.
|
|
183
|
-
|
|
184
|
-
|
|
185
172
|
|
|
186
173
|
### `strkit mi`: Mendelian inheritance analysis
|
|
187
174
|
|
|
@@ -14,18 +14,18 @@ setup(
|
|
|
14
14
|
|
|
15
15
|
python_requires="~=3.9",
|
|
16
16
|
install_requires=[
|
|
17
|
-
"Flask>=2.2.
|
|
18
|
-
"pysam>=0.19,<0.
|
|
19
|
-
"numpy>=1.23.4,<1.
|
|
17
|
+
"Flask>=2.2.5,<2.3",
|
|
18
|
+
"pysam>=0.19,<0.22",
|
|
19
|
+
"numpy>=1.23.4,<1.26",
|
|
20
20
|
"parasail>=1.2.4,<1.4",
|
|
21
|
-
"scikit-learn>=1.2.1,<1.
|
|
21
|
+
"scikit-learn>=1.2.1,<1.4",
|
|
22
22
|
"scipy>=1.10,<1.11",
|
|
23
|
-
"statsmodels>=0.
|
|
23
|
+
"statsmodels>=0.14.0,<0.15",
|
|
24
24
|
],
|
|
25
25
|
extras_require={
|
|
26
26
|
"rustdeps": [
|
|
27
|
-
"orjson>=3.8.5,<
|
|
28
|
-
"strkit_rust_ext==0.
|
|
27
|
+
"orjson>=3.8.5,<4",
|
|
28
|
+
"strkit_rust_ext==0.4.0",
|
|
29
29
|
],
|
|
30
30
|
},
|
|
31
31
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.8.1
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from .allele import call_alleles
|
|
4
|
-
from .
|
|
5
|
-
from .re_caller import re_call_all_alleles
|
|
4
|
+
from .call_sample import call_sample
|
|
6
5
|
|
|
7
6
|
__all__ = [
|
|
8
7
|
"call_alleles",
|
|
9
8
|
"call_sample",
|
|
10
|
-
"re_call_all_alleles",
|
|
11
9
|
]
|
|
@@ -20,6 +20,8 @@ from typing import Iterable, Literal, Optional, TypedDict, Union
|
|
|
20
20
|
|
|
21
21
|
import strkit.constants as cc
|
|
22
22
|
|
|
23
|
+
from .utils import get_new_seed
|
|
24
|
+
|
|
23
25
|
__all__ = [
|
|
24
26
|
"RepeatCounts",
|
|
25
27
|
"CallDict",
|
|
@@ -27,7 +29,7 @@ __all__ = [
|
|
|
27
29
|
"call_alleles",
|
|
28
30
|
]
|
|
29
31
|
|
|
30
|
-
RepeatCounts = Union[list[int], tuple[int, ...], list[float], tuple[float, ...]]
|
|
32
|
+
RepeatCounts = Union[list[int], tuple[int, ...], list[float], tuple[float, ...], NDArray[np.int_], NDArray[np.float_]]
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
# K-means convergence errors - we expect convergence to some extent with homozygous alleles
|
|
@@ -90,6 +92,7 @@ def fit_gmm(
|
|
|
90
92
|
n_components = n_alleles
|
|
91
93
|
while n_components > 0:
|
|
92
94
|
if n_components == 1: # Don't need to do the full fit for a single peak, just calculate the parameters
|
|
95
|
+
# I've confirmed this gives an ~identical result to fitting a GMM with one parameter.
|
|
93
96
|
fake_g: object = type("", (), {})()
|
|
94
97
|
fake_g.means_ = np.array([[np.mean(sample_rs)]])
|
|
95
98
|
fake_g.weights_ = np.array([[1.0]])
|
|
@@ -101,7 +104,7 @@ def fit_gmm(
|
|
|
101
104
|
init_params=init_params,
|
|
102
105
|
covariance_type="spherical",
|
|
103
106
|
n_init=N_GM_INIT,
|
|
104
|
-
random_state=rng
|
|
107
|
+
random_state=get_new_seed(rng),
|
|
105
108
|
).fit(sample_rs)
|
|
106
109
|
|
|
107
110
|
# noinspection PyUnresolvedReferences
|
|
@@ -145,6 +148,11 @@ class CallDict(TypedDict):
|
|
|
145
148
|
modal_n_peaks: int
|
|
146
149
|
|
|
147
150
|
|
|
151
|
+
def make_read_weights(read_weights: Optional[Iterable[float]], num_reads: int) -> NDArray[np.float_]:
|
|
152
|
+
return np.array(
|
|
153
|
+
read_weights if read_weights is not None else np.array(([1/num_reads] * num_reads) if num_reads else []))
|
|
154
|
+
|
|
155
|
+
|
|
148
156
|
def call_alleles(
|
|
149
157
|
repeats_fwd: RepeatCounts,
|
|
150
158
|
repeats_rev: RepeatCounts,
|
|
@@ -169,10 +177,8 @@ def call_alleles(
|
|
|
169
177
|
fwd_len = fwd_strand_reads.shape[0]
|
|
170
178
|
rev_len = rev_strand_reads.shape[0]
|
|
171
179
|
|
|
172
|
-
fwd_strand_weights =
|
|
173
|
-
|
|
174
|
-
rev_strand_weights = np.array(
|
|
175
|
-
read_weights_rev if read_weights_rev is not None else np.array(([1/rev_len] * rev_len) if rev_len else []))
|
|
180
|
+
fwd_strand_weights = make_read_weights(read_weights_fwd, fwd_len)
|
|
181
|
+
rev_strand_weights = make_read_weights(read_weights_rev, rev_len)
|
|
176
182
|
|
|
177
183
|
assert fwd_strand_reads.shape == fwd_strand_weights.shape
|
|
178
184
|
assert rev_strand_reads.shape == rev_strand_weights.shape
|
|
@@ -23,13 +23,14 @@ from .realign import realign_read
|
|
|
23
23
|
from .repeats import get_repeat_count, get_ref_repeat_count
|
|
24
24
|
from .snvs import (
|
|
25
25
|
SNV_OUT_OF_RANGE_CHAR,
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
get_candidate_snvs,
|
|
27
|
+
get_read_snvs,
|
|
28
|
+
# get_read_snvs_dbsnp,
|
|
28
29
|
calculate_useful_snvs,
|
|
29
30
|
call_and_filter_useful_snvs,
|
|
30
31
|
)
|
|
31
|
-
from .types import ReadDict, ReadDictExtra
|
|
32
|
-
from .utils import find_pair_by_ref_pos, normalize_contig, round_to_base_pos
|
|
32
|
+
from .types import ReadDict, ReadDictExtra, CandidateSNV
|
|
33
|
+
from .utils import find_pair_by_ref_pos, normalize_contig, round_to_base_pos, get_new_seed
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
__all__ = [
|
|
@@ -244,8 +245,70 @@ def calculate_read_distance(
|
|
|
244
245
|
return distance_matrix
|
|
245
246
|
|
|
246
247
|
|
|
247
|
-
def
|
|
248
|
-
|
|
248
|
+
def process_read_snvs_for_locus(
|
|
249
|
+
contig: str,
|
|
250
|
+
left_coord_adj: int,
|
|
251
|
+
right_coord_adj: int,
|
|
252
|
+
left_most_coord: int,
|
|
253
|
+
right_most_coord: int,
|
|
254
|
+
ref: FastaFile,
|
|
255
|
+
read_dict_items: tuple[tuple[str, ReadDict], ...],
|
|
256
|
+
read_dict_extra: dict[str, ReadDictExtra],
|
|
257
|
+
read_pairs: dict[str, list[tuple[int, int]]],
|
|
258
|
+
candidate_snvs_dict: dict[int, CandidateSNV],
|
|
259
|
+
only_known_snvs: bool,
|
|
260
|
+
logger_: logging.Logger,
|
|
261
|
+
locus_log_str: str,
|
|
262
|
+
) -> set[int]:
|
|
263
|
+
# Loop through a second time if we are using SNVs. We do a second loop rather than just using the first loop
|
|
264
|
+
# in order to have collected the edges of the reference sequence we can cache for faster SNV calculation.
|
|
265
|
+
|
|
266
|
+
locus_snvs: set[int] = set()
|
|
267
|
+
|
|
268
|
+
ref_cache = ref.fetch(contig, left_most_coord, right_most_coord + 1).upper()
|
|
269
|
+
|
|
270
|
+
for rn, read in read_dict_items:
|
|
271
|
+
# --> RE-ENABLE FOR DE NOVO SNV FINDER <--
|
|
272
|
+
scl = read_dict_extra[rn]["sig_clip_left"]
|
|
273
|
+
scr = read_dict_extra[rn]["sig_clip_right"]
|
|
274
|
+
if scl or scr:
|
|
275
|
+
logger_.debug(
|
|
276
|
+
f"{locus_log_str} - {rn} has significant clipping; trimming pairs by "
|
|
277
|
+
f"{significant_clip_snv_take_in} bp per side for SNV-finding")
|
|
278
|
+
|
|
279
|
+
snv_pairs = read_pairs[rn]
|
|
280
|
+
|
|
281
|
+
if len(snv_pairs) < (twox_takein := significant_clip_snv_take_in * 2):
|
|
282
|
+
logger_.warning(f"{locus_log_str} - skipping SNV calculation for '{rn}' (<{twox_takein} pairs)")
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if read_dict_extra[rn]["sig_clip_left"]:
|
|
286
|
+
snv_pairs = snv_pairs[significant_clip_snv_take_in:]
|
|
287
|
+
if read_dict_extra[rn]["sig_clip_right"]:
|
|
288
|
+
snv_pairs = snv_pairs[:-1 * significant_clip_snv_take_in]
|
|
289
|
+
|
|
290
|
+
# snv_pairs = list(filter(lambda p: p[1] in candidate_snvs_dict, snv_pairs))
|
|
291
|
+
|
|
292
|
+
# --> RE-ENABLE FOR DE NOVO SNV FINDER <--
|
|
293
|
+
snvs = get_read_snvs(
|
|
294
|
+
read_dict_extra[rn]["_qs"],
|
|
295
|
+
snv_pairs,
|
|
296
|
+
ref_cache,
|
|
297
|
+
left_most_coord,
|
|
298
|
+
left_coord_adj,
|
|
299
|
+
right_coord_adj,
|
|
300
|
+
)
|
|
301
|
+
# snvs = get_read_snvs_dbsnp(
|
|
302
|
+
# candidate_snvs_dict_items_flat,
|
|
303
|
+
# read_dict_extra[rn]["_qs"],
|
|
304
|
+
# read_pairs[rn],
|
|
305
|
+
# left_coord_adj,
|
|
306
|
+
# right_coord_adj,
|
|
307
|
+
# )
|
|
308
|
+
locus_snvs.update(filter(lambda p: (not only_known_snvs) or p in candidate_snvs_dict, snvs.keys()))
|
|
309
|
+
read_dict_extra[rn]["snv"] = snvs
|
|
310
|
+
|
|
311
|
+
return locus_snvs
|
|
249
312
|
|
|
250
313
|
|
|
251
314
|
def call_alleles_with_incorporated_snvs(
|
|
@@ -259,6 +322,7 @@ def call_alleles_with_incorporated_snvs(
|
|
|
259
322
|
read_dict_extra: dict[str, dict],
|
|
260
323
|
n_reads_in_dict: int, # We could derive this again, but we already have before...
|
|
261
324
|
useful_snvs: list[tuple[int, int]],
|
|
325
|
+
candidate_snvs_dict: dict[int, CandidateSNV],
|
|
262
326
|
rng: np.random.Generator,
|
|
263
327
|
logger_: logging.Logger,
|
|
264
328
|
locus_log_str: str,
|
|
@@ -390,7 +454,7 @@ def call_alleles_with_incorporated_snvs(
|
|
|
390
454
|
gm_filter_factor=1, # n_alleles=1, so this is ignored
|
|
391
455
|
hq=hq,
|
|
392
456
|
force_int=not fractional,
|
|
393
|
-
seed=
|
|
457
|
+
seed=get_new_seed(rng),
|
|
394
458
|
logger_=logger_,
|
|
395
459
|
debug_str=f"{locus_log_str} a{ci}"
|
|
396
460
|
)
|
|
@@ -412,8 +476,11 @@ def call_alleles_with_incorporated_snvs(
|
|
|
412
476
|
# is an array of length 1.
|
|
413
477
|
|
|
414
478
|
# Leaving this as an np.array(...) for type detect since #calls is low:
|
|
415
|
-
|
|
416
|
-
|
|
479
|
+
cdd_sort_order_determiner = np.array(
|
|
480
|
+
[(x["peaks"][0], x["call_95_cis"][0][0]) for x in cdd],
|
|
481
|
+
dtype=[("p", np.float_), ("i", np.float_ if fractional else np.int_)])
|
|
482
|
+
# To reorder call arrays in least-to-greatest by raw peak mean, and then by 95% CI left boundary:
|
|
483
|
+
peak_order: NDArray[np.int_] = np.argsort(cdd_sort_order_determiner, order=("p", "i"))
|
|
417
484
|
|
|
418
485
|
# Re-order the call data dictionary, now that we've established an ordering
|
|
419
486
|
# noinspection PyTypeChecker
|
|
@@ -432,7 +499,7 @@ def call_alleles_with_incorporated_snvs(
|
|
|
432
499
|
|
|
433
500
|
# All call_datas are truth-y; all arrays should be ordered by peak_order
|
|
434
501
|
call_data = {
|
|
435
|
-
"call":
|
|
502
|
+
"call": np.concatenate(tuple(cc["call"] for cc in cdd_ordered), axis=0),
|
|
436
503
|
"call_95_cis": np.concatenate(tuple(cc["call_95_cis"] for cc in cdd_ordered), axis=0),
|
|
437
504
|
"call_99_cis": np.concatenate(tuple(cc["call_99_cis"] for cc in cdd_ordered), axis=0),
|
|
438
505
|
"peaks": np.concatenate(tuple(cc["peaks"] for cc in cdd_ordered), axis=None),
|
|
@@ -446,7 +513,7 @@ def call_alleles_with_incorporated_snvs(
|
|
|
446
513
|
|
|
447
514
|
# Called useful SNVs to add to the final return dictionary:
|
|
448
515
|
called_useful_snvs: list[dict] = call_and_filter_useful_snvs(
|
|
449
|
-
n_alleles, read_dict, useful_snvs, locus_log_str, logger_)
|
|
516
|
+
n_alleles, read_dict, useful_snvs, candidate_snvs_dict, locus_log_str, logger_)
|
|
450
517
|
|
|
451
518
|
if not called_useful_snvs: # No useful SNVs left, so revert to "dist" assignment method
|
|
452
519
|
return "dist", None
|
|
@@ -454,23 +521,6 @@ def call_alleles_with_incorporated_snvs(
|
|
|
454
521
|
return assign_method, (call_data, called_useful_snvs)
|
|
455
522
|
|
|
456
523
|
|
|
457
|
-
def _human_chrom_to_refseq_accession(contig: str, snv_vcf_contigs: tuple[str, ...]) -> Optional[str]:
|
|
458
|
-
contig = contig.removeprefix("chr")
|
|
459
|
-
if contig == "X":
|
|
460
|
-
contig = "23"
|
|
461
|
-
if contig == "Y":
|
|
462
|
-
contig = "24"
|
|
463
|
-
if contig == "M":
|
|
464
|
-
contig = "12920"
|
|
465
|
-
contig = f"NC_{contig.zfill(6)}"
|
|
466
|
-
|
|
467
|
-
for vcf_contig in snv_vcf_contigs:
|
|
468
|
-
if vcf_contig.startswith(contig):
|
|
469
|
-
return vcf_contig
|
|
470
|
-
|
|
471
|
-
return None
|
|
472
|
-
|
|
473
|
-
|
|
474
524
|
def call_locus(
|
|
475
525
|
t_idx: int,
|
|
476
526
|
t: tuple,
|
|
@@ -530,6 +580,7 @@ def call_locus(
|
|
|
530
580
|
# and polyploidy is hard.
|
|
531
581
|
# should_incorporate_snvs: bool = incorporate_snvs and n_alleles == 2
|
|
532
582
|
should_incorporate_snvs: bool = snv_vcf_file is not None and n_alleles == 2
|
|
583
|
+
only_known_snvs: bool = True # TODO: parametrize
|
|
533
584
|
|
|
534
585
|
try:
|
|
535
586
|
ref_left_flank_seq = ref.fetch(ref_contig, left_flank_coord, left_coord)
|
|
@@ -555,7 +606,7 @@ def call_locus(
|
|
|
555
606
|
return None
|
|
556
607
|
|
|
557
608
|
# String representation of locus for logging purposes
|
|
558
|
-
locus_log_str: str = f"{sample_id
|
|
609
|
+
locus_log_str: str = f"{sample_id}{' ' if sample_id else ''}locus {t_idx}: {contig}:{left_coord}-{right_coord}"
|
|
559
610
|
|
|
560
611
|
# Get reference repeat count by our method, so we can calculate offsets from reference
|
|
561
612
|
ref_cn: Union[int, float]
|
|
@@ -593,18 +644,11 @@ def call_locus(
|
|
|
593
644
|
|
|
594
645
|
# Find candidate SNVs, if we're using SNV data
|
|
595
646
|
|
|
596
|
-
|
|
597
|
-
if should_incorporate_snvs and snv_vcf_file:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
snv_contig = snv_contig.removeprefix("chr")
|
|
602
|
-
elif snv_vcf_file_format == "acc":
|
|
603
|
-
snv_contig = _human_chrom_to_refseq_accession(snv_contig, snv_vcf_contigs)
|
|
604
|
-
# Otherwise, leave as-is
|
|
605
|
-
|
|
606
|
-
for snv in snv_vcf_file.fetch(snv_contig, left_most_coord, right_most_coord + 1):
|
|
607
|
-
candidate_snvs.append((snv.id, snv.pos, snv.ref, snv.alts or ()))
|
|
647
|
+
candidate_snvs_dict: dict[int, CandidateSNV] = {} # Lookup dictionary for candidate SNVs by position
|
|
648
|
+
if n_overlapping_reads and should_incorporate_snvs and snv_vcf_file:
|
|
649
|
+
# ^^ n_overlapping_reads check since otherwise we will have invalid left/right_most_coord
|
|
650
|
+
candidate_snvs_dict = get_candidate_snvs(
|
|
651
|
+
snv_vcf_file, snv_vcf_contigs, snv_vcf_file_format, contig, left_most_coord, right_most_coord)
|
|
608
652
|
|
|
609
653
|
# Build the read dictionary with segment information, copy number, weight, & more. ---------------------------------
|
|
610
654
|
|
|
@@ -614,8 +658,6 @@ def call_locus(
|
|
|
614
658
|
|
|
615
659
|
# Aggregations for additional read-level data
|
|
616
660
|
read_kmers: Counter[str] = Counter()
|
|
617
|
-
locus_snvs: set[int] = set()
|
|
618
|
-
|
|
619
661
|
read_pairs: dict[str, list[tuple[int, int]]] = {}
|
|
620
662
|
|
|
621
663
|
# Keep track of left-most and right-most coordinates
|
|
@@ -660,7 +702,7 @@ def call_locus(
|
|
|
660
702
|
q: mp.Queue = mp.Queue()
|
|
661
703
|
proc = mp.Process(target=realign_read, daemon=False, kwargs=dict(
|
|
662
704
|
# fetch an extra base for the right flank coordinate check later (needs to be >= the exclusive coord)
|
|
663
|
-
ref_seq=ref_left_flank_seq
|
|
705
|
+
ref_seq=f"{ref_left_flank_seq}{ref_seq}{ref_right_flank_seq_plus_1}", # TODO: plus 1, really?
|
|
664
706
|
query_seq=calculate_seq_with_wildcards(qs, fqqs),
|
|
665
707
|
left_flank_coord=left_flank_coord,
|
|
666
708
|
flank_size=flank_size,
|
|
@@ -790,13 +832,13 @@ def call_locus(
|
|
|
790
832
|
# Observed significant increase in annoying, probably false SNVs near the edges of significantly
|
|
791
833
|
# clipped reads in CCS data. Figure out if we have large clipping for later use here in the SNV finder.
|
|
792
834
|
# --> RE-ENABLE FOR DE NOVO SNV FINDER <--
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
835
|
+
cigar_first_op = segment.cigartuples[0]
|
|
836
|
+
cigar_last_op = segment.cigartuples[-1]
|
|
837
|
+
|
|
838
|
+
read_dict_extra[rn]["sig_clip_left"] = (
|
|
839
|
+
cigar_first_op[0] in (4, 5) and cigar_first_op[1] >= significant_clip_threshold)
|
|
840
|
+
read_dict_extra[rn]["sig_clip_right"] = (
|
|
841
|
+
cigar_last_op[0] in (4, 5) and cigar_last_op[1] >= significant_clip_threshold)
|
|
800
842
|
|
|
801
843
|
# Cache aligned pairs, since it takes a lot of time to extract, and we use it for calculate_useful_snvs
|
|
802
844
|
read_pairs[rn] = pairs
|
|
@@ -860,50 +902,13 @@ def call_locus(
|
|
|
860
902
|
elif should_incorporate_snvs and n_reads_in_dict >= min_snv_read_coverage and not have_rare_realigns:
|
|
861
903
|
# LIMITATION: Currently can only use SNVs for haplotyping with haploid/diploid
|
|
862
904
|
|
|
863
|
-
#
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
905
|
+
# Second read loop occurs in this function
|
|
906
|
+
locus_snvs: set[int] = process_read_snvs_for_locus(
|
|
907
|
+
contig, left_coord_adj, right_coord_adj, left_most_coord, right_most_coord, ref, read_dict_items,
|
|
908
|
+
read_dict_extra, read_pairs, candidate_snvs_dict, only_known_snvs, logger_, locus_log_str)
|
|
867
909
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
# scl = read_dict_extra[rn]["sig_clip_left"]
|
|
871
|
-
# scr = read_dict_extra[rn]["sig_clip_right"]
|
|
872
|
-
# if scl or scr:
|
|
873
|
-
# logger_.debug(
|
|
874
|
-
# f"{locus_log_str} - {rn} has significant clipping; trimming pairs by "
|
|
875
|
-
# f"{significant_clip_snv_take_in} bp per side for SNV-finding")
|
|
876
|
-
#
|
|
877
|
-
# snv_pairs = read_pairs[rn]
|
|
878
|
-
#
|
|
879
|
-
# if len(snv_pairs) < (twox_takein := significant_clip_snv_take_in * 2):
|
|
880
|
-
# logger_.warning(f"{locus_log_str} - skipping SNV calculation for '{rn}' (<{twox_takein} pairs)")
|
|
881
|
-
# continue
|
|
882
|
-
#
|
|
883
|
-
# if read_dict_extra[rn]["sig_clip_left"]:
|
|
884
|
-
# snv_pairs = snv_pairs[significant_clip_snv_take_in:]
|
|
885
|
-
# if read_dict_extra[rn]["sig_clip_right"]:
|
|
886
|
-
# snv_pairs = snv_pairs[:-1*significant_clip_snv_take_in]
|
|
887
|
-
|
|
888
|
-
# --> RE-ENABLE FOR DE NOVO SNV FINDER <--
|
|
889
|
-
# snvs = get_read_snvs(
|
|
890
|
-
# contig,
|
|
891
|
-
# read_dict_extra[rn]["_qs"],
|
|
892
|
-
# snv_pairs,
|
|
893
|
-
# ref_cache,
|
|
894
|
-
# left_most_coord,
|
|
895
|
-
# left_coord_adj,
|
|
896
|
-
# right_coord_adj,
|
|
897
|
-
# )
|
|
898
|
-
snvs = get_read_snvs_dbsnp(
|
|
899
|
-
candidate_snvs, read_dict_extra[rn]["_qs"], read_pairs[rn], left_coord_adj, right_coord_adj)
|
|
900
|
-
locus_snvs.update(snvs.keys())
|
|
901
|
-
read_dict_extra[rn]["snv"] = snvs
|
|
902
|
-
|
|
903
|
-
# End of second read loop --------------------------------------------------------------------------------------
|
|
904
|
-
|
|
905
|
-
useful_snvs: list[tuple[int, int]] = (
|
|
906
|
-
calculate_useful_snvs(n_reads_in_dict, read_dict_items, read_dict_extra, read_pairs, locus_snvs))
|
|
910
|
+
useful_snvs: list[tuple[int, int]] = calculate_useful_snvs(
|
|
911
|
+
n_reads_in_dict, read_dict_items, read_dict_extra, read_pairs, locus_snvs, min_allele_reads)
|
|
907
912
|
n_useful_snvs: int = len(useful_snvs)
|
|
908
913
|
|
|
909
914
|
if not n_useful_snvs:
|
|
@@ -920,6 +925,7 @@ def call_locus(
|
|
|
920
925
|
read_dict_extra=read_dict_extra,
|
|
921
926
|
n_reads_in_dict=n_reads_in_dict,
|
|
922
927
|
useful_snvs=useful_snvs,
|
|
928
|
+
candidate_snvs_dict=candidate_snvs_dict,
|
|
923
929
|
rng=rng,
|
|
924
930
|
logger_=logger_,
|
|
925
931
|
locus_log_str=locus_log_str,
|
|
@@ -929,25 +935,6 @@ def call_locus(
|
|
|
929
935
|
call_data = call_res[0] # Call data dictionary
|
|
930
936
|
call_dict_base["snvs"] = call_res[1] # Called useful SNVs
|
|
931
937
|
|
|
932
|
-
# TODO:
|
|
933
|
-
# - use reads with at least 1 SNPs called to separate 'training reads'
|
|
934
|
-
# - eliminate 1-SNV reads with a unique haplotype vs any non-1-SNP reads... or something like that.
|
|
935
|
-
# (see m64012_190920_173625/31917565/ccs)
|
|
936
|
-
# - then, assign all reads - either based on what group they 'trained' or (for no SNP call ones)
|
|
937
|
-
# based on the GMMs (maybe some kind of certainty of assignment???)
|
|
938
|
-
|
|
939
|
-
# TODO:
|
|
940
|
-
# - maybe 3 approaches:
|
|
941
|
-
# - if not enough SNV info / almost no reads have it, just do old method.
|
|
942
|
-
# - if we have some SNV info for all reads AND it's not a single read count (perfectly homozygous)
|
|
943
|
-
# (how to quantify?), do distance-based with read copy numbers AND SNV data - e.g. if we have just 1 SNP
|
|
944
|
-
# - if we have LOTS of SNV data for the majority of reads, do assignment just using SNV data and assign
|
|
945
|
-
# reads to peaks after (random assignment if equal chance of both groups)
|
|
946
|
-
# - if we have COMPLETE SNV data (4+ for every read) we can do phasing + peak assignment just with SNVs
|
|
947
|
-
# and just call the Gaussians from the separated reads (even just calculate stdev + mean for each bootstrap
|
|
948
|
-
# iteration, which might save us some time...).
|
|
949
|
-
# - keep track of which option as 'peak_calling_method' (pcm) or something
|
|
950
|
-
|
|
951
938
|
elif n_reads_in_dict < min_snv_read_coverage:
|
|
952
939
|
logger_.debug(
|
|
953
940
|
f"{locus_log_str} - not enough coverage for SNV incorporation "
|
|
@@ -975,7 +962,7 @@ def call_locus(
|
|
|
975
962
|
gm_filter_factor=3, # TODO: parametrize
|
|
976
963
|
hq=hq,
|
|
977
964
|
force_int=not fractional,
|
|
978
|
-
seed=
|
|
965
|
+
seed=get_new_seed(rng),
|
|
979
966
|
logger_=logger_,
|
|
980
967
|
debug_str=locus_log_str,
|
|
981
968
|
) or {} # Still false-y
|
|
@@ -25,6 +25,7 @@ from strkit.logger import logger
|
|
|
25
25
|
|
|
26
26
|
from .call_locus import call_locus
|
|
27
27
|
from .non_daemonic_pool import NonDaemonicPool
|
|
28
|
+
from .utils import get_new_seed
|
|
28
29
|
|
|
29
30
|
__all__ = [
|
|
30
31
|
"call_sample",
|
|
@@ -251,7 +252,7 @@ def call_sample(
|
|
|
251
252
|
# We use locus-specific random seeds for replicability, no matter which order
|
|
252
253
|
# the loci are yanked out of the queue / how many processes we have.
|
|
253
254
|
# Tuple of (1-indexed locus index, locus data, locus-specific random seed)
|
|
254
|
-
locus_queue.put((t_idx, t, rng
|
|
255
|
+
locus_queue.put((t_idx, t, get_new_seed(rng)))
|
|
255
256
|
contig_set.add(t[0])
|
|
256
257
|
num_loci += 1
|
|
257
258
|
|
|
@@ -274,7 +275,7 @@ def call_sample(
|
|
|
274
275
|
"realign": realign,
|
|
275
276
|
"hq": hq,
|
|
276
277
|
# "incorporate_snvs": incorporate_snvs,
|
|
277
|
-
"snv_vcf": str(snv_vcf),
|
|
278
|
+
"snv_vcf": str(snv_vcf) if snv_vcf else None,
|
|
278
279
|
"targeted": targeted,
|
|
279
280
|
"fractional": fractional,
|
|
280
281
|
"respect_ref": respect_ref,
|