strkit 0.12.0__tar.gz → 0.12.0a1__tar.gz

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