strkit 0.11.8__tar.gz → 0.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {strkit-0.11.8/strkit.egg-info → strkit-0.12.0}/PKG-INFO +9 -2
  2. {strkit-0.11.8 → strkit-0.12.0}/README.md +7 -1
  3. {strkit-0.11.8 → strkit-0.12.0}/setup.py +1 -0
  4. strkit-0.12.0/strkit/VERSION +1 -0
  5. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/call_locus.py +20 -8
  6. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/call_sample.py +32 -66
  7. strkit-0.12.0/strkit/call/consensus.py +26 -0
  8. strkit-0.12.0/strkit/call/output/__init__.py +9 -0
  9. strkit-0.12.0/strkit/call/output/json_report.py +46 -0
  10. strkit-0.12.0/strkit/call/output/tsv.py +34 -0
  11. strkit-0.12.0/strkit/call/output/vcf.py +170 -0
  12. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/types.py +6 -4
  13. {strkit-0.11.8 → strkit-0.12.0}/strkit/constants.py +0 -13
  14. {strkit-0.11.8 → strkit-0.12.0}/strkit/entry.py +25 -7
  15. strkit-0.12.0/strkit/mi/repeathmm.py +92 -0
  16. strkit-0.12.0/strkit/mi/straglr.py +98 -0
  17. strkit-0.12.0/strkit/mi/tandem_genotypes.py +88 -0
  18. {strkit-0.11.8 → strkit-0.12.0/strkit.egg-info}/PKG-INFO +9 -2
  19. {strkit-0.11.8 → strkit-0.12.0}/strkit.egg-info/SOURCES.txt +5 -0
  20. {strkit-0.11.8 → strkit-0.12.0}/strkit.egg-info/requires.txt +1 -0
  21. strkit-0.11.8/strkit/VERSION +0 -1
  22. strkit-0.11.8/strkit/mi/repeathmm.py +0 -192
  23. strkit-0.11.8/strkit/mi/straglr.py +0 -201
  24. strkit-0.11.8/strkit/mi/tandem_genotypes.py +0 -172
  25. {strkit-0.11.8 → strkit-0.12.0}/LICENSE +0 -0
  26. {strkit-0.11.8 → strkit-0.12.0}/MANIFEST.in +0 -0
  27. {strkit-0.11.8 → strkit-0.12.0}/pyproject.toml +0 -0
  28. {strkit-0.11.8 → strkit-0.12.0}/setup.cfg +0 -0
  29. {strkit-0.11.8 → strkit-0.12.0}/strkit/__init__.py +0 -0
  30. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/__init__.py +0 -0
  31. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/align_matrix.py +0 -0
  32. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/allele.py +0 -0
  33. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/cigar.py +0 -0
  34. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/non_daemonic_pool.py +0 -0
  35. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/realign.py +0 -0
  36. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/repeats.py +0 -0
  37. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/snvs.py +0 -0
  38. {strkit-0.11.8 → strkit-0.12.0}/strkit/call/utils.py +0 -0
  39. {strkit-0.11.8 → strkit-0.12.0}/strkit/catalog/__init__.py +0 -0
  40. {strkit-0.11.8 → strkit-0.12.0}/strkit/catalog/combine.py +0 -0
  41. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/__init__.py +0 -0
  42. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/_bed_4.py +0 -0
  43. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/converter.py +0 -0
  44. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/expansionhunter.py +0 -0
  45. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/gangstr.py +0 -0
  46. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/hipstr.py +0 -0
  47. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/straglr.py +0 -0
  48. {strkit-0.11.8 → strkit-0.12.0}/strkit/convert/tandem_genotypes.py +0 -0
  49. {strkit-0.11.8 → strkit-0.12.0}/strkit/exceptions.py +0 -0
  50. {strkit-0.11.8 → strkit-0.12.0}/strkit/json.py +0 -0
  51. {strkit-0.11.8 → strkit-0.12.0}/strkit/logger.py +0 -0
  52. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/__init__.py +0 -0
  53. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/base.py +0 -0
  54. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/expansionhunter.py +0 -0
  55. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/gangstr.py +0 -0
  56. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/result.py +0 -0
  57. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/strkit.py +0 -0
  58. {strkit-0.11.8 → strkit-0.12.0}/strkit/mi/vcf_utils.py +0 -0
  59. {strkit-0.11.8 → strkit-0.12.0}/strkit/utils.py +0 -0
  60. {strkit-0.11.8 → strkit-0.12.0}/strkit/viz/__init__.py +0 -0
  61. {strkit-0.11.8 → strkit-0.12.0}/strkit/viz/server.py +0 -0
  62. {strkit-0.11.8 → strkit-0.12.0}/strkit/viz/static/logo.png +0 -0
  63. {strkit-0.11.8 → strkit-0.12.0}/strkit/viz/templates/browser.html +0 -0
  64. {strkit-0.11.8 → strkit-0.12.0}/strkit.egg-info/dependency_links.txt +0 -0
  65. {strkit-0.11.8 → strkit-0.12.0}/strkit.egg-info/entry_points.txt +0 -0
  66. {strkit-0.11.8 → strkit-0.12.0}/strkit.egg-info/top_level.txt +0 -0
  67. {strkit-0.11.8 → strkit-0.12.0}/tests/test_caller_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strkit
