strkit 0.10.0__tar.gz → 0.11.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 (60) hide show
  1. {strkit-0.10.0/strkit.egg-info → strkit-0.11.1}/PKG-INFO +41 -37
  2. {strkit-0.10.0 → strkit-0.11.1}/README.md +39 -35
  3. {strkit-0.10.0 → strkit-0.11.1}/setup.py +1 -1
  4. strkit-0.11.1/strkit/VERSION +1 -0
  5. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/allele.py +14 -9
  6. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/call_locus.py +47 -31
  7. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/base.py +15 -17
  8. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/result.py +37 -7
  9. strkit-0.11.1/strkit/viz/static/logo.png +0 -0
  10. {strkit-0.10.0 → strkit-0.11.1}/strkit/viz/templates/browser.html +30 -11
  11. {strkit-0.10.0 → strkit-0.11.1/strkit.egg-info}/PKG-INFO +41 -37
  12. {strkit-0.10.0 → strkit-0.11.1}/strkit.egg-info/requires.txt +1 -1
  13. strkit-0.10.0/strkit/VERSION +0 -1
  14. strkit-0.10.0/strkit/viz/static/logo.png +0 -0
  15. {strkit-0.10.0 → strkit-0.11.1}/LICENSE +0 -0
  16. {strkit-0.10.0 → strkit-0.11.1}/MANIFEST.in +0 -0
  17. {strkit-0.10.0 → strkit-0.11.1}/pyproject.toml +0 -0
  18. {strkit-0.10.0 → strkit-0.11.1}/setup.cfg +0 -0
  19. {strkit-0.10.0 → strkit-0.11.1}/strkit/__init__.py +0 -0
  20. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/__init__.py +0 -0
  21. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/align_matrix.py +0 -0
  22. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/call_sample.py +0 -0
  23. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/cigar.py +0 -0
  24. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/non_daemonic_pool.py +0 -0
  25. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/realign.py +0 -0
  26. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/repeats.py +0 -0
  27. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/snvs.py +0 -0
  28. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/types.py +0 -0
  29. {strkit-0.10.0 → strkit-0.11.1}/strkit/call/utils.py +0 -0
  30. {strkit-0.10.0 → strkit-0.11.1}/strkit/catalog/__init__.py +0 -0
  31. {strkit-0.10.0 → strkit-0.11.1}/strkit/catalog/combine.py +0 -0
  32. {strkit-0.10.0 → strkit-0.11.1}/strkit/constants.py +0 -0
  33. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/__init__.py +0 -0
  34. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/_bed_4.py +0 -0
  35. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/converter.py +0 -0
  36. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/expansionhunter.py +0 -0
  37. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/gangstr.py +0 -0
  38. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/hipstr.py +0 -0
  39. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/straglr.py +0 -0
  40. {strkit-0.10.0 → strkit-0.11.1}/strkit/convert/tandem_genotypes.py +0 -0
  41. {strkit-0.10.0 → strkit-0.11.1}/strkit/entry.py +0 -0
  42. {strkit-0.10.0 → strkit-0.11.1}/strkit/exceptions.py +0 -0
  43. {strkit-0.10.0 → strkit-0.11.1}/strkit/json.py +0 -0
  44. {strkit-0.10.0 → strkit-0.11.1}/strkit/logger.py +0 -0
  45. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/__init__.py +0 -0
  46. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/expansionhunter.py +0 -0
  47. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/gangstr.py +0 -0
  48. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/repeathmm.py +0 -0
  49. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/straglr.py +0 -0
  50. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/strkit.py +0 -0
  51. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/tandem_genotypes.py +0 -0
  52. {strkit-0.10.0 → strkit-0.11.1}/strkit/mi/vcf_utils.py +0 -0
  53. {strkit-0.10.0 → strkit-0.11.1}/strkit/utils.py +0 -0
  54. {strkit-0.10.0 → strkit-0.11.1}/strkit/viz/__init__.py +0 -0
  55. {strkit-0.10.0 → strkit-0.11.1}/strkit/viz/server.py +0 -0
  56. {strkit-0.10.0 → strkit-0.11.1}/strkit.egg-info/SOURCES.txt +0 -0
  57. {strkit-0.10.0 → strkit-0.11.1}/strkit.egg-info/dependency_links.txt +0 -0
  58. {strkit-0.10.0 → strkit-0.11.1}/strkit.egg-info/entry_points.txt +0 -0
  59. {strkit-0.10.0 → strkit-0.11.1}/strkit.egg-info/top_level.txt +0 -0
  60. {strkit-0.10.0 → strkit-0.11.1}/tests/test_caller_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strkit
3
- Version: 0.10.0
3
+ Version: 0.11.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
@@ -13,7 +13,7 @@ Requires-Python: ~=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: Flask<2.3,>=2.2.5
16
- Requires-Dist: pysam<0.22,>=0.19
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
19
  Requires-Dist: scikit-learn<1.4,>=1.2.1
@@ -31,13 +31,46 @@ A genotyping and analysis toolkit for short(ish) tandem repeats.
31
31
 
32
32
  <img src="./docs/images/strkit_logo_small.png" alt="" width="500" height="324" />
33
33
 
34
- &copy; David Lougheed and McGill University 2021-2023 (versions up to and including `0.8.0a1`).
35
34
 
