gaftools 1.0.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.
- gaftools-1.0.0/.attic/.vcf_based_sorting_scripts/add-bubble-info.py +235 -0
- gaftools-1.0.0/.attic/.vcf_based_sorting_scripts/bubble-sort.py +249 -0
- gaftools-1.0.0/.attic/bicc_networkx.py +165 -0
- gaftools-1.0.0/.attic/find_bubbles.py +121 -0
- gaftools-1.0.0/.attic/graph.py +131 -0
- gaftools-1.0.0/.attic/sort.py +293 -0
- gaftools-1.0.0/.github/workflows/ci.yml +90 -0
- gaftools-1.0.0/.gitignore +22 -0
- gaftools-1.0.0/.pre-commit-config.yaml +18 -0
- gaftools-1.0.0/.readthedocs.yaml +22 -0
- gaftools-1.0.0/CHANGES.rst +7 -0
- gaftools-1.0.0/LICENSE +21 -0
- gaftools-1.0.0/MANIFEST.in +11 -0
- gaftools-1.0.0/PKG-INFO +64 -0
- gaftools-1.0.0/README.md +5 -0
- gaftools-1.0.0/docs/Makefile +81 -0
- gaftools-1.0.0/docs/README.rst +24 -0
- gaftools-1.0.0/docs/_static/bo_tags.png +0 -0
- gaftools-1.0.0/docs/_static/no_tags.png +0 -0
- gaftools-1.0.0/docs/changes.rst +3 -0
- gaftools-1.0.0/docs/conf.py +49 -0
- gaftools-1.0.0/docs/develop.rst +138 -0
- gaftools-1.0.0/docs/guide.rst +305 -0
- gaftools-1.0.0/docs/index.rst +12 -0
- gaftools-1.0.0/docs/installation.rst +55 -0
- gaftools-1.0.0/docs/requirements.txt +5 -0
- gaftools-1.0.0/gaftools/.gitignore +2 -0
- gaftools-1.0.0/gaftools/__init__.py +3 -0
- gaftools-1.0.0/gaftools/__main__.py +96 -0
- gaftools-1.0.0/gaftools/_version.py +16 -0
- gaftools-1.0.0/gaftools/args.py +21 -0
- gaftools-1.0.0/gaftools/cli/__init__.py +26 -0
- gaftools-1.0.0/gaftools/cli/index.py +182 -0
- gaftools-1.0.0/gaftools/cli/order_gfa.py +350 -0
- gaftools-1.0.0/gaftools/cli/phase.py +132 -0
- gaftools-1.0.0/gaftools/cli/realign.py +292 -0
- gaftools-1.0.0/gaftools/cli/sort.py +260 -0
- gaftools-1.0.0/gaftools/cli/stat.py +181 -0
- gaftools-1.0.0/gaftools/cli/view.py +251 -0
- gaftools-1.0.0/gaftools/conversion.py +239 -0
- gaftools-1.0.0/gaftools/gaf.py +205 -0
- gaftools-1.0.0/gaftools/gfa.py +876 -0
- gaftools-1.0.0/gaftools/timer.py +60 -0
- gaftools-1.0.0/gaftools/utils.py +90 -0
- gaftools-1.0.0/gaftools.egg-info/PKG-INFO +64 -0
- gaftools-1.0.0/gaftools.egg-info/SOURCES.txt +94 -0
- gaftools-1.0.0/gaftools.egg-info/dependency_links.txt +1 -0
- gaftools-1.0.0/gaftools.egg-info/entry_points.txt +2 -0
- gaftools-1.0.0/gaftools.egg-info/requires.txt +19 -0
- gaftools-1.0.0/gaftools.egg-info/top_level.txt +1 -0
- gaftools-1.0.0/pyproject.toml +83 -0
- gaftools-1.0.0/requirements.txt +2 -0
- gaftools-1.0.0/scripts/split_graph.sh +82 -0
- gaftools-1.0.0/setup.cfg +4 -0
- gaftools-1.0.0/setup.py +16 -0
- gaftools-1.0.0/tests/data/alignments-graphaligner.gaf +2 -0
- gaftools-1.0.0/tests/data/alignments-graphaligner.gaf.gz +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck-only-s2.gaf +2 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck-only-s464827.gaf +1 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf +3 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf.gvi +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf.gz +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf.gz.gvi +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-stable.gaf +2 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck-only-s2.gaf +2 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck-only-s464827.gaf +1 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf +3 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf.gvi +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf.gz +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf.gz.gvi +0 -0
- gaftools-1.0.0/tests/data/alignments-minigraph-unstable.gaf +2 -0
- gaftools-1.0.0/tests/data/alignments-more-graphaligner.gaf +8 -0
- gaftools-1.0.0/tests/data/alignments-more-graphaligner.gaf.gz +0 -0
- gaftools-1.0.0/tests/data/large-graph-chr1.gfa.gz +0 -0
- gaftools-1.0.0/tests/data/reads-conversioncheck.fa +6 -0
- gaftools-1.0.0/tests/data/reads-more.fa +16 -0
- gaftools-1.0.0/tests/data/reads.fa +4 -0
- gaftools-1.0.0/tests/data/reads.fa.fai +2 -0
- gaftools-1.0.0/tests/data/smallgraph-noseq.gfa +32 -0
- gaftools-1.0.0/tests/data/smallgraph-ordered.gfa +32 -0
- gaftools-1.0.0/tests/data/smallgraph.gfa +32 -0
- gaftools-1.0.0/tests/data/smallgraph_withN.gfa +5 -0
- gaftools-1.0.0/tests/data/test_GFA_class.gfa +8 -0
- gaftools-1.0.0/tests/data/test_GFA_class_wrong_graph.gfa +8 -0
- gaftools-1.0.0/tests/test_extract_path.py +36 -0
- gaftools-1.0.0/tests/test_gaf.py +44 -0
- gaftools-1.0.0/tests/test_gaf_gzipped.py +50 -0
- gaftools-1.0.0/tests/test_gfa_class.py +136 -0
- gaftools-1.0.0/tests/test_index.py +65 -0
- gaftools-1.0.0/tests/test_parsegaf_tags.py +22 -0
- gaftools-1.0.0/tests/test_run_ordergfa.py +28 -0
- gaftools-1.0.0/tests/test_run_realign.py +53 -0
- gaftools-1.0.0/tests/test_run_stat.py +63 -0
- gaftools-1.0.0/tests/test_sort.py +31 -0
- gaftools-1.0.0/tests/test_view.py +462 -0
- gaftools-1.0.0/tox.ini +48 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
import logging
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
import gzip
|
|
6
|
+
import re
|
|
7
|
+
from pysam import libcbgzf
|
|
8
|
+
import resource
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(debug):
|
|
14
|
+
handler = logging.StreamHandler()
|
|
15
|
+
root = logging.getLogger()
|
|
16
|
+
root.addHandler(handler)
|
|
17
|
+
root.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
parser = argparse.ArgumentParser()
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--gfa",
|
|
24
|
+
required=True,
|
|
25
|
+
help="GFA file which was used to create the VCF in the GFA-to-VCF pipeline",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--vcf", required=True, help="Input VCF file (output of the GFA-to-VCF conversion pipeline)"
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("--output", default=None, help="Output GFA File path (Default: sys.stdout)")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--bgzip",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Flag to bgzip the output. Can only be given with --output.",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--debug", action="store_true", default=False, help="Print debug messages")
|
|
37
|
+
options = parser.parse_args()
|
|
38
|
+
setup_logging(options.debug)
|
|
39
|
+
|
|
40
|
+
# Checks for argument parsing
|
|
41
|
+
if options.bgzip and not options.output:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"--bgzip flag has been specified but not output path has been defined. Please define the output path."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
add_bubble_tags(options)
|
|
47
|
+
|
|
48
|
+
memory_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
49
|
+
logger.info("\n##### Memory Information #####")
|
|
50
|
+
logger.info("Maximum memory usage: %.3f GB", memory_kb / 1e6)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def add_bubble_tags(options):
|
|
54
|
+
"""
|
|
55
|
+
Adding the bubble tags to the nodes in the GFA file.
|
|
56
|
+
Sort Keys will be added to the GFA S-lines. For this bubble tag, only the top level bubbles (LV=0) are considered.
|
|
57
|
+
|
|
58
|
+
Two tags will be added:
|
|
59
|
+
1. BO:i:<integer> - This tag gives the bubble order. Odd tag means that the node is a scaffold node (at the start or end of the bubble). Even tag means that the node is inside a bubble.
|
|
60
|
+
2. NO:i:<integer> - This tag gives the node order. For nodes with the same BO tag, they are distinguished based on this NO tag to order each node uniquely.
|
|
61
|
+
|
|
62
|
+
Properties of BO tag:
|
|
63
|
+
1. BO tag of the form 2n indicates that node is present in the nth bubble defined in the VCF. Consequently, BO tags 2n-1 and 2n+1 are the scaffold nodes for the nth bubble of the VCF.
|
|
64
|
+
2. This tag has a proper ordering based on the genome coordinate system (Node with BO tag n+1 can be confidently put after Node with BO tag n). There might be some cases where the bubble definition is violated and this order cannot be established. Nodes can be part of two bubbles. Such inconsistent nodes are given a 0 tag.
|
|
65
|
+
|
|
66
|
+
Properties of NO tag:
|
|
67
|
+
1. This does not give ordering based on the genome coordinate system.
|
|
68
|
+
2. The ordering is based on the appearance of the node in the Allele Traversal INFO in the VCF file. Sequentially, the non scaffold nodes will be provided a tag starting from 1.
|
|
69
|
+
3. Though this is not ordered based on genome coordinate, this will be a deterministic ordering of some sort.
|
|
70
|
+
4. NO tag will be 0 for scaffold nodes and inconsistent nodes (nodes present in two top level variants)
|
|
71
|
+
|
|
72
|
+
General Notes:
|
|
73
|
+
1. So inconsistent nodes will have 0 for both BO and NO tag.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if options.output is None:
|
|
77
|
+
writer = sys.stdout
|
|
78
|
+
else:
|
|
79
|
+
if options.bgzip:
|
|
80
|
+
writer = libcbgzf.BGZFile(options.output, "wb")
|
|
81
|
+
else:
|
|
82
|
+
writer = open(options.output, "w")
|
|
83
|
+
node = defaultdict(lambda: [-1, -1])
|
|
84
|
+
read_vcf(options.vcf, node)
|
|
85
|
+
modify_gfa(options.gfa, node, writer)
|
|
86
|
+
writer.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def write_to_file(line, writer):
|
|
90
|
+
try:
|
|
91
|
+
writer.write(line)
|
|
92
|
+
except TypeError:
|
|
93
|
+
writer.write(str.encode(line))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def modify_gfa(gfa, node, writer):
|
|
97
|
+
logger.info("\n##### Parsing GFA file and adding sort keys #####")
|
|
98
|
+
if is_file_gzipped(gfa):
|
|
99
|
+
reader = gzip.open(gfa, "rt")
|
|
100
|
+
else:
|
|
101
|
+
reader = open(gfa, "r")
|
|
102
|
+
|
|
103
|
+
S_lines_not_in_vcf = 0
|
|
104
|
+
line_count = defaultdict(lambda: 0)
|
|
105
|
+
while True:
|
|
106
|
+
line = reader.readline()
|
|
107
|
+
if not line:
|
|
108
|
+
break
|
|
109
|
+
line_count[line[0]] += 1
|
|
110
|
+
if line[0] != "S":
|
|
111
|
+
write_to_file(line, writer)
|
|
112
|
+
continue
|
|
113
|
+
fields = line.split("\t")
|
|
114
|
+
n = fields[1]
|
|
115
|
+
if node[n] == [-1, -1]:
|
|
116
|
+
S_lines_not_in_vcf += 1
|
|
117
|
+
write_to_file(line, writer)
|
|
118
|
+
continue
|
|
119
|
+
bo_tag = "BO:i:"
|
|
120
|
+
no_tag = "NO:i:"
|
|
121
|
+
assert node[n][0] != -1 and node[n][1] != -1, "Node %s has -1,-1 tag" % (n)
|
|
122
|
+
bo_tag += str(node[n][0])
|
|
123
|
+
no_tag += str(node[n][1])
|
|
124
|
+
new_line = line.rstrip() + "\t" + bo_tag + "\t" + no_tag + "\n"
|
|
125
|
+
write_to_file(new_line, writer)
|
|
126
|
+
|
|
127
|
+
for line_type, value in line_count.items():
|
|
128
|
+
logger.info("Number of %s lines: %d" % (line_type, value))
|
|
129
|
+
logger.info("Number of S lines not present in VCF: %d" % (S_lines_not_in_vcf))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def read_vcf(vcf, node):
|
|
133
|
+
logger.info("\n##### Parsing VCF file and reading bubble information #####")
|
|
134
|
+
if is_file_gzipped(vcf):
|
|
135
|
+
reader = gzip.open(vcf, "rt")
|
|
136
|
+
else:
|
|
137
|
+
reader = open(vcf, "r")
|
|
138
|
+
|
|
139
|
+
total_variants = 0
|
|
140
|
+
inconsistent_bubble_starts = 0
|
|
141
|
+
inconsistent_nodes = defaultdict(lambda: 0)
|
|
142
|
+
bo = 1
|
|
143
|
+
while True:
|
|
144
|
+
line = reader.readline()
|
|
145
|
+
if not line:
|
|
146
|
+
break
|
|
147
|
+
if line[0] == "#":
|
|
148
|
+
continue
|
|
149
|
+
fields = line.split("\t")
|
|
150
|
+
info = fields[7].split(";")
|
|
151
|
+
lv = None
|
|
152
|
+
lv = int([x[3:] for x in info if x[0:2] == "LV"][0])
|
|
153
|
+
assert lv is not None, (
|
|
154
|
+
"Variant at chromosome %s position %s does not have a LV field. Need LV info to process"
|
|
155
|
+
% (fields[0], fields[1])
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Not considering any bubble at LV>0
|
|
159
|
+
if lv != 0:
|
|
160
|
+
continue
|
|
161
|
+
total_variants += 1
|
|
162
|
+
|
|
163
|
+
at = None
|
|
164
|
+
at = [x[3:].split(",") for x in info if x[0:2] == "AT"][0]
|
|
165
|
+
assert at is not None, (
|
|
166
|
+
"Variant at chromosome %s position %s does not have a AT field. Need AT info to process"
|
|
167
|
+
% (fields[0], fields[1])
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
flanks = list(filter(None, re.split("(>)|(<)", fields[2])))
|
|
171
|
+
assert flanks[0] == flanks[2], "Bad orientation for the bubble."
|
|
172
|
+
if flanks[0] == "<":
|
|
173
|
+
flanks = [">", flanks[-1], ">", flanks[1]]
|
|
174
|
+
assert len(flanks) == 4
|
|
175
|
+
|
|
176
|
+
if bo == 1:
|
|
177
|
+
node[flanks[1]][0] = (
|
|
178
|
+
bo # Since there is no previous bubble, this value has to be assigned
|
|
179
|
+
)
|
|
180
|
+
node[flanks[1]][1] = 0
|
|
181
|
+
else:
|
|
182
|
+
if (
|
|
183
|
+
node[flanks[1]][0] != bo
|
|
184
|
+
): # By definition, the end node of bubble 1 should be start node of bubble 2. Checking that and counting such inconsistent starts.
|
|
185
|
+
inconsistent_bubble_starts += 1
|
|
186
|
+
node[flanks[1]][0] = bo
|
|
187
|
+
node[flanks[1]][1] = 0
|
|
188
|
+
|
|
189
|
+
no = 1
|
|
190
|
+
for traversal in at:
|
|
191
|
+
tn = list(filter(None, re.split("(>)|(<)", traversal)))
|
|
192
|
+
assert (tn[0] == flanks[0] and tn[1] == flanks[1]) and (
|
|
193
|
+
tn[-2] == flanks[2] and tn[-1] == flanks[3]
|
|
194
|
+
)
|
|
195
|
+
tn = tn[2:-2]
|
|
196
|
+
for n in tn:
|
|
197
|
+
if n == ">" or n == "<":
|
|
198
|
+
continue
|
|
199
|
+
# This condition checks for the case where the scaffold node is also present inside the bubble.
|
|
200
|
+
if node[n][0] == bo:
|
|
201
|
+
node[n][1] = (
|
|
202
|
+
1 # Assigning an NO tag of 1. Usually scaffold nodes are tagged with 0. Here it is tagged with 1 to show that this is also inside a bubble. Hence its orientation cannot be trusted.
|
|
203
|
+
)
|
|
204
|
+
continue
|
|
205
|
+
if not (node[n][0] == -1 or node[n][0] == bo + 1):
|
|
206
|
+
logger.debug(
|
|
207
|
+
"[0] Node %s present in Bubble %s is also present in Bubble %s at level 0. Anomaly found in variant at chromosome %s:%s."
|
|
208
|
+
% (n, str((bo + 1) / 2), str(node[n][0] / 2), fields[0], fields[1])
|
|
209
|
+
) # Asserting that the node n has not been called before. This will violate the definition of top level bubble.
|
|
210
|
+
inconsistent_nodes[0] += 1
|
|
211
|
+
node[n][0] = 0
|
|
212
|
+
node[n][1] = 0
|
|
213
|
+
continue
|
|
214
|
+
node[n][0] = bo + 1
|
|
215
|
+
if node[n][1] != -1:
|
|
216
|
+
continue
|
|
217
|
+
node[n][1] = no
|
|
218
|
+
no += 1
|
|
219
|
+
node[flanks[3]][0] = bo + 2 # Assigning the sort key to the end node of the buble
|
|
220
|
+
node[flanks[3]][1] = 0
|
|
221
|
+
bo += 2
|
|
222
|
+
logger.info("Total Variant Bubbles Processed: %d" % (total_variants))
|
|
223
|
+
logger.info("Inconsistent Bubble Starts: %d" % (inconsistent_bubble_starts))
|
|
224
|
+
for key, value in inconsistent_nodes.items():
|
|
225
|
+
logger.info("Number of nodes found in 2 separate bubbles (at level %d): %d" % (key, value))
|
|
226
|
+
reader.close()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def is_file_gzipped(src):
|
|
230
|
+
with open(src, "rb") as inp:
|
|
231
|
+
return inp.read(2) == b"\x1f\x8b"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This uses the BO and NO tags defined by Samarendra Pani through processing the bubbles in the VCF produced by Glenn Hickey for the MC graph. The script to add these BO and NO tags are in add-bubble-info.py in this directory.
|
|
3
|
+
|
|
4
|
+
Here since the bubbles are already defined, there is no need to process the entire alignment to find orientation of alignment since inversions are already taken into consideration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
from collections import defaultdict, namedtuple
|
|
11
|
+
import functools
|
|
12
|
+
import gzip
|
|
13
|
+
import re
|
|
14
|
+
from pysam import libcbgzf
|
|
15
|
+
import resource
|
|
16
|
+
from whatshap.timer import StageTimer
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
timers = StageTimer()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def setup_logging(debug):
|
|
23
|
+
handler = logging.StreamHandler()
|
|
24
|
+
root = logging.getLogger()
|
|
25
|
+
root.addHandler(handler)
|
|
26
|
+
root.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
parser = argparse.ArgumentParser()
|
|
31
|
+
parser.add_argument("--debug", action="store_true", default=False, help="Print debug messages")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--gfa", required=True, help="GFA file with the sort keys (BO and NO tagged)"
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument("--gaf", required=True, help="Input GAF File")
|
|
36
|
+
parser.add_argument("--output", default=None, help="Output GAF File path (Default: sys.stdout)")
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--bgzip",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Flag to bgzip the output. Can only be given with --output.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
options = parser.parse_args()
|
|
44
|
+
setup_logging(options.debug)
|
|
45
|
+
|
|
46
|
+
validate_arguments(options)
|
|
47
|
+
|
|
48
|
+
bubble_sort(options)
|
|
49
|
+
|
|
50
|
+
memory_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
51
|
+
logger.info("\nMemory Information")
|
|
52
|
+
logger.info(" Maximum memory usage: %.3f GB", memory_kb / 1e6)
|
|
53
|
+
logger.info("\nTime Summary:")
|
|
54
|
+
logger.info(" Time to parse GFA file: %.3f" % (timers.elapsed("read_gfa")))
|
|
55
|
+
logger.info(" Total time to sort GAF file: %.3f" % (timers.elapsed("total_sort")))
|
|
56
|
+
logger.info(" Time to parse GAF file: %.3f" % (timers.elapsed("read_gaf")))
|
|
57
|
+
logger.info(" Time to sort GAF file: %.3f" % (timers.elapsed("sort_gaf")))
|
|
58
|
+
logger.info(" Time to write GAF file: %.3f" % (timers.elapsed("write_gaf")))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def bubble_sort(options):
|
|
62
|
+
if options.output is None:
|
|
63
|
+
writer = sys.stdout
|
|
64
|
+
else:
|
|
65
|
+
if options.bgzip:
|
|
66
|
+
writer = libcbgzf.BGZFile(options.output, "wb")
|
|
67
|
+
else:
|
|
68
|
+
writer = open(options.output, "w")
|
|
69
|
+
nodes = defaultdict(lambda: [-1, -1])
|
|
70
|
+
with timers("read_gfa"):
|
|
71
|
+
read_gfa(options.gfa, nodes)
|
|
72
|
+
with timers("total_sort"):
|
|
73
|
+
sort(options.gaf, nodes, writer)
|
|
74
|
+
writer.close()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sort(gaf, nodes, writer):
|
|
78
|
+
logger.info("\n##### Parsing GAF file and sorting it #####")
|
|
79
|
+
if is_file_gzipped(gaf):
|
|
80
|
+
reader = gzip.open(gaf, "rt")
|
|
81
|
+
else:
|
|
82
|
+
reader = open(gaf, "r")
|
|
83
|
+
|
|
84
|
+
Alignment = namedtuple("Alignment", ["offset", "BO", "NO", "start"])
|
|
85
|
+
gaf_alignments = []
|
|
86
|
+
|
|
87
|
+
# First pass: Store all the alignment lines as minimally. Just storing line offset and alignment string.
|
|
88
|
+
with timers("read_gaf"):
|
|
89
|
+
logger.debug("Processing and Storing GAF Alignments...")
|
|
90
|
+
while True:
|
|
91
|
+
offset = reader.tell()
|
|
92
|
+
line = reader.readline()
|
|
93
|
+
if not line:
|
|
94
|
+
break
|
|
95
|
+
line = line.split("\t")
|
|
96
|
+
bo, no, start = process_alignment(line, nodes, offset)
|
|
97
|
+
gaf_alignments.append(Alignment(offset=offset, BO=bo, NO=no, start=start))
|
|
98
|
+
|
|
99
|
+
# Sorting the alignments based on BO and NO tag
|
|
100
|
+
with timers("sort_gaf"):
|
|
101
|
+
logger.debug("Sorting the alignments...")
|
|
102
|
+
gaf_alignments.sort(key=functools.cmp_to_key(compare_gaf))
|
|
103
|
+
|
|
104
|
+
# Writing the sorted file
|
|
105
|
+
with timers("write_gaf"):
|
|
106
|
+
logger.debug("Writing Output File...")
|
|
107
|
+
for alignment in gaf_alignments:
|
|
108
|
+
off = alignment.offset
|
|
109
|
+
reader.seek(off)
|
|
110
|
+
line = reader.readline()
|
|
111
|
+
write_to_file(line, writer)
|
|
112
|
+
|
|
113
|
+
reader.close()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def read_gfa(gfa, node):
|
|
117
|
+
logger.info("\n##### Parsing GFA file and reading sort key information #####")
|
|
118
|
+
if is_file_gzipped(gfa):
|
|
119
|
+
reader = gzip.open(gfa, "rt")
|
|
120
|
+
else:
|
|
121
|
+
reader = open(gfa, "r")
|
|
122
|
+
|
|
123
|
+
total_nodes = 0
|
|
124
|
+
tagged_nodes = 0
|
|
125
|
+
while True:
|
|
126
|
+
line = reader.readline()
|
|
127
|
+
if not line:
|
|
128
|
+
break
|
|
129
|
+
if line[0] != "S":
|
|
130
|
+
continue
|
|
131
|
+
total_nodes += 1
|
|
132
|
+
fields = line.split("\t")
|
|
133
|
+
for f in fields:
|
|
134
|
+
if f.startswith("BO:i:"):
|
|
135
|
+
node[fields[1]][0] = int(f[5:])
|
|
136
|
+
tagged_nodes += 1
|
|
137
|
+
if f.startswith("NO:i:"):
|
|
138
|
+
node[fields[1]][1] = int(f[5:])
|
|
139
|
+
|
|
140
|
+
logger.info("Total Nodes Processed: %d" % (total_nodes))
|
|
141
|
+
logger.info("Nodes with tags: %d" % (tagged_nodes))
|
|
142
|
+
reader.close()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def process_alignment(line, nodes, offset):
|
|
146
|
+
path = list(filter(None, re.split("(>)|(<)", line[5])))
|
|
147
|
+
orient = None
|
|
148
|
+
# If there is no scaffold node present in the alignment, then assuming that it is in the correct orientation.
|
|
149
|
+
# TODO: Need to find a way to deal with such alignments.
|
|
150
|
+
rv = False
|
|
151
|
+
bo = None
|
|
152
|
+
no = None
|
|
153
|
+
start = None
|
|
154
|
+
for n in path:
|
|
155
|
+
if n in [">", "<"]:
|
|
156
|
+
orient = n
|
|
157
|
+
continue
|
|
158
|
+
if nodes[n][0] == -1 or nodes[n][1] == -1:
|
|
159
|
+
logger.debug("[ERR]\tOF:i:%d\tND:Z:%s" % (offset, n))
|
|
160
|
+
# raise RuntimeError("Found a node which was not in VCF.")
|
|
161
|
+
# These nodes exist.
|
|
162
|
+
continue
|
|
163
|
+
if nodes[n][0] % 2 == 1 and nodes[n][1] == 0:
|
|
164
|
+
# Here we assume that the orientation of the first scaffold node found is the orientation for all the scaffold nodes. (Here we will have to keep in mind that some scaffold nodes are inside the bubbles also and their orientation can be anything there.)
|
|
165
|
+
# TODO: Have to check for these scaffold nodes that can be present inside bubbles (Cannot trust them)
|
|
166
|
+
# TODO: Have given the NO tag of 1 to these scaffold nodes to distinguish them from normal scaffold nodes which are tagged with 0.
|
|
167
|
+
if orient == ">":
|
|
168
|
+
break
|
|
169
|
+
else:
|
|
170
|
+
rv = True
|
|
171
|
+
break
|
|
172
|
+
# TODO: What to do when the start node that we have is an untagged node?
|
|
173
|
+
# One argument is to just sort them to the end and completely ignore them since they dont belong to any vcf bubble.
|
|
174
|
+
# That works for my tools but not ideal for general purpose.
|
|
175
|
+
if rv:
|
|
176
|
+
l = int(line[6])
|
|
177
|
+
e = int(line[8])
|
|
178
|
+
start = l - e
|
|
179
|
+
n = path[-1]
|
|
180
|
+
bo = nodes[n][0]
|
|
181
|
+
no = nodes[n][1]
|
|
182
|
+
else:
|
|
183
|
+
start = int(line[7])
|
|
184
|
+
n = path[1]
|
|
185
|
+
bo = nodes[n][0]
|
|
186
|
+
no = nodes[n][1]
|
|
187
|
+
# print(bo, no, start, sep='\t')
|
|
188
|
+
return bo, no, start
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def compare_gaf(al1, al2):
|
|
192
|
+
# Since we are sorting in ascending order, al1 is above al2 if it comes before al2.
|
|
193
|
+
# Have to consider the case when the start node is untagged and has BO and NO has -1. These should be sorted to the end and not the start
|
|
194
|
+
if al1.BO == -1 and al2.BO == -1:
|
|
195
|
+
if al1.offset < al2.offset:
|
|
196
|
+
return -1
|
|
197
|
+
else:
|
|
198
|
+
return 1
|
|
199
|
+
elif al1.BO == -1:
|
|
200
|
+
return 1
|
|
201
|
+
elif al2.BO == -1:
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
# Comparing BO tags
|
|
205
|
+
if al1.BO < al2.BO:
|
|
206
|
+
return -1
|
|
207
|
+
if al1.BO > al2.BO:
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
# Comparing NO tags
|
|
211
|
+
if al1.NO < al2.NO:
|
|
212
|
+
return -1
|
|
213
|
+
if al1.BO > al2.BO:
|
|
214
|
+
return 1
|
|
215
|
+
|
|
216
|
+
# Comparing start position in node
|
|
217
|
+
if al1.start < al2.start:
|
|
218
|
+
return -1
|
|
219
|
+
if al1.start > al2.start:
|
|
220
|
+
return 1
|
|
221
|
+
|
|
222
|
+
# This will be execulted only when two reads start at the exact same position at the same node. Then we use offset values. So the read which comes first in the GAF file will come higher up.
|
|
223
|
+
if al1.offset < al2.offset:
|
|
224
|
+
return -1
|
|
225
|
+
if al1.offset > al2.offset:
|
|
226
|
+
return 1
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def write_to_file(line, writer):
|
|
230
|
+
try:
|
|
231
|
+
writer.write(line)
|
|
232
|
+
except TypeError:
|
|
233
|
+
writer.write(str.encode(line))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def is_file_gzipped(src):
|
|
237
|
+
with open(src, "rb") as inp:
|
|
238
|
+
return inp.read(2) == b"\x1f\x8b"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def validate_arguments(options):
|
|
242
|
+
if options.bgzip and not options.output:
|
|
243
|
+
raise RuntimeError(
|
|
244
|
+
"--bgzip flag has been specified but not output path has been defined. Please define the output path."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
def biccs_nx(graph):
|
|
2
|
+
def edge_stack_to_set(edge_stack):
|
|
3
|
+
out_set = set()
|
|
4
|
+
for es in edge_stack:
|
|
5
|
+
for n in es:
|
|
6
|
+
out_set.add(n)
|
|
7
|
+
return out_set
|
|
8
|
+
|
|
9
|
+
def next_child(stack_item):
|
|
10
|
+
if not stack_item[3]:
|
|
11
|
+
return None
|
|
12
|
+
if stack_item[2] >= len(stack_item[3]):
|
|
13
|
+
return None
|
|
14
|
+
else:
|
|
15
|
+
stack_item[2] += 1
|
|
16
|
+
return stack_item[3][stack_item[2] - 1]
|
|
17
|
+
|
|
18
|
+
# depth-first search algorithm to generate articulation points
|
|
19
|
+
# and biconnected components
|
|
20
|
+
visited = set()
|
|
21
|
+
|
|
22
|
+
for n in graph.nodes:
|
|
23
|
+
if n in visited:
|
|
24
|
+
continue
|
|
25
|
+
discovery = {n: 0}
|
|
26
|
+
low = {n: 0}
|
|
27
|
+
root_children = 0
|
|
28
|
+
artic_points = []
|
|
29
|
+
components = []
|
|
30
|
+
visited.add(n)
|
|
31
|
+
edge_stack = []
|
|
32
|
+
# stack = [(start, start, iter(G[start]))]
|
|
33
|
+
neighbors = graph[n].neighbors()
|
|
34
|
+
stack = [[n, n, 0, neighbors]]
|
|
35
|
+
while stack:
|
|
36
|
+
parent = stack[-1][0]
|
|
37
|
+
child = stack[-1][1]
|
|
38
|
+
nn = next_child(stack[-1])
|
|
39
|
+
|
|
40
|
+
# pdb.set_trace()
|
|
41
|
+
if nn:
|
|
42
|
+
if nn == parent:
|
|
43
|
+
continue
|
|
44
|
+
if nn in visited:
|
|
45
|
+
if discovery[nn] <= discovery[child]:
|
|
46
|
+
edge_stack.append((child, nn))
|
|
47
|
+
low[child] = min(low[child], discovery[nn])
|
|
48
|
+
else:
|
|
49
|
+
low[nn] = len(discovery)
|
|
50
|
+
discovery[nn] = low[nn]
|
|
51
|
+
visited.add(nn)
|
|
52
|
+
stack.append([child, nn, 0, graph[nn].neighbors()])
|
|
53
|
+
edge_stack.append((child, nn))
|
|
54
|
+
elif nn is None:
|
|
55
|
+
stack.pop()
|
|
56
|
+
if len(stack) > 1:
|
|
57
|
+
if low[child] >= discovery[parent]:
|
|
58
|
+
artic_points.append(parent)
|
|
59
|
+
cut_point = edge_stack.index((parent, child))
|
|
60
|
+
comp = edge_stack_to_set(edge_stack[cut_point:])
|
|
61
|
+
components.append(comp)
|
|
62
|
+
edge_stack = edge_stack[:cut_point]
|
|
63
|
+
# components.append(edge_stack_to_set(edge_stack[edge_stack.index((parent, child)):]))
|
|
64
|
+
low[parent] = min(low[parent], low[child])
|
|
65
|
+
elif stack:
|
|
66
|
+
root_children += 1
|
|
67
|
+
components.append(
|
|
68
|
+
edge_stack_to_set(edge_stack[edge_stack.index((parent, child)) :])
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if root_children > 1:
|
|
72
|
+
artic_points.append[n]
|
|
73
|
+
|
|
74
|
+
return components, artic_points
|
|
75
|
+
|
|
76
|
+
def bi_cc_rec(
|
|
77
|
+
self, n_id, parent, low, disc, stack, node_ids, all_bi_cc, artic_points, disc_time
|
|
78
|
+
):
|
|
79
|
+
# Count of children in current node
|
|
80
|
+
u = node_ids[n_id]
|
|
81
|
+
children = 0
|
|
82
|
+
# Initialize discovery time and low value
|
|
83
|
+
disc[u] = disc_time[0]
|
|
84
|
+
low[u] = disc_time[0]
|
|
85
|
+
disc_time[0] += 1
|
|
86
|
+
|
|
87
|
+
# Recur for all the vertices adjacent to this vertex
|
|
88
|
+
for neighbor_id in self.nodes[n_id].neighbors():
|
|
89
|
+
v = node_ids[neighbor_id]
|
|
90
|
+
# If v is not visited yet, then make it a child of u
|
|
91
|
+
# in DFS tree and recur for it
|
|
92
|
+
if disc[v] == -1:
|
|
93
|
+
parent[v] = n_id
|
|
94
|
+
children += 1
|
|
95
|
+
stack.append((n_id, neighbor_id)) # store the edge in stack
|
|
96
|
+
self.bi_cc_rec(
|
|
97
|
+
neighbor_id,
|
|
98
|
+
parent,
|
|
99
|
+
low,
|
|
100
|
+
disc,
|
|
101
|
+
stack,
|
|
102
|
+
node_ids,
|
|
103
|
+
all_bi_cc,
|
|
104
|
+
artic_points,
|
|
105
|
+
disc_time,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Check if the subtree rooted with v has a connection to
|
|
109
|
+
# one of the ancestors of u
|
|
110
|
+
# Case 1 -- per Strongly Connected Components Article
|
|
111
|
+
low[u] = min(low[u], low[v])
|
|
112
|
+
|
|
113
|
+
# If u is an articulation point, pop
|
|
114
|
+
# all edges from stack until (u, v)
|
|
115
|
+
if parent[u] == -1 and children > 1 or parent[u] != -1 and low[v] >= disc[u]:
|
|
116
|
+
artic_points.append(n_id)
|
|
117
|
+
w = -1
|
|
118
|
+
one_bi_cc = set()
|
|
119
|
+
while w != (n_id, neighbor_id):
|
|
120
|
+
w = stack.pop()
|
|
121
|
+
for n in w:
|
|
122
|
+
one_bi_cc.add(n)
|
|
123
|
+
all_bi_cc.append(one_bi_cc)
|
|
124
|
+
|
|
125
|
+
elif neighbor_id != parent[u] and low[u] > disc[v]:
|
|
126
|
+
low[u] = min(low[u], disc[v])
|
|
127
|
+
stack.append((n_id, neighbor_id))
|
|
128
|
+
|
|
129
|
+
def bicc(self):
|
|
130
|
+
"""
|
|
131
|
+
find biconnected components and returns a list of these components in terms of node ids
|
|
132
|
+
"""
|
|
133
|
+
disc_time = [0]
|
|
134
|
+
all_bi_cc = list()
|
|
135
|
+
node_ids = dict()
|
|
136
|
+
artic_points = []
|
|
137
|
+
# node_ids_list = [0] * len(self)
|
|
138
|
+
for idx, n_id in enumerate(list(self.nodes.keys())):
|
|
139
|
+
node_ids[n_id] = idx
|
|
140
|
+
# node_ids_list[idx] = n_id
|
|
141
|
+
|
|
142
|
+
# Initialize disc and low, and parent arrays
|
|
143
|
+
n_vertices = len(self)
|
|
144
|
+
disc = [-1] * n_vertices
|
|
145
|
+
low = [-1] * n_vertices
|
|
146
|
+
parent = [-1] * n_vertices
|
|
147
|
+
stack = []
|
|
148
|
+
# Call the recursive helper function to
|
|
149
|
+
# find articulation points
|
|
150
|
+
# in DFS tree rooted with vertex 'i'
|
|
151
|
+
for n_id in self.nodes.keys():
|
|
152
|
+
i = node_ids[n_id]
|
|
153
|
+
if disc[i] == -1:
|
|
154
|
+
self.bi_cc_rec(
|
|
155
|
+
n_id, parent, low, disc, stack, node_ids, all_bi_cc, artic_points, disc_time
|
|
156
|
+
)
|
|
157
|
+
# If stack is not empty, pop all edges from stack
|
|
158
|
+
if stack:
|
|
159
|
+
one_bi_cc = set()
|
|
160
|
+
while stack:
|
|
161
|
+
w = stack.pop()
|
|
162
|
+
for n in w:
|
|
163
|
+
one_bi_cc.add(n)
|
|
164
|
+
all_bi_cc.append(one_bi_cc)
|
|
165
|
+
return all_bi_cc, artic_points
|