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/.gitignore +2 -0
- gaftools/__init__.py +3 -0
- gaftools/__main__.py +96 -0
- gaftools/_version.py +21 -0
- gaftools/args.py +21 -0
- gaftools/cli/__init__.py +26 -0
- gaftools/cli/find_path.py +65 -0
- gaftools/cli/gfa2rgfa.py +340 -0
- gaftools/cli/index.py +181 -0
- gaftools/cli/order_gfa.py +335 -0
- gaftools/cli/phase.py +135 -0
- gaftools/cli/realign.py +290 -0
- gaftools/cli/sort.py +242 -0
- gaftools/cli/stat.py +162 -0
- gaftools/cli/view.py +267 -0
- gaftools/conversion.py +239 -0
- gaftools/gaf.py +205 -0
- gaftools/gfa.py +912 -0
- gaftools/timer.py +60 -0
- gaftools/utils.py +148 -0
- gaftools-1.1.2.dist-info/METADATA +74 -0
- gaftools-1.1.2.dist-info/RECORD +26 -0
- gaftools-1.1.2.dist-info/WHEEL +5 -0
- gaftools-1.1.2.dist-info/entry_points.txt +2 -0
- gaftools-1.1.2.dist-info/licenses/LICENSE +21 -0
- gaftools-1.1.2.dist-info/top_level.txt +1 -0
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))
|