gaftools 1.1.2__py3-none-any.whl

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.
gaftools/cli/index.py ADDED
@@ -0,0 +1,181 @@
1
+ """
2
+ Index a GAF file for the view functionality.
3
+
4
+ This script creates an inverse look-up table where:
5
+ - key: the node information
6
+ - value: the offsets in the GAF file where the node is present
7
+ """
8
+
9
+ import re
10
+ import pickle
11
+ from pysam import libcbgzf
12
+ from collections import defaultdict
13
+ import logging
14
+
15
+ import gaftools.utils as utils
16
+ from gaftools.gaf import GAF
17
+ from gaftools.gfa import GFA
18
+ from gaftools.timer import StageTimer
19
+ from gaftools.cli import log_memory_usage
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def run(gaf_path, gfa_path, output=None):
25
+ timers = StageTimer()
26
+ if output is None:
27
+ output = gaf_path + ".gvi"
28
+
29
+ # Detecting if GAF has stable or unstable coordinate
30
+ gaf = GAF(gaf_path)
31
+ stable = None
32
+ # checking format in the first 10 lines.
33
+ for i, gaf_line in enumerate(gaf.read_file()):
34
+ if i == 10:
35
+ break
36
+ if i == 0:
37
+ stable = gaf_line.detect_path_format()
38
+ assert stable == gaf_line.detect_path_format()
39
+ gaf.close()
40
+
41
+ nodes = {}
42
+ reference = defaultdict(lambda: [])
43
+ ref_contig = []
44
+
45
+ gfa_file = GFA(graph_file=gfa_path, low_memory=True)
46
+ contigs = list(gfa_file.contigs.keys())
47
+ ref_contig = [contig for contig in gfa_file.contigs if gfa_file.contigs[contig] == 0]
48
+ nodes = gfa_file.nodes
49
+ if stable:
50
+ for contig in contigs:
51
+ path = gfa_file.get_path(contig)
52
+ for node in path:
53
+ reference[contig].append(gfa_file[node])
54
+ nodes = gfa_file.nodes
55
+ del gfa_file
56
+
57
+ if utils.is_file_gzipped(gaf_path):
58
+ gaf_file = libcbgzf.BGZFile(gaf_path, "rb")
59
+ else:
60
+ gaf_file = open(gaf_path, "rt")
61
+
62
+ out_dict = {}
63
+ offset = 0
64
+ while True:
65
+ offset = gaf_file.tell()
66
+ mapping = gaf_file.readline()
67
+ if not mapping:
68
+ break
69
+ try:
70
+ val = mapping.rstrip().split("\t")
71
+ except TypeError:
72
+ val = mapping.decode("utf-8").rstrip().split("\t")
73
+
74
+ if stable:
75
+ with timers("convert_coord"):
76
+ alignment = convert_coord(val, reference)
77
+ else:
78
+ alignment = list(re.split(">|<", val[5]))[1:]
79
+ for a in alignment:
80
+ try:
81
+ out_dict[
82
+ (
83
+ nodes[a].id,
84
+ nodes[a].tags["SN"][1],
85
+ int(nodes[a].tags["SO"][1]),
86
+ int(nodes[a].tags["SO"][1]) + int(nodes[a].tags["LN"][1]),
87
+ )
88
+ ].append(offset)
89
+ except KeyError:
90
+ out_dict[
91
+ (
92
+ nodes[a].id,
93
+ nodes[a].tags["SN"][1],
94
+ int(nodes[a].tags["SO"][1]),
95
+ int(nodes[a].tags["SO"][1]) + int(nodes[a].tags["LN"][1]),
96
+ )
97
+ ] = [offset]
98
+ out_dict["ref_contig"] = ref_contig
99
+
100
+ gaf_file.close()
101
+
102
+ with open(output, "wb") as handle:
103
+ with timers("write_file"):
104
+ pickle.dump(out_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
105
+
106
+ logger.info("\n== SUMMARY ==")
107
+ total_time = timers.total()
108
+ log_memory_usage()
109
+ logger.info(
110
+ "Time to convert coordinates: %9.2f s", timers.elapsed("convert_coord")
111
+ )
112
+ logger.info(
113
+ "Time to write the file: %9.2f s", timers.elapsed("write_file")
114
+ )
115
+ logger.info("Time spent on rest: %9.2f s", total_time - timers.sum())
116
+ logger.info("Total time: %9.2f s", total_time)
117
+
118
+
119
+ def convert_coord(line, ref):
120
+ unstable_coord = []
121
+ gaf_contigs = list(filter(None, re.split("(>)|(<)", line[5])))
122
+ for nd in gaf_contigs:
123
+ if nd == ">" or nd == "<":
124
+ continue
125
+ if ":" in nd and "-" in nd:
126
+ tmp = nd.rstrip().split(":")
127
+ query_contig_name = tmp[0]
128
+ (query_start, query_end) = tmp[1].rstrip().split("-")
129
+ else:
130
+ query_start = line[7]
131
+ query_end = line[8]
132
+ query_contig_name = nd
133
+
134
+ """Find the matching nodes from the reference genome here"""
135
+ start, end = utils.search_intervals(
136
+ ref[query_contig_name], int(query_start), int(query_end), 0, len(ref[query_contig_name])
137
+ )
138
+
139
+ for node in ref[query_contig_name][start : end + 1]:
140
+ cases = -1
141
+ if (
142
+ int(node.tags["SO"][1])
143
+ <= int(query_start)
144
+ < int(node.tags["SO"][1]) + int(node.tags["LN"][1])
145
+ ):
146
+ cases = 1
147
+ elif (
148
+ int(node.tags["SO"][1])
149
+ < int(query_end)
150
+ <= int(node.tags["SO"][1]) + int(node.tags["LN"][1])
151
+ ):
152
+ cases = 2
153
+ elif (
154
+ int(query_start)
155
+ < int(node.tags["SO"][1])
156
+ < int(node.tags["SO"][1]) + int(node.tags["LN"][1])
157
+ < int(query_end)
158
+ ):
159
+ cases = 3
160
+
161
+ if cases != -1:
162
+ unstable_coord.append(node.id)
163
+
164
+ return unstable_coord
165
+
166
+
167
+ # fmt: off
168
+ def add_arguments(parser):
169
+ arg = parser.add_argument
170
+ # Positional arguments
171
+ arg('gaf_path', metavar='GAF',
172
+ help='GAF file (can be bgzip-compressed)')
173
+ arg('gfa_path', metavar='rGFA',
174
+ help='rGFA file (can be bgzip-compressed)')
175
+ arg('-o', '--output', default=None,
176
+ help='Output GAF View Index (GVI) file. Default: <GAF file>.gvi')
177
+ # fmt: on
178
+
179
+
180
+ def main(args):
181
+ run(**vars(args))
@@ -0,0 +1,335 @@
1
+ """
2
+ Order bubbles in the GFA by adding BO and NO tags.
3
+
4
+ The BO (Bubble Order) tags order bubbles in the GFA.
5
+ The NO (Node Order) tags order the nodes in a bubble (in a lexicographic order).
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ import logging
11
+ import time
12
+ from collections import defaultdict
13
+ from gaftools.gfa import GFA
14
+ from gaftools.utils import DEFAULT_CHROMOSOME
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def run_order_gfa(
20
+ gfa_filename,
21
+ outdir,
22
+ by_chrom,
23
+ chromosome_order=None,
24
+ with_sequence=False,
25
+ ):
26
+ if chromosome_order is not None:
27
+ chromosome_order = chromosome_order.split(sep=",")
28
+
29
+ if not os.path.isdir(outdir):
30
+ logging.warning(f"The directory {outdir} does not exist, creating one")
31
+ try:
32
+ os.makedirs(outdir)
33
+ except PermissionError:
34
+ logging.error(f"were not able to create directory {outdir}. Permission Denied")
35
+ sys.exit()
36
+ except FileNotFoundError:
37
+ logging.error(f"were not able to create directory {outdir}, File not found")
38
+ sys.exit()
39
+ except OSError:
40
+ logging.error(f"were not able to create directory {outdir}, OSError")
41
+ sys.exit()
42
+
43
+ logger.info(f"Reading {gfa_filename}")
44
+ if with_sequence:
45
+ graph = GFA(gfa_filename, low_memory=False)
46
+ else:
47
+ graph = GFA(gfa_filename, low_memory=True)
48
+ # the __str__ functions print the number of nodes and edges
49
+ logger.info("The graph has:")
50
+ logger.info(graph.__str__())
51
+
52
+ components = graph.all_components()
53
+ logger.info(f"Connected components: {len(components)}")
54
+
55
+ # name_comps checks the most frequent SN tag for the node in the component
56
+ # and returns a dict of chromosome_name: {nodes...}
57
+ components = name_comps(graph, components)
58
+ # if not chromosome_order is None: # user gave a list
59
+ if chromosome_order != [""]: # user gave a list
60
+ for c in chromosome_order:
61
+ if c not in set(components.keys()):
62
+ logger.error(
63
+ f"The chromosome name provided {c} did not match with a component in the graph"
64
+ )
65
+ logger.error(f" What was Found: {','.join(sorted(components.keys()))}")
66
+ sys.exit(1)
67
+
68
+ else: # user did not give a
69
+ try:
70
+ assert set(components.keys()) == set(DEFAULT_CHROMOSOME)
71
+ except AssertionError:
72
+ logger.error(
73
+ f"chromosome order was not provided, so the default was taken, but the default did not match"
74
+ f" what was found in the graph, which is {','.join(sorted(components.keys()))}"
75
+ )
76
+ sys.exit(1)
77
+ chromosome_order = DEFAULT_CHROMOSOME
78
+ # running index for the bubble index (BO) already used
79
+ bo = 0
80
+ total_bubbles = 0
81
+ # todo output final GFA with all the chromosomes ordered
82
+ out_gfa = []
83
+ out_csv = []
84
+ for chromosome in chromosome_order:
85
+ logger.info("Processing %s", chromosome)
86
+ component_nodes = components[chromosome]
87
+ # Initialize files
88
+ # f_gfa = open(outdir+'/'+gfa_filename.split("/")[-1][:-4]+'-'+chromosome+'.gfa', 'w')
89
+
90
+ scaffold_nodes, inside_nodes, node_order, bo, bubble_count = decompose_and_order(
91
+ graph, component_nodes, chromosome, bo
92
+ )
93
+
94
+ # skip a chromosome if something went wrong
95
+ if scaffold_nodes:
96
+ f_gfa = (
97
+ outdir
98
+ + os.sep
99
+ + gfa_filename.split(os.sep)[-1].split(".")[0]
100
+ + "-"
101
+ + chromosome
102
+ + ".gfa"
103
+ )
104
+ out_gfa.append(f_gfa)
105
+ csv_file = (
106
+ outdir + os.sep + gfa_filename.split(os.sep)[-1][:-4] + "-" + chromosome + ".csv"
107
+ )
108
+ out_csv.append(csv_file)
109
+ f_colors = open(csv_file, "w")
110
+ f_colors.write("Name,Color,SN,SO,BO,NO\n")
111
+ total_bubbles += bubble_count
112
+ for node_name in sorted(component_nodes):
113
+ node = graph.nodes[node_name]
114
+ bo_tag, no_tag = node_order[node_name]
115
+
116
+ node.tags["BO"] = ("i", bo_tag)
117
+ node.tags["NO"] = ("i", no_tag)
118
+
119
+ if node_name in scaffold_nodes:
120
+ color = "orange"
121
+ elif node_name in inside_nodes:
122
+ color = "blue"
123
+ else:
124
+ color = "gray"
125
+
126
+ if "SN" in node.tags:
127
+ sn_tag = node.tags["SN"][1]
128
+ else:
129
+ sn_tag = "NA"
130
+ if "SO" in node.tags:
131
+ so_tag = node.tags["SO"][1]
132
+ else:
133
+ so_tag = "NA"
134
+ f_colors.write(
135
+ "{},{},{},{},{},{}\n".format(node_name, color, sn_tag, so_tag, bo_tag, no_tag)
136
+ )
137
+
138
+ graph.write_gfa(
139
+ set_of_nodes=component_nodes,
140
+ output_file=f_gfa,
141
+ append=False,
142
+ order_bo=True,
143
+ )
144
+
145
+ f_colors.close()
146
+
147
+ else:
148
+ logger.warning(f"Chromosome {chromosome} was skipped")
149
+ if not by_chrom:
150
+ final_gfa = (
151
+ outdir + os.sep + gfa_filename.split(os.sep)[-1].split(".")[0] + "-complete" + ".gfa"
152
+ )
153
+ final_csv = (
154
+ outdir + os.sep + gfa_filename.split(os.sep)[-1].split(".")[0] + "-complete" + ".csv"
155
+ )
156
+ with open(final_gfa, "w") as outfile:
157
+ # outputting all the S lines first
158
+ for f in out_gfa:
159
+ with open(f, "r") as infile:
160
+ for l in infile:
161
+ if l.startswith("S"):
162
+ outfile.write(l)
163
+ # outputting all the S lines
164
+ for f in out_gfa:
165
+ with open(f, "r") as infile:
166
+ for l in infile:
167
+ if l.startswith("L"):
168
+ outfile.write(l)
169
+ for f in out_gfa:
170
+ os.remove(f)
171
+
172
+ with open(final_csv, "w") as outfile:
173
+ for f in out_csv:
174
+ with open(f, "r") as infile:
175
+ for l in infile:
176
+ outfile.write(l)
177
+ for f in out_csv:
178
+ os.remove(f)
179
+
180
+ logger.info("Total bubbles: %d", total_bubbles)
181
+
182
+
183
+ def decompose_and_order(graph, component, component_name, bo_start=0):
184
+ """
185
+ This function takes the graph and a component
186
+ detects all biconnected components, order the scaffold nodes and starts ordering
187
+ """
188
+ logger.info(f" Input component: {len(component)} nodes")
189
+ logger.info(f" Finding Biconnected Components of the component {component_name}")
190
+ if len(component) == 1:
191
+ node = list(component)[0]
192
+ return component, set(), {node: (bo_start, 0)}, bo_start + 1, 0
193
+
194
+ new_graph = graph.graph_from_comp(component)
195
+ start = time.perf_counter()
196
+ all_biccs, artic_points = new_graph.biccs()
197
+ logger.info(
198
+ f" It took {time.perf_counter() - start} seconds to find the Biconnected Components"
199
+ )
200
+ bubbles = []
201
+ scaffold_graph = GFA()
202
+ scaffold_node_types = dict()
203
+ for n in artic_points:
204
+ scaffold_graph.add_node(n)
205
+ scaffold_node_types[n] = "s"
206
+ inside_nodes = set()
207
+
208
+ for bc in all_biccs:
209
+ # the components still have the articulation nodes in, need to be removed
210
+ bc_inside_nodes = bc.difference(artic_points)
211
+ bc_end_nodes = bc.intersection(artic_points)
212
+ inside_nodes.update(bc_inside_nodes)
213
+
214
+ if len(bc_inside_nodes) == 0:
215
+ assert len(bc_end_nodes) == 2
216
+ node1, node2 = tuple(bc_end_nodes)
217
+ scaffold_graph.add_edge(node1, "+", node2, "+", 0)
218
+
219
+ else:
220
+ bubble_index = len(bubbles)
221
+ bubbles.append(bc_inside_nodes)
222
+ scaffold_graph.add_node(str(bubble_index))
223
+ scaffold_node_types[str(bubble_index)] = "b"
224
+ for end_node in bc_end_nodes:
225
+ scaffold_graph.add_edge(str(bubble_index), "+", end_node, "+", 0)
226
+
227
+ logger.info(f" Bubbles: {len(bubbles)}")
228
+ logger.info(f" Scaffold graph: {len(scaffold_graph)} nodes")
229
+
230
+ # Find start/end points of the line by looking for nodes with degree 1
231
+ degree_one = [x.id for x in scaffold_graph.nodes.values() if len(x.neighbors()) == 1]
232
+ degree_two = [x.id for x in scaffold_graph.nodes.values() if len(x.neighbors()) == 2]
233
+
234
+ try:
235
+ assert len(degree_one) == 2
236
+ except AssertionError:
237
+ logger.warning(
238
+ f"Error: In Chromosome {component_name}, we found more or less than two nodes with degree 1. Skipping this chromosome"
239
+ )
240
+ # hacky but for now maybe ok
241
+ return None, None, None, None, None
242
+
243
+ try:
244
+ assert len(degree_two) == len(scaffold_graph) - 2
245
+ except AssertionError:
246
+ logger.warning(
247
+ f"Error: In Chromosome {component_name}, the number of nodes with degree 2 did not mach the expected number"
248
+ )
249
+ return None, None, None, None, None
250
+
251
+ # the scaffold graph should be a line graph here
252
+ traversal = scaffold_graph.dfs(degree_one[0])
253
+ traversal_scaffold_only = [
254
+ node_name for node_name in traversal if scaffold_node_types[node_name] == "s"
255
+ ]
256
+ # check that all scaffold nodes carry the same sequence name (SN), i.e. all came for the linear reference
257
+ assert len(set(new_graph[n].tags["SN"] for n in traversal_scaffold_only)) == 1
258
+ # I save tags as key:(type, value), so "SO":(i, '123')
259
+ coordinates = list(int(new_graph[n].tags["SO"][1]) for n in traversal_scaffold_only)
260
+
261
+ # make sure that the traversal is in ascending order
262
+ if coordinates[0] > coordinates[-1]:
263
+ traversal.reverse()
264
+ traversal_scaffold_only.reverse()
265
+ coordinates.reverse()
266
+ for i in range(len(coordinates) - 1):
267
+ assert coordinates[i] < coordinates[i + 1]
268
+ # compute dictionary mapping each node name to the corresponding "bubble order" and "node order" (BO,NO)
269
+ node_order = dict()
270
+ bo = bo_start
271
+ for node in traversal:
272
+ node_type = scaffold_node_types[node]
273
+ if node_type == "s":
274
+ node_order[node] = (bo, 0)
275
+ elif node_type == "b":
276
+ for i, n in enumerate(sorted(bubbles[int(node)])):
277
+ node_order[n] = (bo, i + 1)
278
+ else:
279
+ assert False
280
+ bo += 1
281
+ return artic_points, inside_nodes, node_order, bo, len(bubbles)
282
+
283
+
284
+ def count_sn(graph, comp):
285
+ """
286
+ counts which SN tag is the majority in that chromosome to name the chromosome accordingly
287
+ """
288
+ counts = defaultdict(int)
289
+ for n in comp:
290
+ if "SN" not in graph[n].tags:
291
+ continue
292
+ counts[graph[n].tags["SN"][1]] += 1
293
+ return counts
294
+
295
+
296
+ def name_comps(graph, components):
297
+ """
298
+ the graph is GFA object and components is a list of sets of the node ids of each component
299
+ for each component we take a majority vote of the SN tage and name the component accordingly
300
+ """
301
+ named_comps = dict()
302
+ current_tag = ""
303
+ for comp in components:
304
+ counts = count_sn(graph, comp)
305
+ most_freq = 0
306
+ for tag, count in counts.items():
307
+ if most_freq <= count:
308
+ current_tag, most_freq = tag, count
309
+ if current_tag == "":
310
+ raise ValueError(
311
+ "Were not able to assign a chromosome to component, SN tags could be missing"
312
+ )
313
+ named_comps[current_tag] = comp
314
+ return named_comps
315
+
316
+
317
+ # fmt: off
318
+ def add_arguments(parser):
319
+ arg = parser.add_argument
320
+ arg("--chromosome_order", default="",
321
+ help="Order in which to arrange chromosomes in terms of BO sorting. "
322
+ "Expecting comma-separated list. Default: chr1,...,chr22,chrX,chrY,chrM")
323
+ arg("--with-sequence", default=False, action="store_true",
324
+ help="Retain sequences in output (default is to strip sequences)")
325
+ arg("gfa_filename", metavar="GRAPH",
326
+ help="Input rGFA file")
327
+ arg("--outdir", default="./out",
328
+ help='Output Directory to store all the GFA and CSV files. Default location is a "out" folder from the directory of execution.')
329
+ arg("--by-chrom", default=False, action="store_true",
330
+ help="Outputs each chromosome as a separate GFA, otherwise, all chromosomes in one GFA file")
331
+ # fmt: on
332
+
333
+
334
+ def main(args):
335
+ run_order_gfa(**vars(args))
gaftools/cli/phase.py ADDED
@@ -0,0 +1,135 @@
1
+ """
2
+ Add phasing information to the GAF file from a haplotag TSV file.
3
+
4
+ The script uses the TSV file containing the haplotag information generated from WhatsHap's haplotag command.
5
+ The H1 and H2 labels for each read are then added to the reads in the GAF file.
6
+ """
7
+
8
+ import logging
9
+
10
+ from gaftools.utils import FileWriter
11
+ from gaftools.cli import log_memory_usage
12
+ from gaftools.timer import StageTimer
13
+ from gaftools.gaf import GAF
14
+
15
+
16
+ class Node:
17
+ def __init__(self, chr_name, haplotype, phase_set):
18
+ self.chr_name = chr_name
19
+ self.phase_set = phase_set
20
+ self.haplotype = haplotype
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+ timers = StageTimer()
25
+
26
+
27
+ def run(gaf_file, tsv_file, output=None):
28
+ add_phase_info(gaf_file, tsv_file, output)
29
+ logger.info("\n== SUMMARY ==")
30
+ total_time = timers.total()
31
+ log_memory_usage()
32
+ logger.info("Time to read TSV %9.2f s", timers.elapsed("read_tsv"))
33
+ logger.info(
34
+ "Time to read and write GAF %9.2f s", timers.elapsed("read_write_gaf")
35
+ )
36
+ logger.info("Time spent on rest: %9.2f s", total_time - timers.sum())
37
+ logger.info("Total time: %9.2f s", total_time)
38
+
39
+
40
+ def add_phase_info(gaf_path, tsv_path, out_path):
41
+ """This function adds phasing information to the gaf file using .tsv file of the WhatsHap
42
+ Haplotag... The information is added as two tags ("ps:Z" and "ht:Z") before the cigar string
43
+ """
44
+
45
+ logger.info("INFO: Adding phasing information...")
46
+
47
+ tsv_file = open(tsv_path, "r")
48
+
49
+ phase = {}
50
+ with timers("read_tsv"):
51
+ for line in tsv_file:
52
+ line_elements = line.rstrip().split("\t")
53
+
54
+ if line_elements[0] not in phase:
55
+ tmp = Node(line_elements[3], line_elements[1], line_elements[2])
56
+ phase[line_elements[0]] = tmp
57
+
58
+ gaf_out = FileWriter(out_path)
59
+
60
+ line_count = 0
61
+ missing_in_tsv = 0
62
+ phased = 0
63
+ gaf_file = GAF(gaf_path)
64
+ with timers("read_write_gaf"):
65
+ for gaf_line in gaf_file.read_file():
66
+ if line_count != 0:
67
+ gaf_out.write("\n")
68
+
69
+ line_count += 1
70
+
71
+ gaf_out.write(
72
+ "%s\t%s\t%s\t%s\t+\t%s\t%d\t%d\t%d\t%d\t%d\t%d"
73
+ % (
74
+ gaf_line.query_name,
75
+ gaf_line.query_length,
76
+ gaf_line.query_start,
77
+ gaf_line.query_end,
78
+ gaf_line.path,
79
+ gaf_line.path_length,
80
+ gaf_line.path_start,
81
+ gaf_line.path_end,
82
+ gaf_line.residue_matches,
83
+ gaf_line.alignment_block_length,
84
+ gaf_line.mapping_quality,
85
+ )
86
+ )
87
+
88
+ in_tsv = True
89
+ if gaf_line.query_name not in phase:
90
+ missing_in_tsv += 1
91
+ in_tsv = False
92
+
93
+ if in_tsv and phase[gaf_line.query_name].haplotype != "none":
94
+ gaf_out.write(
95
+ "\tps:Z:%s-%s\tht:Z:%s\t"
96
+ % (
97
+ phase[gaf_line.query_name].chr_name,
98
+ phase[gaf_line.query_name].phase_set,
99
+ phase[gaf_line.query_name].haplotype,
100
+ )
101
+ )
102
+ phased += 1
103
+ else:
104
+ gaf_out.write("\tps:Z:none\tht:Z:none")
105
+
106
+ for k in gaf_line.tags.keys():
107
+ gaf_out.write("\t%s:%s" % (k, gaf_line.tags[k]))
108
+
109
+ gaf_out.write("\t%s" % gaf_line.cigar)
110
+
111
+ logger.info(
112
+ "INFO: Added phasing info (ps:Z and ht:Z) for %d reads out of %d GAF lines - (%d reads are missing in .tsv)"
113
+ % (phased, line_count, missing_in_tsv)
114
+ )
115
+ gaf_file.close()
116
+ tsv_file.close()
117
+ gaf_out.close()
118
+
119
+
120
+ # fmt: off
121
+ def add_arguments(parser):
122
+ arg = parser.add_argument
123
+
124
+ # Positional arguments
125
+ arg('gaf_file', metavar='GAF',
126
+ help='GAF file (can be bgzip-compressed)')
127
+ arg('tsv_file', metavar='TSV',
128
+ help='WhatsHap haplotag TSV file. Refer to https://whatshap.readthedocs.io/en/latest/guide.html#whatshap-haplotag')
129
+ arg('-o', '--output', default=None,
130
+ help='Output GAF file (bgzipped if the file ends with .gz). If omitted, output is directed to stdout.')
131
+ # fmt: on
132
+
133
+
134
+ def main(args):
135
+ run(**vars(args))