36
- &copy; David Lougheed and McGill University 2021-2023, with portions &copy; McGill University 2023
37
- (versions beyond `0.8.0a1`).
35
+ ## Copyright and License
36
+
37
+ * 2021-2023: &copy; David Lougheed (DL) and McGill University 2021-2023 (versions up to and including `0.8.0a1`),
38
+ created during graduate research by DL.
39
+ * 2023+: (versions beyond `0.8.0a1`):
40
+ * Portions &copy; DL and McGill University 2021-2023
41
+ * Portions &copy; McGill University 2023
42
+ * Portions &copy; DL 2023
43
+
44
+
45
+ ### Notice
46
+
47
+ This program is free software: you can redistribute it and/or modify
48
+ it under the terms of the GNU General Public License as published by
49
+ the Free Software Foundation, either version 3 of the License, or
50
+ (at your option) any later version.
51
+
52
+ This program is distributed in the hope that it will be useful,
53
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
54
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
55
+ GNU General Public License for more details.
56
+
57
+ You should have received a copy of the GNU General Public License
58
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
59
+
60
+ ### Exceptions
61
+
62
+ **Some exclusions to this license apply; specifically portions of
63
+ [`strkit/viz/templates/browser.html`](strkit/viz/templates/browser.html) and
64
+ the STRkit logo files ([./docs/images/strkit_logo_small.png](./docs/images/strkit_logo_small.png)
65
+ and [./strkit/viz/static/logo.png](./strkit/viz/static/logo.png).)**
38
66
 
39
67
  The STRkit logo is &copy; David Lougheed 2022, and was designed by Evelyn Lougheed. It is not licensed
40
- under the terms of the GPL 3.0; all rights are reserved.
68
+ under the terms of the GPL 3.0; it is instead licensed under the terms of the
69
+ [CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/).
70
+
71
+ Portions of `viz/templates/browser.html` copyright (C) 2021-2022 Observable, Inc.
72
+ Used under the terms of the ISC license.
73
+
41
74
 
42
75
  ## Installation
43
76
 
@@ -99,7 +132,8 @@ strkit call \
99
132
  --min-reads 4 \ # Minimum number of supporting reads needed to make a call
100
133
  --min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
101
134
  --flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
102
- --seed 183 # Fixed random number generator seed for replicability
135
+ --seed 183 \ # Fixed random number generator seed for replicability
136
+ --processes 10 # Number of parallel processes to use; DEFAULT: 1
103
137
  ```
104
138
 
105
139
  Ideally, you should be using a read file aligned with parameters tuned for tandem repeats.
@@ -215,33 +249,3 @@ by looking at inheritance patterns. This tool provides a few different ways to d
215
249
 
216
250
  **For more information on what kind of analyses can be done with this data**, see the
217
251
  [Trio analyses with STRkit](./docs/trio_analyses.md) page.
218
-
219
-
220
-
221
- ## Copyright and License
222
-
223
- **Some exclusions to this license apply; specifically portions of
224
- [`strkit/viz/templates/browser.html`](strkit/viz/templates/browser.html)
225
- and files in the `catalogs` directory.**
226
-
227
- &copy; David Lougheed 2021-2023 (versions up to and including `0.8.0a1`).
228
-
229
- &copy; David Lougheed 2021-2023 with portions &copy; McGill University 2023 (versions beyond `0.8.0a1`).
230
-
231
- Portions of `viz/templates/browser.html` copyright (C) 2021-2022 Observable, Inc.
232
- Used under the terms of the ISC license.
233
-
234
- ### Notice
235
-
236
- This program is free software: you can redistribute it and/or modify
237
- it under the terms of the GNU General Public License as published by
238
- the Free Software Foundation, either version 3 of the License, or
239
- (at your option) any later version.
240
-
241
- This program is distributed in the hope that it will be useful,
242
- but WITHOUT ANY WARRANTY; without even the implied warranty of
243
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
244
- GNU General Public License for more details.
245
-
246
- You should have received a copy of the GNU General Public License
247
- along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -6,13 +6,46 @@ A genotyping and analysis toolkit for short(ish) tandem repeats.
6
6
 
7
7
  <img src="./docs/images/strkit_logo_small.png" alt="" width="500" height="324" />
8
8
 
9
- &copy; David Lougheed and McGill University 2021-2023 (versions up to and including `0.8.0a1`).
10
9
 
11
- &copy; David Lougheed and McGill University 2021-2023, with portions &copy; McGill University 2023
12
- (versions beyond `0.8.0a1`).
10
+ ## Copyright and License
11
+
12
+ * 2021-2023: &copy; David Lougheed (DL) and McGill University 2021-2023 (versions up to and including `0.8.0a1`),
13
+ created during graduate research by DL.
14
+ * 2023+: (versions beyond `0.8.0a1`):
15
+ * Portions &copy; DL and McGill University 2021-2023
16
+ * Portions &copy; McGill University 2023
17
+ * Portions &copy; DL 2023
18
+
19
+
20
+ ### Notice
21
+
22
+ This program is free software: you can redistribute it and/or modify
23
+ it under the terms of the GNU General Public License as published by
24
+ the Free Software Foundation, either version 3 of the License, or
25
+ (at your option) any later version.
26
+
27
+ This program is distributed in the hope that it will be useful,
28
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
29
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
+ GNU General Public License for more details.
31
+
32
+ You should have received a copy of the GNU General Public License
33
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
34
+
35
+ ### Exceptions
36
+
37
+ **Some exclusions to this license apply; specifically portions of
38
+ [`strkit/viz/templates/browser.html`](strkit/viz/templates/browser.html) and
39
+ the STRkit logo files ([./docs/images/strkit_logo_small.png](./docs/images/strkit_logo_small.png)
40
+ and [./strkit/viz/static/logo.png](./strkit/viz/static/logo.png).)**
13
41
 
14
42
  The STRkit logo is &copy; David Lougheed 2022, and was designed by Evelyn Lougheed. It is not licensed
15
- under the terms of the GPL 3.0; all rights are reserved.
43
+ under the terms of the GPL 3.0; it is instead licensed under the terms of the
44
+ [CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/).
45
+
46
+ Portions of `viz/templates/browser.html` copyright (C) 2021-2022 Observable, Inc.
47
+ Used under the terms of the ISC license.
48
+
16
49
 
17
50
  ## Installation
18
51
 
@@ -74,7 +107,8 @@ strkit call \
74
107
  --min-reads 4 \ # Minimum number of supporting reads needed to make a call
75
108
  --min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
76
109
  --flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
77
- --seed 183 # Fixed random number generator seed for replicability
110
+ --seed 183 \ # Fixed random number generator seed for replicability
111
+ --processes 10 # Number of parallel processes to use; DEFAULT: 1
78
112
  ```
