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.
Files changed (96) hide show
  1. gaftools-1.0.0/.attic/.vcf_based_sorting_scripts/add-bubble-info.py +235 -0
  2. gaftools-1.0.0/.attic/.vcf_based_sorting_scripts/bubble-sort.py +249 -0
  3. gaftools-1.0.0/.attic/bicc_networkx.py +165 -0
  4. gaftools-1.0.0/.attic/find_bubbles.py +121 -0
  5. gaftools-1.0.0/.attic/graph.py +131 -0
  6. gaftools-1.0.0/.attic/sort.py +293 -0
  7. gaftools-1.0.0/.github/workflows/ci.yml +90 -0
  8. gaftools-1.0.0/.gitignore +22 -0
  9. gaftools-1.0.0/.pre-commit-config.yaml +18 -0
  10. gaftools-1.0.0/.readthedocs.yaml +22 -0
  11. gaftools-1.0.0/CHANGES.rst +7 -0
  12. gaftools-1.0.0/LICENSE +21 -0
  13. gaftools-1.0.0/MANIFEST.in +11 -0
  14. gaftools-1.0.0/PKG-INFO +64 -0
  15. gaftools-1.0.0/README.md +5 -0
  16. gaftools-1.0.0/docs/Makefile +81 -0
  17. gaftools-1.0.0/docs/README.rst +24 -0
  18. gaftools-1.0.0/docs/_static/bo_tags.png +0 -0
  19. gaftools-1.0.0/docs/_static/no_tags.png +0 -0
  20. gaftools-1.0.0/docs/changes.rst +3 -0
  21. gaftools-1.0.0/docs/conf.py +49 -0
  22. gaftools-1.0.0/docs/develop.rst +138 -0
  23. gaftools-1.0.0/docs/guide.rst +305 -0
  24. gaftools-1.0.0/docs/index.rst +12 -0
  25. gaftools-1.0.0/docs/installation.rst +55 -0
  26. gaftools-1.0.0/docs/requirements.txt +5 -0
  27. gaftools-1.0.0/gaftools/.gitignore +2 -0
  28. gaftools-1.0.0/gaftools/__init__.py +3 -0
  29. gaftools-1.0.0/gaftools/__main__.py +96 -0
  30. gaftools-1.0.0/gaftools/_version.py +16 -0
  31. gaftools-1.0.0/gaftools/args.py +21 -0
  32. gaftools-1.0.0/gaftools/cli/__init__.py +26 -0
  33. gaftools-1.0.0/gaftools/cli/index.py +182 -0
  34. gaftools-1.0.0/gaftools/cli/order_gfa.py +350 -0
  35. gaftools-1.0.0/gaftools/cli/phase.py +132 -0
  36. gaftools-1.0.0/gaftools/cli/realign.py +292 -0
  37. gaftools-1.0.0/gaftools/cli/sort.py +260 -0
  38. gaftools-1.0.0/gaftools/cli/stat.py +181 -0
  39. gaftools-1.0.0/gaftools/cli/view.py +251 -0
  40. gaftools-1.0.0/gaftools/conversion.py +239 -0
  41. gaftools-1.0.0/gaftools/gaf.py +205 -0
  42. gaftools-1.0.0/gaftools/gfa.py +876 -0
  43. gaftools-1.0.0/gaftools/timer.py +60 -0
  44. gaftools-1.0.0/gaftools/utils.py +90 -0
  45. gaftools-1.0.0/gaftools.egg-info/PKG-INFO +64 -0
  46. gaftools-1.0.0/gaftools.egg-info/SOURCES.txt +94 -0
  47. gaftools-1.0.0/gaftools.egg-info/dependency_links.txt +1 -0
  48. gaftools-1.0.0/gaftools.egg-info/entry_points.txt +2 -0
  49. gaftools-1.0.0/gaftools.egg-info/requires.txt +19 -0
  50. gaftools-1.0.0/gaftools.egg-info/top_level.txt +1 -0
  51. gaftools-1.0.0/pyproject.toml +83 -0
  52. gaftools-1.0.0/requirements.txt +2 -0
  53. gaftools-1.0.0/scripts/split_graph.sh +82 -0
  54. gaftools-1.0.0/setup.cfg +4 -0
  55. gaftools-1.0.0/setup.py +16 -0
  56. gaftools-1.0.0/tests/data/alignments-graphaligner.gaf +2 -0
  57. gaftools-1.0.0/tests/data/alignments-graphaligner.gaf.gz +0 -0
  58. gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck-only-s2.gaf +2 -0
  59. gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck-only-s464827.gaf +1 -0
  60. gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf +3 -0
  61. gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf.gvi +0 -0
  62. gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf.gz +0 -0
  63. gaftools-1.0.0/tests/data/alignments-minigraph-stable-conversioncheck.gaf.gz.gvi +0 -0
  64. gaftools-1.0.0/tests/data/alignments-minigraph-stable.gaf +2 -0
  65. gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck-only-s2.gaf +2 -0
  66. gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck-only-s464827.gaf +1 -0
  67. gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf +3 -0
  68. gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf.gvi +0 -0
  69. gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf.gz +0 -0
  70. gaftools-1.0.0/tests/data/alignments-minigraph-unstable-conversioncheck.gaf.gz.gvi +0 -0
  71. gaftools-1.0.0/tests/data/alignments-minigraph-unstable.gaf +2 -0
  72. gaftools-1.0.0/tests/data/alignments-more-graphaligner.gaf +8 -0
  73. gaftools-1.0.0/tests/data/alignments-more-graphaligner.gaf.gz +0 -0
  74. gaftools-1.0.0/tests/data/large-graph-chr1.gfa.gz +0 -0
  75. gaftools-1.0.0/tests/data/reads-conversioncheck.fa +6 -0
  76. gaftools-1.0.0/tests/data/reads-more.fa +16 -0
  77. gaftools-1.0.0/tests/data/reads.fa +4 -0
  78. gaftools-1.0.0/tests/data/reads.fa.fai +2 -0
  79. gaftools-1.0.0/tests/data/smallgraph-noseq.gfa +32 -0
  80. gaftools-1.0.0/tests/data/smallgraph-ordered.gfa +32 -0
  81. gaftools-1.0.0/tests/data/smallgraph.gfa +32 -0
  82. gaftools-1.0.0/tests/data/smallgraph_withN.gfa +5 -0
  83. gaftools-1.0.0/tests/data/test_GFA_class.gfa +8 -0
  84. gaftools-1.0.0/tests/data/test_GFA_class_wrong_graph.gfa +8 -0
  85. gaftools-1.0.0/tests/test_extract_path.py +36 -0
  86. gaftools-1.0.0/tests/test_gaf.py +44 -0
  87. gaftools-1.0.0/tests/test_gaf_gzipped.py +50 -0
  88. gaftools-1.0.0/tests/test_gfa_class.py +136 -0
  89. gaftools-1.0.0/tests/test_index.py +65 -0
  90. gaftools-1.0.0/tests/test_parsegaf_tags.py +22 -0
  91. gaftools-1.0.0/tests/test_run_ordergfa.py +28 -0
  92. gaftools-1.0.0/tests/test_run_realign.py +53 -0
  93. gaftools-1.0.0/tests/test_run_stat.py +63 -0
  94. gaftools-1.0.0/tests/test_sort.py +31 -0
  95. gaftools-1.0.0/tests/test_view.py +462 -0
  96. 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