3
- Version: 0.11.8
3
+ Version: 0.12.0
4
4
  Summary: A toolkit for analyzing variation in short(ish) tandem repeats.
5
5
  Home-page: https://github.com/davidlougheed/strkit
6
6
  Author: David Lougheed
@@ -16,6 +16,7 @@ Requires-Dist: Flask<3.1,>=2.2.5
16
16
  Requires-Dist: pysam<0.23,>=0.19
17
17
  Requires-Dist: numpy<1.27,>=1.23.4
18
18
  Requires-Dist: parasail<1.4,>=1.2.4
19
+ Requires-Dist: pyfamsa<0.4,>=0.3.0
19
20
  Requires-Dist: scikit-learn<1.4,>=1.2.1
20
21
  Requires-Dist: scipy<1.12,>=1.10
21
22
  Requires-Dist: statsmodels<0.15,>=0.14.0
@@ -115,6 +116,8 @@ long read data should still work.
115
116
  * Whole-genome and targeted genotyping modes to adjust this re-weighting.
116
117
  * Incorporation of single-nucleotide variation (SNVs) for better and faster calling plus
117
118
  additional downstream analysis possibilities.
119
+ * Recommended for **HiFi data only**. In my testing, this worsens runtime and call quality for
120
+ ONT-UL data, but speeds up the tool and improves call quality for HiFi data.
118
121
  * Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
119
122
  * 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
120
123
 
@@ -128,10 +131,11 @@ strkit call \
128
131
  --realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
129
132
  --ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
130
133
  --loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
131
- --incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
134
+ --incorporate-snvs path/to/dbsnp.vcf.gz \ # [RECOMMENDED FOR HIFI ONLY] If you want SNV calls to help phase STRs & speed up calling
132
135
  --min-reads 4 \ # Minimum number of supporting reads needed to make a call
133
136
  --min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
134
137
  --flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
138
+ --consensus \ # If this flag is set, consensus sequences are calculated for alleles. This increases runtime!
135
139
  --seed 183 \ # Fixed random number generator seed for replicability
136
140
  --processes 10 # Number of parallel processes to use; DEFAULT: 1
137
141
  ```
@@ -160,6 +164,9 @@ specify a path to output a more detailed JSON document to. This document contain
160
164
  labels, and some other information that isn't included in the normal TSV file. If you want this
161
165
  file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
162
166
 
167
+ If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
168
+ `--vcf ...` argument.
169
+
163
170
  See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
164
171
  how to format a locus catalog or choose from existing available catalogs.
165
172
 
@@ -90,6 +90,8 @@ long read data should still work.
90
90
  * Whole-genome and targeted genotyping modes to adjust this re-weighting.
91
91
  * Incorporation of single-nucleotide variation (SNVs) for better and faster calling plus
92
92
  additional downstream analysis possibilities.
93
+ * Recommended for **HiFi data only**. In my testing, this worsens runtime and call quality for
94
+ ONT-UL data, but speeds up the tool and improves call quality for HiFi data.
93
95
  * Parallelized for faster computing on clusters and for ad-hoc fast analysis of single samples.
94
96
  * 95% confidence intervals on calls via a user-configurable optional parametric bootstrapping process.
95
97
 
@@ -103,10 +105,11 @@ strkit call \
103
105
  --realign \ # If using PacBio HiFi reads, enable this to enable local realignment / read recovery
104
106
  --ref path/to/reference.fa.gz \ # [REQUIRED] Indexed FASTA-formatted reference genome
105
107
  --loci path/to/loci.bed \ # [REQUIRED] TRF-formatted (or 4-col, with motif as last column) list of loci to genotype
106
- --incorporate-snvs path/to/dbsnp.vcf.gz \ # If you want SNV calls to help phase STRs & speed up STRkit, include this
108
+ --incorporate-snvs path/to/dbsnp.vcf.gz \ # [RECOMMENDED FOR HIFI ONLY] If you want SNV calls to help phase STRs & speed up calling
107
109
  --min-reads 4 \ # Minimum number of supporting reads needed to make a call
108
110
  --min-allele-reads 2 \ # Minimum number of supporting reads needed to call a specific allele size
109
111
  --flank-size 70 \ # Size of the flanking region to use on either side of a region to properly anchor reads
112
+ --consensus \ # If this flag is set, consensus sequences are calculated for alleles. This increases runtime!
110
113
  --seed 183 \ # Fixed random number generator seed for replicability
111
114
  --processes 10 # Number of parallel processes to use; DEFAULT: 1
112
115
  ```
@@ -135,6 +138,9 @@ specify a path to output a more detailed JSON document to. This document contain
135
138
  labels, and some other information that isn't included in the normal TSV file. If you want this
136
139
  file to be indented and human-readable, use the `--indent-json` flag in addition to `--json ...`.
137
140
 
141
+ If you want to output a VCF file (currently with STR sequences only, not with phased SNVs), use the
142
+ `--vcf ...` argument.
143
+
138
144
  See the '[Caller catalog format & choosing a catalog](./docs/caller_catalog.md)' page for more on
139
145
  how to format a locus catalog or choose from existing available catalogs.
140
146
 
@@ -18,6 +18,7 @@ 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",
21
22
  "scikit-learn>=1.2.1,<1.4",
22
23
  "scipy>=1.10,<1.12",
23
24
  "statsmodels>=0.14.0,<0.15",
@@ -0,0 +1 @@
1
+ 0.12.0
@@ -18,10 +18,11 @@ from sklearn.cluster import AgglomerativeClustering
18
18
  from numpy.typing import NDArray
19
19
  from typing import Iterable, Literal, Optional, Union
20
20
 
21
- from strkit.call.allele import CallDict, get_n_alleles, call_alleles
21
+ from strkit.call.allele import CallDict, call_alleles
22
22
  from strkit.utils import apply_or_none
23
23
 
24
24
  from .align_matrix import match_score
25
+ from .consensus import consensus_seq
25
26
  from .realign import realign_read
26
27
  from .repeats import get_repeat_count, get_ref_repeat_count
27
28
  from .snvs import (
@@ -109,6 +110,10 @@ def get_read_coords_from_matched_pairs(
109
110
  left_flank_start, _ = matched_pairs[lhs]
110
111
  # ------------------------------------------------------------------------------------------------------------------
111
112
 
113
+ # Binary search for left flank end ---------------------------------------------------------------------------------
114
+ # TODO
115
+ # ------------------------------------------------------------------------------------------------------------------
116
+
112
117
  for query_coord, ref_coord in matched_pairs[lhs+1:]:
113
118
  # Skip gaps on either side to find mapped flank indices
114
119
  if ref_coord < left_coord:
@@ -136,7 +141,7 @@ def get_read_coords_from_matched_pairs(
136
141
 
137
142
 
138
143
  def get_overlapping_segments_and_related_data(
139
- bfs: tuple[pysam.AlignmentFile],
144
+ bfs: tuple[pysam.AlignmentFile, ...],
140
145
  read_contig: str,
141
146
  left_flank_coord: int,
142
147
  right_flank_coord: int,
@@ -551,6 +556,7 @@ def debug_log_flanking_seq(logger_: logging.Logger, locus_log_str: str, rn: str,
551
556
  def call_locus(
552
557
  t_idx: int,
553
558
  t: tuple,
559
+ n_alleles: int,
554
560
  bfs: tuple[AlignmentFile, ...],
555
561
  ref: FastaFile,
556
562
  min_reads: int,
@@ -561,7 +567,6 @@ def call_locus(
561
567
  seed: int,
562
568
  logger_: logging.Logger,
563
569
  sample_id: Optional[str] = None,
564
- sex_chroms: Optional[str] = None,
565
570
  realign: bool = False,
566
571
  hq: bool = False,
567
572
  # incorporate_snvs: bool = False,
@@ -572,6 +577,7 @@ def call_locus(
572
577
  fractional: bool = False,
573
578
  respect_ref: bool = False,
574
579
  count_kmers: str = "none", # "none" | "peak" | "read"
580
+ consensus: bool = False,
575
581
  log_level: int = logging.WARNING,
576
582
  read_file_has_chr: bool = True,
577
583
  ref_file_has_chr: bool = True,
@@ -599,10 +605,6 @@ def call_locus(
599
605
  ref_seq: str = ""
600
606
  raised: bool = False
601
607
 
602
- n_alleles: Optional[int] = get_n_alleles(2, sex_chroms, contig)
603
- if n_alleles is None: # Sex chromosome, but we don't have a specified sex chromosome karyotype
604
- return None
605
-
606
608
  # Currently, only support diploid use of SNVs. There's not much of a point with haploid loci,
607
609
  # and polyploidy is hard.
608
610
  # should_incorporate_snvs: bool = incorporate_snvs and n_alleles == 2
@@ -799,6 +801,7 @@ def call_locus(
799
801
  flank_len: int = len(flank_left_seq) + len(flank_right_seq)
800
802
  tr_len_w_flank: int = tr_len + flank_len
801
803
 
804
+ tr_read_seq = qs[left_flank_end:right_flank_start]
802
805
  tr_read_seq_wc = calculate_seq_with_wildcards(qs[left_flank_end:right_flank_start], qqs)
803
806
 
804
807
  if count_kmers != "none":
@@ -848,6 +851,7 @@ def call_locus(
848
851
  read_dict_extra[rn] = {
849
852
  "_ref_start": segment_start,
850
853
  "_ref_end": segment_end,
854
+ **({"_tr_seq": tr_read_seq} if consensus else {}),
851
855
  }
852
856
 
853
857
  # Reads can show up more than once - TODO - cache this information across loci
@@ -886,6 +890,7 @@ def call_locus(
886
890
  }),
887
891
  "motif": motif,
888
892
  "ref_cn": ref_cn,
893
+ **({"ref_seq": ref_seq} if consensus else {}),
889
894
  "reads": read_dict,
890
895
  }
891
896
 
@@ -1008,12 +1013,13 @@ def call_locus(
1008
1013
  call_stdevs = call_data.get("peak_stdevs")
1009
1014
  call_modal_n = call_data.get("modal_n_peaks")
1010
1015
 
1011
- # Assign reads to peaks and compute peak k-mers --------------------------------------------------------------------
1016
+ # Assign reads to peaks and compute peak k-mers (and optionally consensus sequences) -------------------------------
1012
1017
 
1013
1018
  # We cannot call read-level cluster labels with >2 peaks using distance alone;
1014
1019
  # don't know how re-sampling has occurred.
1015
1020
  call_peak_n_reads: list[int] = []
1016
1021
  peak_kmers: list[Counter] = [Counter() for _ in range(call_modal_n or 0)]
1022
+ call_seqs: list[str] = []
1017
1023
  if read_peaks_called := call_modal_n and call_modal_n <= 2:
1018
1024
  peaks: NDArray[np.float_] = call_peaks[:call_modal_n]
1019
1025
  stdevs: NDArray[np.float_] = call_stdevs[:call_modal_n]
@@ -1059,6 +1065,11 @@ def call_locus(
1059
1065
 
1060
1066
  call_peak_n_reads = list(map(len, allele_reads))
1061
1067
 
1068
+ if consensus:
1069
+ call_seqs = list(
1070
+ map(lambda a: consensus_seq(map(lambda rr: read_dict_extra[rr]["_tr_seq"], a)), allele_reads)
1071
+ )
1072
+
1062
1073
  peak_data = {
1063
1074
  "means": call_peaks.tolist(), # from np.ndarray
1064
1075
  "weights": call_weights.tolist(), # from np.ndarray
@@ -1066,6 +1077,7 @@ def call_locus(
1066
1077
  "modal_n": call_modal_n,
1067
1078
  "n_reads": call_peak_n_reads,
1068
1079
  **({"kmers": list(map(dict, peak_kmers))} if count_kmers in ("peak", "both") else {}),
1080
+ **({"seqs": call_seqs} if consensus else {}),
1069
1081
  } if call_data else None
1070
1082
 
1071
1083
  # Calculate call time ----------------------------------------------------------------------------------------------
@@ -9,22 +9,19 @@ import pathlib
9
9
  import numpy as np
10
10
  import os
11
11
  import re
12
- import sys
13
12
  import time
14
13
 
15
14
  from datetime import datetime
16
15
  from multiprocessing.synchronize import Event as EventClass # For type hinting
17
16
  from pysam import AlignmentFile
18
17
 
19
- from typing import Literal, Optional, Union
20
-
21
- from strkit import __version__
22
-
23
- from strkit.json import dumps_indented, json
18
+ from typing import Literal, Optional
24
19
  from strkit.logger import logger
25
20
 
21
+ from .allele import get_n_alleles
26
22
  from .call_locus import call_locus
27
23
  from .non_daemonic_pool import NonDaemonicPool
24
+ from .output import output_json_report, output_tsv as output_tsv_fn, output_vcf
28
25
  from .utils import get_new_seed
29
26
 
30
27
  __all__ = [
@@ -48,7 +45,6 @@ def locus_worker(
48
45
  num_bootstrap: int,
49
46
  flank_size: int,
50
47
  sample_id: Optional[str],
51
- sex_chroms: Optional[str],
52
48
  realign: bool,
53
49
  hq: bool,
54
50
  # incorporate_snvs: bool,
@@ -57,6 +53,7 @@ def locus_worker(
57
53
  fractional: bool,
58
54
  respect_ref: bool,
59
55
  count_kmers: str,
56
+ consensus: bool,
60
57
  log_level: int,
61
58
  locus_queue: mp.Queue,
62
59
  is_single_processed: bool,
@@ -102,9 +99,9 @@ def locus_worker(
102
99
  if td is None: # Kill signal
103
100
  break
104
101
 
105
- t_idx, t, locus_seed = td
102
+ t_idx, t, n_alleles, locus_seed = td
106
103
  res = call_locus(
107
- t_idx, t, bfs, ref,
104
+ t_idx, t, n_alleles, bfs, ref,
108
105
  min_reads=min_reads,
109
106
  min_allele_reads=min_allele_reads,
110
107
  min_avg_phred=min_avg_phred,
@@ -113,7 +110,6 @@ def locus_worker(
113
110
  seed=locus_seed,
114
111
  logger_=lg,
115
112
  sample_id=sample_id,
116
- sex_chroms=sex_chroms,
117
113
  realign=realign,
118
114
  hq=hq,
119
115
  # incorporate_snvs=incorporate_snvs,
@@ -124,6 +120,7 @@ def locus_worker(
124
120
  fractional=fractional,
125
121
  respect_ref=respect_ref,
126
122
  count_kmers=count_kmers,
123
+ consensus=consensus,
127
124
  log_level=log_level,
128
125
  read_file_has_chr=read_file_has_chr,
129
126
  ref_file_has_chr=ref_file_has_chr,
@@ -182,10 +179,6 @@ def parse_loci_bed(loci_file: str):
182
179
  yield from (tuple(line.split("\t")) for line in (s.strip() for s in tf) if line and not line.startswith("#"))
183
180
 
184
181
 
185
- def _cn_to_str(cn: Union[int, float]) -> str:
186
- return f"{cn:.1f}" if isinstance(cn, float) else str(cn)
187
-
188
-
189
182
  def call_sample(
190
183
  read_files: tuple[str, ...],
191
184
  reference_file: str,
@@ -205,8 +198,10 @@ def call_sample(
205
198
  fractional: bool = False,
206
199
  respect_ref: bool = False,
207
200
  count_kmers: str = "none", # "none" | "peak" | "read"
201
+ consensus: bool = False,
208
202
  log_level: int = logging.WARNING,
209
203
  json_path: Optional[str] = None,
204
+ vcf_path: Optional[str] = None,
210
205
  indent_json: bool = False,
211
206
  output_tsv: bool = True,
212
207
  processes: int = 1,
@@ -253,11 +248,17 @@ def call_sample(
253
248
  # Keep track of all contigs we are processing to speed up downstream Mendelian inheritance analysis.
254
249
  contig_set: set[str] = set()
255
250
  for t_idx, t in enumerate(parse_loci_bed(loci_file), 1):
251
+ contig = t[0]
252
+
253
+ n_alleles: Optional[int] = get_n_alleles(2, sex_chroms, contig)
254
+ if n_alleles is None: # Sex chromosome, but we don't have a specified sex chromosome karyotype
255
+ continue # --> skip the locus
256
+
256
257
  # We use locus-specific random seeds for replicability, no matter which order
257
258
  # the loci are yanked out of the queue / how many processes we have.
258
259
  # Tuple of (1-indexed locus index, locus data, locus-specific random seed)
259
- locus_queue.put((t_idx, t, get_new_seed(rng)))
260
- contig_set.add(t[0])
260
+ locus_queue.put((t_idx, t, n_alleles, get_new_seed(rng)))
261
+ contig_set.add(contig)
261
262
  num_loci += 1
262
263
 
263
264
  # At the end of the queue, add a None value (* the # of processes).
@@ -275,7 +276,6 @@ def call_sample(
275
276
  "num_bootstrap": num_bootstrap,
276
277
  "flank_size": flank_size,
277
278
  "sample_id": sample_id_final,
278
- "sex_chroms": sex_chroms,
279
279
  "realign": realign,
280
280
  "hq": hq,
281
281
  # "incorporate_snvs": incorporate_snvs,
@@ -284,6 +284,7 @@ def call_sample(
284
284
  "fractional": fractional,
285
285
  "respect_ref": respect_ref,
286
286
  "count_kmers": count_kmers,
287
+ "consensus": consensus,
287
288
  "log_level": log_level,
288
289
  }
289
290
 
@@ -317,54 +318,19 @@ def call_sample(
317
318
  time_taken = datetime.now() - start_time
318
319
 
319
320
  if output_tsv:
320
- 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")
321
+ output_tsv_fn(results, has_snv_vcf=snv_vcf is not None)
343
322
 
344
323
  if json_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")
324
+ output_json_report(
325
+ sample_id_final,
326
+ job_params,
327
+ processes,
328
+ time_taken,
329
+ contig_set,
330
+ results,
331
+ json_path,
332
+ indent_json,
333
+ )
334
+
335
+ if vcf_path:
336
+ output_vcf(sample_id_final, reference_file, results, vcf_path)
@@ -0,0 +1,26 @@
1
+ import numpy as np
2
+ import pyfamsa
3
+ from scipy.stats import mode
4
+ from typing import Iterable
5
+
6
+ __all__ = [
7
+ "consensus_seq",
8
+ ]
9
+
10
+ GAP_ORD = ord("-")
11
+
12
+ aligner = pyfamsa.Aligner(threads=1, guide_tree="upgma", keep_duplicates=True)
13
+ join_str = "".join
14
+
15
+
16
+ def _not_gap(x: int) -> bool:
17
+ return x != GAP_ORD
18
+
19
+
20
+ def consensus_seq(seqs: Iterable[str]) -> str:
21
+ # Align all the allele tandem repeat sequences together into a multiple sequence alignment
22
+ msa = aligner.align(pyfamsa.Sequence(f"s{i}".encode("ascii"), seq.encode("ascii")) for i, seq in enumerate(seqs))
23
+ # Turn the MSA into a numpy matrix as setup for calculating the modal value at each position
24
+ msa_grid = np.array(list(map(lambda aseq: list(map(int, aseq.sequence)), msa)))
25
+ # Return a stringified, gapless version of the column-wise mode for the MSA
26
+ return join_str(map(chr, filter(_not_gap, getattr(mode(msa_grid), "mode", ()))))
@@ -0,0 +1,9 @@
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
+ ]
@@ -0,0 +1,46 @@
1
+ import sys
2
+ from datetime import timedelta
3
+ from typing import Optional
4
+
5
+ from strkit import __version__
6
+ from strkit.json import dumps_indented, json
7
+
8
+ __all__ = ["output_json_report"]
9
+
10
+
11
+ def output_json_report(
12
+ sample_id: Optional[str],
13
+ job_params: dict,
14
+ processes: int,
15
+ time_taken: timedelta,
16
+ contig_set: set[str],
17
+ results: tuple[dict, ...],
18
+ json_path: str,
19
+ indent_json: bool,
20
+ ):
21
+ json_report = {
22
+ "sample_id": sample_id,
23
+ "caller": {
24
+ "name": "strkit",
25
+ "version": __version__,
26
+ },
27
+ "parameters": {
28
+ **job_params,
29
+ "processes": processes,
30
+ },
31
+ "runtime": time_taken.total_seconds(),
32
+ "contigs": tuple(contig_set),
33
+ "results": results,
34
+ }
35
+
36
+ dfn = dumps_indented if indent_json else json.dumps
37
+ report_data = dfn(json_report)
38
+
39
+ if json_path == "stdout":
40
+ sys.stdout.buffer.write(report_data)
41
+ sys.stdout.write("\n")
42
+ sys.stdout.flush()
43
+ else:
44
+ with open(json_path, "wb") as jf:
45
+ jf.write(report_data)
46
+ jf.write(b"\n")
@@ -0,0 +1,34 @@
1
+ import sys
2
+ from typing import Union
3
+
4
+ __all__ = ["output_tsv"]
5
+
6
+
7
+ def _cn_to_str(cn: Union[int, float]) -> str:
8
+ return f"{cn:.1f}" if isinstance(cn, float) else str(cn)
9
+
10
+
11
+ def output_tsv(results: tuple[dict, ...], has_snv_vcf: bool):
12
+ for res in results:
13
+ has_call = res["call"] is not None
14
+ # n_peaks = res["peaks"]["modal_n"]
15
+
16
+ sys.stdout.write("\t".join((
17
+ res["contig"],
18
+ str(res["start"]),
19
+ str(res["end"]),
20
+ res["motif"],
21
+ _cn_to_str(res["ref_cn"]),
22
+ ",".join(map(_cn_to_str, sorted(r["cn"] for r in res["reads"].values()))) if res["reads"] else ".",
23
+ "|".join(map(_cn_to_str, res["call"])) if has_call else ".",
24
+ ("|".join("-".join(map(_cn_to_str, gc)) for gc in res["call_95_cis"]) if has_call else "."),
25
+ # *((res["assign_method"] if has_call else ".",) if incorporate_snvs else ()),
26
+ *((res["assign_method"] if has_call else ".",) if has_snv_vcf else ()),
27
+
28
+ # ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["means"][:n_peaks]))
29
+ # if has_call and n_peaks <= 2 else "."),
30
+ # ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["weights"][:n_peaks]))
31
+ # if has_call and n_peaks <= 2 else "."),
32
+ # ("|".join(map(lambda x: f"{x:.5f}", res["peaks"]["stdevs"][:n_peaks]))
33
+ # if has_call and n_peaks <= 2 else "."),
34
+ )) + "\n")