79
113
 
80
114
  Ideally, you should be using a read file aligned with parameters tuned for tandem repeats.
@@ -190,33 +224,3 @@ by looking at inheritance patterns. This tool provides a few different ways to d
190
224
 
191
225
  **For more information on what kind of analyses can be done with this data**, see the
192
226
  [Trio analyses with STRkit](./docs/trio_analyses.md) page.
193
-
194
-
195
-
196
- ## Copyright and License
197
-
198
- **Some exclusions to this license apply; specifically portions of
199
- [`strkit/viz/templates/browser.html`](strkit/viz/templates/browser.html)
200
- and files in the `catalogs` directory.**
201
-
202
- &copy; David Lougheed 2021-2023 (versions up to and including `0.8.0a1`).
203
-
204
- &copy; David Lougheed 2021-2023 with portions &copy; McGill University 2023 (versions beyond `0.8.0a1`).
205
-
206
- Portions of `viz/templates/browser.html` copyright (C) 2021-2022 Observable, Inc.
207
- Used under the terms of the ISC license.
208
-
209
- ### Notice
210
-
211
- This program is free software: you can redistribute it and/or modify
212
- it under the terms of the GNU General Public License as published by
213
- the Free Software Foundation, either version 3 of the License, or
214
- (at your option) any later version.
215
-
216
- This program is distributed in the hope that it will be useful,
217
- but WITHOUT ANY WARRANTY; without even the implied warranty of
218
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
219
- GNU General Public License for more details.
220
-
221
- You should have received a copy of the GNU General Public License
222
- along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -15,7 +15,7 @@ setup(
15
15
  python_requires="~=3.9",
16
16
  install_requires=[
17
17
  "Flask>=2.2.5,<2.3",
18
- "pysam>=0.19,<0.22",
18
+ "pysam>=0.19,<0.23",
19
19
  "numpy>=1.23.4,<1.27",
20
20
  "parasail>=1.2.4,<1.4",
21
21
  "scikit-learn>=1.2.1,<1.4",
@@ -0,0 +1 @@
1
+ 0.11.1
@@ -42,19 +42,24 @@ N_GM_INIT = 3
42
42
 
43
43
  FLOAT_32_EPSILON = np.finfo(np.float32).eps
44
44
 
45
+ CI_PERCENTILE_RANGES = {
46
+ "95": (2.5, 97.5),
47
+ "99": (0.5, 99.5),
48
+ }
49
+
45
50
 
46
51
  def _array_as_int(n: Union[NDArray[np.int_], NDArray[np.float_]]) -> NDArray[np.int32]:
47
52
  return np.rint(n).astype(np.int32)
48
53
 
49
54
 
50
- def _calculate_cis(samples, force_int: bool = False, ci: str = "95") -> Union[NDArray[np.int32], NDArray[np.float_]]:
51
- r = {
52
- "95": (2.5, 97.5),
53
- "99": (0.5, 99.5),
54
- }[ci]
55
- # TODO: enable for numpy >=1.22
56
- # percentiles = np.percentile(samples, r, axis=1, method="interpolated_inverted_cdf").transpose()
57
- percentiles = np.percentile(samples, r, axis=1, interpolation="nearest").transpose()
55
+ def _calculate_cis(
56
+ samples,
57
+ force_int: bool = False,
58
+ ci: str = Literal["95", "99"],
59
+ ) -> Union[NDArray[np.int32], NDArray[np.float_]]:
60
+ percentiles = np.percentile(
61
+ samples, CI_PERCENTILE_RANGES[ci], axis=1, method="interpolated_inverted_cdf"
62
+ ).transpose()
58
63
  return _array_as_int(percentiles) if force_int else percentiles
59
64
 
60
65
 
@@ -139,7 +144,7 @@ def fit_gmm(
139
144
 
140
145
 
141
146
  class CallDict(TypedDict):
142
- call: Union[NDArray[np.int32], NDArray[np.float]]
147
+ call: Union[NDArray[np.int32], NDArray[np.float_]]
143
148
  call_95_cis: Union[NDArray[NDArray[np.int32]], NDArray[NDArray[np.float_]]]
144
149
  call_99_cis: Union[NDArray[NDArray[np.int32]], NDArray[NDArray[np.float_]]]
145
150
  peaks: NDArray[np.float_]
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import itertools
4
5
  import logging
5
6
  import multiprocessing as mp
6
7
  import numpy as np
7
8
  import pysam
9
+ import operator
8
10
  import queue
9
11
 
10
12
  from collections import Counter
@@ -55,10 +57,21 @@ significant_clip_threshold = 100
55
57
  significant_clip_snv_take_in = 250
56
58
 
57
59
 
60
+ # property getters & other partials
61
+ cn_getter = operator.itemgetter("cn")
62
+ weight_getter = operator.itemgetter("w")
63
+ cat_strs = "".join
64
+
65
+
66
+ @functools.cache
67
+ def _mask_low_q_base(base_and_qual: tuple[str, int]) -> str:
68
+ return base_and_qual[0] if base_and_qual[1] > base_wildcard_threshold else "X"
69
+
70
+
58
71
  def calculate_seq_with_wildcards(qs: str, quals: Optional[list[int]]) -> str:
59
72
  if quals is None:
60
73
  return qs # No quality information, so don't do anything
61
- return "".join(map(lambda x: x[0] if x[1] > base_wildcard_threshold else "X", zip(qs, quals)))
74
+ return cat_strs(map(_mask_low_q_base, zip(qs, quals)))
62
75
 
63
76
 
64
77
  def get_read_coords_from_matched_pairs(
@@ -79,7 +92,7 @@ def get_read_coords_from_matched_pairs(
79
92
 
80
93
  # Skip gaps on either side to find mapped flank indices
81
94
 
82
- # Binary search for left flank start
95
+ # Binary search for left flank start -------------------------------------------------------------------------------
83
96
  lhs, found = find_pair_by_ref_pos(matched_pairs, left_flank_coord)
84
97
 
85
98
  # lhs now contains the index for the closest starting coordinate to left_flank_coord
@@ -94,6 +107,7 @@ def get_read_coords_from_matched_pairs(
94
107
  lhs -= 1
95
108
 
96
109
  left_flank_start, _ = matched_pairs[lhs]
110
+ # ------------------------------------------------------------------------------------------------------------------
97
111
 
98
112
  for query_coord, ref_coord in matched_pairs[lhs+1:]:
99
113
  # Skip gaps on either side to find mapped flank indices
@@ -216,7 +230,7 @@ def calculate_read_distance(
216
230
  r1 = read_dict_items[i][1]
217
231
  r1_snv_u = r1["snvu"]
218
232
 
219
- r1_out_of_range: set[int] = {y for y in useful_snvs_range if r1_snv_u[y] == SNV_OUT_OF_RANGE_CHAR}
233
+ r1_out_of_range: set[int] = set(filter(lambda y: r1_snv_u[y] == SNV_OUT_OF_RANGE_CHAR, useful_snvs_range))
220
234
 
221
235
  for j in range(i + 1, n_reads):
222
236
  r2 = read_dict_items[j][1]
@@ -359,11 +373,13 @@ def call_alleles_with_incorporated_snvs(
359
373
  continue
360
374
 
361
375
  read_useful_snv_bases = tuple(snv_bases[bi] for bi, _pos in useful_snvs)
362
- non_blank_read_useful_snv_bases = [bb for bb in read_useful_snv_bases if bb != SNV_OUT_OF_RANGE_CHAR]
376
+ n_non_blank_read_useful_snv_bases = len(
377
+ tuple(filter(lambda bb: bb != SNV_OUT_OF_RANGE_CHAR, read_useful_snv_bases))
378
+ )
363
379
 
364
- if nbr := len(non_blank_read_useful_snv_bases): # TODO: parametrize
380
+ if n_non_blank_read_useful_snv_bases: # TODO: parametrize
365
381
  read_dict_items_with_at_least_one_snv.append(read_item)
366
- if nbr >= 2: # TODO: parametrize
382
+ if n_non_blank_read_useful_snv_bases >= 2: # TODO: parametrize
367
383
  read_dict_items_with_many_snvs.append(read_item)
368
384
  else:
369
385
  read_dict_items_with_no_snvs.append(read_item)
@@ -371,7 +387,7 @@ def call_alleles_with_incorporated_snvs(
371
387
  read["snvu"] = read_useful_snv_bases # Store read-level 'useful' SNVs
372
388
 
373
389
  if print_snvs:
374
- print(rn, f"\t{read['cn']:.0f}", "\t", "".join(read_useful_snv_bases), nbr)
390
+ print(rn, f"\t{read['cn']:.0f}", "\t", cat_strs(read_useful_snv_bases), n_non_blank_read_useful_snv_bases)
375
391
 
376
392
  n_reads_with_many_snvs: int = len(read_dict_items_with_many_snvs)
377
393
  n_reads_with_at_least_one_snv: int = len(read_dict_items_with_at_least_one_snv)
@@ -424,25 +440,23 @@ def call_alleles_with_incorporated_snvs(
424
440
  cluster_labels = c.labels_
425
441
  cluster_indices = tuple(range(n_alleles))
426
442
 
427
- cluster_reads: list[list[ReadDict]] = []
443
+ cluster_reads: list[tuple[ReadDict, ...]] = []
428
444
  cns: Union[list[list[int]], list[list[float]]] = []
429
445
  c_ws: list[Union[NDArray[np.int_], NDArray[np.float_]]] = []
430
446
 
431
447
  for ci in cluster_indices:
432
- crs: list[ReadDict] = []
433
-
434
448
  # Find reads for cluster
435
- for i, (_, r) in enumerate(read_dict_items_with_at_least_one_snv):
436
- if cluster_labels[i] == ci:
437
- crs.append(r)
449
+ crs: tuple[ReadDict, ...] = tuple(
450
+ r for i, (_, r) in enumerate(read_dict_items_with_at_least_one_snv)
451
+ if cluster_labels[i] == ci
452
+ )
438
453
 
439
454
  # Calculate copy number set
440
- cns.append([r["cn"] for r in crs])
455
+ cns.append(list(map(cn_getter, crs)))
441
456
 
442
457
  # Calculate weights array
443
- ws = np.fromiter((r["w"] for r in crs), dtype=np.float_)
444
- ws = ws / np.sum(ws)
445
- c_ws.append(ws)
458
+ ws = np.fromiter(map(weight_getter, crs), dtype=np.float_)
459
+ c_ws.append(ws / np.sum(ws))
446
460
 
447
461
  cluster_reads.append(crs)
448
462
 
@@ -501,9 +515,7 @@ def call_alleles_with_incorporated_snvs(
501
515
  for rd in cluster_reads_ordered[i]:
502
516
  rd["p"] = i
503
517
 
504
- # Make peak weights sum to 1
505
- peak_weights = np.concatenate(tuple(cc["peak_weights"] for cc in cdd), axis=0)
506
- peak_weights /= np.sum(peak_weights)
518
+ peak_weights_pre_adj = np.concatenate(tuple(cc["peak_weights"] for cc in cdd), axis=0)
507
519
 
508
520
  # All call_datas are truth-y; all arrays should be ordered by peak_order
509
521
  call_data = {
@@ -513,7 +525,8 @@ def call_alleles_with_incorporated_snvs(
513
525
  "peaks": np.concatenate(tuple(cc["peaks"] for cc in cdd_ordered), axis=None),
514
526
 
515
527
  # TODO: Readjust peak weights when combining or don't include
516
- "peak_weights": peak_weights,
528
+ # Make peak weights sum to 1
529
+ "peak_weights": peak_weights_pre_adj / np.sum(peak_weights_pre_adj),
517
530
 
518
531
  "peak_stdevs": np.concatenate(tuple(cc["peak_stdevs"] for cc in cdd_ordered), axis=0),
519
532
  "modal_n_peaks": n_alleles, # n. of alleles = n. of peaks always -- if we phased using SNVs
@@ -904,7 +917,8 @@ def call_locus(
904
917
  # reference genome. If we have a rare realignment (e.g., a large expansion), we cannot use SNVs.
905
918
  have_rare_realigns: bool = False
906
919
  for rn, read in read_dict_items:
907
- n_same_cn_no_realign = sum(1 for _, r2 in read_dict_items if not r2.get("realn") and r2["cn"] == read["cn"])
920
+ read_cn = read["cn"]
921
+ n_same_cn_no_realign = sum(1 for _, r2 in read_dict_items if not r2.get("realn") and r2["cn"] == read_cn)
908
922
  if read.get("realn") and n_same_cn_no_realign == 0:
909
923
  have_rare_realigns = True
910
924
  break
@@ -960,9 +974,9 @@ def call_locus(
960
974
  if single_or_dist_assign: # Didn't use SNVs, so call the 'old-fashioned' way - using only copy number
961
975
  # Dicts are ordered in Python; very nice :)
962
976
  rdvs = tuple(read_dict.values())
963
- rcns = tuple(map(lambda rr: rr["cn"], rdvs))
977
+ rcns = tuple(map(cn_getter, rdvs))
964
978
  read_cns = np.fromiter(rcns, dtype=np.float_ if fractional else np.int_)
965
- read_weights = np.fromiter(map(lambda rr: rr["w"], rdvs), dtype=np.float_)
979
+ read_weights = np.fromiter(map(weight_getter, rdvs), dtype=np.float_)
966
980
  read_weights = read_weights / np.sum(read_weights) # Normalize to probabilities
967
981
 
968
982
  call_data = call_alleles(
@@ -1005,9 +1019,7 @@ def call_locus(
1005
1019
  stdevs: NDArray[np.float_] = call_stdevs[:call_modal_n]
1006
1020
  weights: NDArray[np.float_] = call_weights[:call_modal_n]
1007
1021
 
1008
- allele_reads: list[list[str]] = []
1009
- for _ in range(call_modal_n):
1010
- allele_reads.append([])
1022
+ allele_reads: list[list[str]] = [list() for _ in range(call_modal_n)]
1011
1023
 
1012
1024
  for r, rd in read_dict_items:
1013
1025
  # Need latter term for peaks that we overwrite if we revert to "dist" assignment:
@@ -1053,7 +1065,7 @@ def call_locus(
1053
1065
  "stdevs": call_stdevs.tolist(), # from np.ndarray
1054
1066
  "modal_n": call_modal_n,
1055
1067
  "n_reads": call_peak_n_reads,
1056
- **({"kmers": [dict(c) for c in peak_kmers]} if count_kmers in ("peak", "both") else {}),
1068
+ **({"kmers": list(map(dict, peak_kmers))} if count_kmers in ("peak", "both") else {}),
1057
1069
  } if call_data else None
1058
1070
 
1059
1071
  # Calculate call time ----------------------------------------------------------------------------------------------
@@ -1066,11 +1078,15 @@ def call_locus(
1066
1078
 
1067
1079
  # Finally, compile the call into a dictionary with all information to return ---------------------------------------
1068
1080
 
1069
- def _ndarray_serialize(x: Iterable) -> list[Union[int, float, np.int_, np.float_]]:
1070
- return [(round(y) if not fractional else round_to_base_pos(y, motif_size)) for y in x]
1081
+ if fractional:
1082
+ def _ndarray_serialize(x: Iterable) -> list[Union[float, np.float_]]:
1083
+ return [round_to_base_pos(y, motif_size) for y in x]
1084
+ else:
1085
+ def _ndarray_serialize(x: Iterable) -> list[Union[int, float, np.int_, np.float_]]:
1086
+ return list(map(round, x))
1071
1087
 
1072
1088
  def _nested_ndarray_serialize(x: Iterable) -> list[list[Union[int, float, np.int_, np.float_]]]:
1073
- return [_ndarray_serialize(y) for y in x]
1089
+ return list(map(_ndarray_serialize, x))
1074
1090
 
1075
1091
  call_val = apply_or_none(_ndarray_serialize, call)
1076
1092
  call_95_cis_val = apply_or_none(_nested_ndarray_serialize, call_95_cis)
@@ -13,6 +13,15 @@ __all__ = [
13
13
  ]
14
14
 
15
15
 
16
+ def _line_filter_fn(s: str) -> bool:
17
+ """
18
+ Filter function to skip blank lines and comments
19
+ :param s: line of a file
20
+ :return: whether the line is not blank and is not a comment
21
+ """
22
+ return s and not s.startswith("#")
23
+
24
+
16
25
  # noinspection PyUnusedLocal
17
26
  class BaseCalculator(ABC):
18
27
  def __init__(
@@ -73,33 +82,22 @@ class BaseCalculator(ABC):
73
82
  def mt_corr(self) -> str:
74
83
  return self._mt_corr
75
84
 
76
- def _make_loci_dict(self) -> dict:
85
+ def _make_loci_dict(self) -> dict[tuple[str, str, str], list[str, ...]]:
77
86
  if not self._loci_file:
78
87
  return {}
79
88
 
80
89
  with open(self._loci_file, "r") as lf:
81
90
  return {
82
91
  tuple(d[:3]): d[3:]
83
- for d in (
84
- line.split("\t")
85
- for line in map(lambda x: x.strip(), lf)
86
- if line and not line.startswith("#")
87
- )
92
+ for d in map(lambda line: line.split("\t"), filter(_line_filter_fn, map(str.strip, lf)))
88
93
  }
89
94
 
90
- def _make_exclude_set(self) -> set:
95
+ def _make_exclude_set(self) -> set[tuple[str, str, str]]:
91
96
  if not self._exclude_file:
92
97
  return set()
93
98
 
94
99
  with open(self._exclude_file, "r") as lf:
95
- return {
96
- tuple(d[:3])
97
- for d in (
98
- line.split("\t")
99
- for line in map(lambda x: x.strip(), lf)
100
- if line and not line.startswith("#")
101
- )
102
- }
100
+ return set(map(lambda line: tuple(line.split("\t")[:3]), filter(_line_filter_fn, map(str.strip, lf))))
103
101
 
104
102
  def should_exclude_locus(self, locus: tuple[str, str, str]) -> bool:
105
103
  return locus in self._exclude_set
@@ -136,8 +134,8 @@ class BaseCalculator(ABC):
136
134
  def calculate(self, included_contigs: set) -> Optional[MIResult]:
137
135
  res: float = 0
138
136
  res_pm1: float = 0
139
- res_95_ci: float = 0
140
- res_99_ci: float = 0
137
+ res_95_ci: Optional[float] = None
138
+ res_99_ci: Optional[float] = None
141
139
  n_total: int = 0
142
140
 
143
141
  contig_results = []
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import itertools
4
4
  import logging
5
-
5
+ import math
6
6
  import numpy as np
7
7
  import sys
8
8
  import scipy.stats as sst
@@ -486,11 +486,13 @@ class MIResult:
486
486
  self._logger.warning("Cannot correct for multiple testing when test is not enabled")
487
487
  return
488
488
 
489
- loci = [locus for cr in self.contig_results for locus in cr]
490
489
  p_values = [locus.p_value for cr in self.contig_results for locus in cr]
491
490
  not_tested = {i for i, p in enumerate(p_values) if p is None}
492
491
 
493
- loci_filt = [locus for i, locus in enumerate(loci) if i not in not_tested]
492
+ loci_filt = [
493
+ locus for i, locus in enumerate(locus for cr in self.contig_results for locus in cr)
494
+ if i not in not_tested
495
+ ]
494
496
  p_values_filt = np.fromiter((p for i, p in enumerate(p_values) if i not in not_tested), dtype=np.float64)
495
497
 
496
498
  if (mtm := self._mt_corr) != "none":
@@ -517,16 +519,44 @@ class MIResult:
517
519
  def _res_str(res: Union[float, int]) -> str:
518
520
  return f"{res:.1f}" if isinstance(res, float) else str(res)
519
521
 
522
+ def _std_err_for_bernoulli(self, val: float) -> float:
523
+ return math.sqrt((val * (1 - val)) / self._n_loci_seen)
524
+
525
+ def _estimate_ci_for_bernoulli(self, val: float) -> tuple[float, float]:
526
+ std_err = self._std_err_for_bernoulli(val)
527
+ # 1.96 standard deviations for 95% CI, by CLM
528
+ return val - 1.96*std_err, val + 1.96*std_err
529
+
520
530
  def write_report_json(self, json_path: str, bin_width: int = 10):
521
531
  hist, _bins = self.calculate_histogram(bin_width=bin_width)
522
532
 
533
+ # get 95% CI from standard error, which we estimate from Bernoulli distribution of resulting percentage
534
+
523
535
  obj = {
524
- "mi": self.mi_value,
525
- "mi_pm1": self.mi_value_pm1,
526
- "mi_95": self.mi_value_95_ci,
527
- "mi_99": self.mi_value_99_ci,
536
+ "mi": {
537
+ "val": self.mi_value,
538
+ "est_std_err": self._std_err_for_bernoulli(self.mi_value),
539
+ "est_95_ci": self._estimate_ci_for_bernoulli(self.mi_value),
540
+ },
541
+ "mi_pm1": {
542
+ "val": self.mi_value_pm1,
543
+ "est_std_err": self._std_err_for_bernoulli(self.mi_value_pm1),
544
+ "est_95_ci": self._estimate_ci_for_bernoulli(self.mi_value_pm1),
545
+ },
546
+ "mi_95": {
547
+ "val": self.mi_value_95_ci,
548
+ "est_std_err": self._std_err_for_bernoulli(self.mi_value_95_ci),
549
+ "est_95_ci": self._estimate_ci_for_bernoulli(self.mi_value_95_ci),
550
+ } if self.mi_value_95_ci else None,
551
+ "mi_99": {
552
+ "val": self.mi_value_99_ci,
553
+ "est_std_err": self._std_err_for_bernoulli(self.mi_value_99_ci),
554
+ "est_95_ci": self._estimate_ci_for_bernoulli(self.mi_value_99_ci),
555
+ } if self.mi_value_99_ci else None,
556
+
528
557
  "n_loci_trio_called": sum((len(lr) for lr in self.contig_results)),
529
558
  "n_loci_total": self._n_loci_seen,
559
+
530
560
  "test": self._test_to_perform,
531
561
  "hist": hist,
532
562
  "significant_loci": [dict(nm) for nm in self.sorted_output_loci],
Binary file
@@ -211,6 +211,7 @@
211
211
  let igvContainer = null;
212
212
  let reportMetadataTable = null;
213
213
 
214
+ let sampleDisplay = null;
214
215
  let regionDisplay = null;
215
216
  let motifDisplay = null;
216
217
  let refGenome = null;
@@ -284,7 +285,7 @@
284
285
  const renderCall = () => {
285
286
  const callData = state.callData;
286
287
 
287
- console.log("call data", callData);
288
+ console.debug("call data", callData);
288
289
 
289
290
  /** @type number[] */
290
291
  const cns = Object.values(callData.reads).map(read => read.cn);
@@ -456,13 +457,19 @@
456
457
  const fetchParams = () =>
457
458
  fetch(`/params`)
458
459
  .then(r => r.json())
459
- .then(ps => {params = ps;})
460
+ .then(ps => {
461
+ params = ps;
462
+ console.debug("params", ps);
463
+ })
460
464
  .catch(console.error);
461
465
 
462
466
  const fetchReportMetadata = () =>
463
467
  fetch("/report-metadata")
464
468
  .then(r => r.json())
465
- .then(rm => {reportMetadata = rm;})
469
+ .then(rm => {
470
+ reportMetadata = rm;
471
+ console.debug("report metadata", rm);
472
+ })
466
473
  .catch(console.error);
467
474
 
468
475
  const handleSearchResultClick = i => {
@@ -489,6 +496,7 @@
489
496
  igvContainer = document.getElementById("igv-browser");
490
497
  reportMetadataTable = document.getElementById("report-metadata");
491
498
 
499
+ sampleDisplay = document.getElementById("sample");
492
500
  regionDisplay = document.getElementById("region");
493
501
  motifDisplay = document.getElementById("motif");
494
502
  refGenome = document.getElementById("ref-genome");
@@ -540,6 +548,10 @@
540
548
  });
541
549
 
542
550
  fetchReportMetadata().then(() => {
551
+ if (reportMetadata.sample_id) {
552
+ sampleDisplay.innerText = reportMetadata.sample_id;
553
+ }
554
+
543
555
  const other = {};
544
556
 
545
557
  const renderObj = (k, v) => {
@@ -588,7 +600,7 @@
588
600
  background-image: url("{{ url_for('static', filename='logo.png') }}");
589
601
  background-attachment: fixed;
590
602
  background-repeat: no-repeat;
591
- background-position: bottom right;
603
+ background-position: bottom 24px right 36px;
592
604
  background-size: 300px auto;
593
605
  }
594
606
 
@@ -626,6 +638,11 @@
626
638
  font-weight: normal;
627
639
  }
628
640
 
641
+ #sample {
642
+ font-style: italic;
643
+ color: #666;
644
+ }
645
+
629
646
  #locus-search {
630
647
  position: relative;
631
648
  }
@@ -851,21 +868,23 @@
851
868
  }
852
869
 
853
870
 
854
- @media screen and (max-width: 1100px) {
871
+ @media screen and (max-width: 1320px) {
855
872
  h1 {
856
873
  font-size: 20px;
857
874
  }
858
-
859
- #tab-overview {
860
- flex-direction: column;
861
- }
862
875
  }
863
876
 
864
- @media screen and (max-width: 880px) {
877
+ @media screen and (max-width: 1100px) {
865
878
  h1 {
866
879
  font-size: 18px;
867
880
  }
868
881
 
882
+ #tab-overview {
883
+ flex-direction: column;
884
+ }
885
+ }
886
+
887
+ @media screen and (max-width: 980px) {
869
888
  header {
870
889
  flex-direction: column;
871
890
  }
@@ -891,7 +910,7 @@
891
910
  <div id="strkit">
892
911
  <header>
893
912
  <h1>
894
- STRkit Browser: <span id="region"></span> &mdash; <span id="motif"></span>
913
+ STRkit Browser: <span id="sample"></span> <span id="region"></span> &mdash; <span id="motif"></span>
895
914
  </h1>
896
915
  <div id="locus-search">
897
916
  <label for="locus-search-box">Select locus:</label>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strkit
3
- Version: 0.10.0
3
+ Version: 0.11.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
@@ -13,7 +13,7 @@ Requires-Python: ~=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: Flask<2.3,>=2.2.5
16
- Requires-Dist: pysam<0.22,>=0.19
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
19
  Requires-Dist: scikit-learn<1.4,>=1.2.1
@@ -31,13 +31,46 @@ A genotyping and analysis toolkit for short(ish) tandem repeats.
31
31
 
32
32
  <img src="./docs/images/strkit_logo_small.png" alt="" width="500" height="324" />
33
33
 
34
- &copy; David Lougheed and McGill University 2021-2023 (versions up to and including `0.8.0a1`).
35
34
 
36
- &copy; David Lougheed and McGill University 2021-2023, with portions &copy; McGill University 2023
37
- (versions beyond `0.8.0a1`).
35
+ ## Copyright and License
36
+
37
+ * 2021-2023: &copy; David Lougheed (DL) and McGill University 2021-2023 (versions up to and including `0.8.0a1`),
38
+ created during graduate research by DL.
39
+ * 2023+: (versions beyond `0.8.0a1`):
40
+ * Portions &copy; DL and McGill University 2021-2023
41
+ * Portions &copy; McGill University 2023
42
+ * Portions &copy; DL 2023
43
+
44
+
45
+ ### Notice
46
+
47
+ This program is free software: you can redistribute it and/or modify
48
+ it under the terms of the GNU General Public License as published by
49
+ the Free Software Foundation, either version 3 of the License, or
50
+ (at your option) any later version.
51
+
52
+ This program is distributed in the hope that it will be useful,
53
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
54
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
55
+ GNU General Public License for more details.
56
+
57
+ You should have received a copy of the GNU General Public License
58
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
59
+
60
+ ### Exceptions
61
+
62
+ **Some exclusions to this license apply; specifically portions of
63
+ [`strkit/viz/templates/browser.html`](strkit/viz/templates/browser.html) and
64
+ the STRkit logo files ([./docs/images/strkit_logo_small.png](./docs/images/strkit_logo_small.png)
65
+ and [./strkit/viz/static/logo.png](./strkit/viz/static/logo.png).)**
38
66
 
39
67
  The STRkit logo is &copy; David Lougheed 2022, and was designed by Evelyn Lougheed. It is not licensed
40
- under the terms of the GPL 3.0; all rights are reserved.
68
+ under the terms of the GPL 3.0; it is instead licensed under the terms of the
69
+ [CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/).
70
+
71
+ Portions of `viz/templates/browser.html` copyright (C) 2021-2022 Observable, Inc.
72
+ Used under the terms of the ISC license.
73
+
41
74
 
42
75
  ## Installation
43
76
 
@@ -99,7 +132,8 @@ strkit call \
99
132
  --min-reads 4 \ # Minimum number of supporting reads needed to make a call
100
133
  --min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
101
134
  --flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
102
- --seed 183 # Fixed random number generator seed for replicability
135
+ --seed 183 \ # Fixed random number generator seed for replicability
136
+ --processes 10 # Number of parallel processes to use; DEFAULT: 1
103
137
  ```
104
138
 
105
139
  Ideally, you should be using a read file aligned with parameters tuned for tandem repeats.
@@ -215,33 +249,3 @@ by looking at inheritance patterns. This tool provides a few different ways to d
215
249
 
216
250
  **For more information on what kind of analyses can be done with this data**, see the
217
251
  [Trio analyses with STRkit](./docs/trio_analyses.md) page.
218
-
219
-
220
-
221
- ## Copyright and License
222
-
223
- **Some exclusions to this license apply; specifically portions of
224
- [`strkit/viz/templates/browser.html`](strkit/viz/templates/browser.html)
225
- and files in the `catalogs` directory.**
226
-
227
- &copy; David Lougheed 2021-2023 (versions up to and including `0.8.0a1`).
228
-
229
- &copy; David Lougheed 2021-2023 with portions &copy; McGill University 2023 (versions beyond `0.8.0a1`).
230
-
231
- Portions of `viz/templates/browser.html` copyright (C) 2021-2022 Observable, Inc.
232
- Used under the terms of the ISC license.
233
-
234
- ### Notice
235
-
236
- This program is free software: you can redistribute it and/or modify
237
- it under the terms of the GNU General Public License as published by
238
- the Free Software Foundation, either version 3 of the License, or
239
- (at your option) any later version.
240
-
241
- This program is distributed in the hope that it will be useful,
242
- but WITHOUT ANY WARRANTY; without even the implied warranty of
243
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
244
- GNU General Public License for more details.
245
-
246
- You should have received a copy of the GNU General Public License
247
- along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -1,5 +1,5 @@
1
1
  Flask<2.3,>=2.2.5
2
- pysam<0.22,>=0.19
2
+ pysam<0.23,>=0.19
3
3
  numpy<1.27,>=1.23.4
4
4
  parasail<1.4,>=1.2.4
5
5
  scikit-learn<1.4,>=1.2.1
@@ -1 +0,0 @@
1
- 0.10.0
Binary file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes