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.
Files changed (66) hide show
  1. {strkit-0.8.0a25/strkit.egg-info → strkit-0.8.1}/PKG-INFO +12 -25
  2. {strkit-0.8.0a25 → strkit-0.8.1}/README.md +11 -24
  3. {strkit-0.8.0a25 → strkit-0.8.1}/setup.py +7 -7
  4. strkit-0.8.1/strkit/VERSION +1 -0
  5. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/call/__init__.py +1 -3
  6. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/call/allele.py +12 -6
  7. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/call_locus.py +101 -114
  8. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/call_sample.py +3 -2
  9. strkit-0.8.1/strkit/call/cigar.py +91 -0
  10. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/realign.py +2 -2
  11. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/repeats.py +6 -3
  12. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/snvs.py +121 -39
  13. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/types.py +9 -2
  14. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/utils.py +10 -4
  15. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/entry.py +1 -85
  16. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/templates/browser.html +3 -2
  17. {strkit-0.8.0a25 → strkit-0.8.1/strkit.egg-info}/PKG-INFO +12 -25
  18. {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/SOURCES.txt +10 -15
  19. strkit-0.8.1/strkit.egg-info/requires.txt +11 -0
  20. {strkit-0.8.0a25 → strkit-0.8.1}/tests/test_caller_utils.py +1 -1
  21. strkit-0.8.0a25/strkit/VERSION +0 -1
  22. strkit-0.8.0a25/strkit/call/caller/__init__.py +0 -5
  23. strkit-0.8.0a25/strkit/call/caller/cigar.py +0 -73
  24. strkit-0.8.0a25/strkit/call/re_caller.py +0 -91
  25. strkit-0.8.0a25/strkit/call/repeathmm.py +0 -95
  26. strkit-0.8.0a25/strkit/call/straglr.py +0 -94
  27. strkit-0.8.0a25/strkit/call/tandem_genotypes.py +0 -64
  28. strkit-0.8.0a25/strkit.egg-info/requires.txt +0 -11
  29. {strkit-0.8.0a25 → strkit-0.8.1}/LICENSE +0 -0
  30. {strkit-0.8.0a25 → strkit-0.8.1}/MANIFEST.in +0 -0
  31. {strkit-0.8.0a25 → strkit-0.8.1}/pyproject.toml +0 -0
  32. {strkit-0.8.0a25 → strkit-0.8.1}/setup.cfg +0 -0
  33. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/__init__.py +0 -0
  34. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/align_matrix.py +0 -0
  35. {strkit-0.8.0a25/strkit/call/caller → strkit-0.8.1/strkit/call}/non_daemonic_pool.py +0 -0
  36. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/catalog/__init__.py +0 -0
  37. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/catalog/combine.py +0 -0
  38. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/constants.py +0 -0
  39. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/__init__.py +0 -0
  40. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/_bed_4.py +0 -0
  41. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/converter.py +0 -0
  42. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/expansionhunter.py +0 -0
  43. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/gangstr.py +0 -0
  44. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/hipstr.py +0 -0
  45. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/straglr.py +0 -0
  46. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/convert/tandem_genotypes.py +0 -0
  47. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/exceptions.py +0 -0
  48. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/json.py +0 -0
  49. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/logger.py +0 -0
  50. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/__init__.py +0 -0
  51. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/base.py +0 -0
  52. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/expansionhunter.py +0 -0
  53. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/gangstr.py +0 -0
  54. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/repeathmm.py +0 -0
  55. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/result.py +0 -0
  56. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/straglr.py +0 -0
  57. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/strkit.py +0 -0
  58. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/tandem_genotypes.py +0 -0
  59. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/mi/vcf_utils.py +0 -0
  60. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/utils.py +0 -0
  61. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/__init__.py +0 -0
  62. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/server.py +0 -0
  63. {strkit-0.8.0a25 → strkit-0.8.1}/strkit/viz/static/logo.png +0 -0
  64. {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/dependency_links.txt +0 -0
  65. {strkit-0.8.0a25 → strkit-0.8.1}/strkit.egg-info/entry_points.txt +0 -0
  66. {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.0a25
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 genotype calculation
95
- and a greater proportion of reads incorporated into the computed genotypes, respectively.
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` or `strkit re-call` on a system with OpenMP,
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 genotype calculation
79
- and a greater proportion of reads incorporated into the computed genotypes, respectively.
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` or `strkit re-call` on a system with OpenMP,
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.2,<2.3",
18
- "pysam>=0.19,<0.21",
19
- "numpy>=1.23.4,<1.25",
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.3",
21
+ "scikit-learn>=1.2.1,<1.4",
22
22
  "scipy>=1.10,<1.11",
23
- "statsmodels>=0.13.5,<0.14",
23
+ "statsmodels>=0.14.0,<0.15",
24
24
  ],
25
25
  extras_require={
26
26
  "rustdeps": [
27
- "orjson>=3.8.5,<3.9",
28
- "strkit_rust_ext==0.3.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 .caller import call_sample
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.integers(0, 4096).item(),
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 = np.array(
173
- read_weights_fwd if read_weights_fwd is not None else np.array(([1/fwd_len] * fwd_len) if fwd_len else []))
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
- # get_read_snvs,
27
- get_read_snvs_dbsnp,
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 _get_new_seed(rng: np.random.Generator) -> int:
248
- return rng.integers(0, 4096).item()
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=_get_new_seed(rng),
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
- cdd_calls = np.array([x["call"][0] for x in cdd])
416
- peak_order: NDArray[np.int_] = np.argsort(cdd_calls) # To reorder call arrays in least-to-greatest by copy number
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": cdd_calls[peak_order],
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 + ' ' if sample_id else ''}locus {t_idx}: {contig}:{left_coord}-{right_coord}"
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
- candidate_snvs: list[tuple[str, int, Optional[str], tuple[str, ...]]] = []
597
- if should_incorporate_snvs and snv_vcf_file:
598
- snv_contig: str = contig
599
- if snv_contig not in snv_vcf_contigs:
600
- if snv_vcf_file_format == "num":
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 + ref_seq + ref_right_flank_seq_plus_1, # TODO: plus 1, really?
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
- # cigar_first_op = segment.cigartuples[0]
794
- # cigar_last_op = segment.cigartuples[-1]
795
- #
796
- # read_dict_extra[rn]["sig_clip_left"] = (
797
- # cigar_first_op[0] in (4, 5) and cigar_first_op[1] >= significant_clip_threshold)
798
- # read_dict_extra[rn]["sig_clip_right"] = (
799
- # cigar_last_op[0] in (4, 5) and cigar_last_op[1] >= significant_clip_threshold)
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
- # Loop through a second time if we are using SNVs. We do a second loop rather than just using the first loop
864
- # in order to have collected the edges of the reference sequence we can cache for faster SNV calculation.
865
-
866
- # ref_cache = ref.fetch(contig, left_most_coord, right_most_coord + 1).upper()
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
- for rn, read in read_dict_items:
869
- # --> RE-ENABLE FOR DE NOVO SNV FINDER <--
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=_get_new_seed(rng),
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.integers(0, 4096).item()))
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,