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 ADDED
@@ -0,0 +1,2 @@
1
+ *.cpp
2
+ *.cpython-*.so
gaftools/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ from ._version import version as __version__
gaftools/__main__.py ADDED
@@ -0,0 +1,96 @@
1
+ # The code has been taken from WhatsHap.
2
+ # Link to WhatsHap: https://github.com/whatshap/whatshap
3
+
4
+
5
+ import sys
6
+ import pkgutil
7
+ import importlib
8
+ import logging
9
+
10
+ import gaftools.cli as cli_package
11
+ from . import __version__
12
+ from .args import HelpfulArgumentParser
13
+ from .cli import CommandLineError
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class NiceFormatter(logging.Formatter):
20
+ """
21
+ Do not prefix "INFO:" to info-level log messages (but do it for all other
22
+ levels).
23
+
24
+ Based on http://stackoverflow.com/a/9218261/715090 .
25
+ """
26
+
27
+ def format(self, record):
28
+ if record.levelno != logging.INFO:
29
+ record.msg = "{}: {}".format(record.levelname, record.msg)
30
+ return super().format(record)
31
+
32
+
33
+ def setup_logging(debug):
34
+ """
35
+ Set up logging. If debug is True, then DEBUG level messages are printed.
36
+ """
37
+ handler = logging.StreamHandler()
38
+ handler.setFormatter(NiceFormatter())
39
+ root = logging.getLogger()
40
+ root.addHandler(handler)
41
+ root.setLevel(logging.DEBUG if debug else logging.INFO)
42
+
43
+
44
+ # Ensure all the versions here
45
+ def ensure_version():
46
+ from pysam import __version__ as pysam_version
47
+ from distutils.version import LooseVersion
48
+
49
+ if LooseVersion(pysam_version) < LooseVersion("0.18.0"):
50
+ sys.exit("gaftools requires pysam >= 0.18.0")
51
+
52
+
53
+ def main(argv=sys.argv[1:]):
54
+ ensure_version()
55
+ parser = HelpfulArgumentParser(description=__doc__, prog="gaftools")
56
+ parser.add_argument("--version", action="version", version="%(prog)s " + __version__)
57
+ parser.add_argument("--debug", action="store_true", default=False, help="Print debug messages")
58
+ subparsers = parser.add_subparsers()
59
+
60
+ # Import each module that implements a subcommand and add a subparser for it.
61
+ # Each subcommand is implemented as a module in the cli subpackage.
62
+ # It needs to implement an add_arguments() and a main() function.
63
+ modules = pkgutil.iter_modules(cli_package.__path__)
64
+ for _, module_name, _ in modules:
65
+ module = importlib.import_module("." + module_name, cli_package.__name__)
66
+ subparser = subparsers.add_parser(
67
+ module_name,
68
+ help=module.__doc__.strip().split("\n", maxsplit=1)[0],
69
+ description=module.__doc__,
70
+ )
71
+ subparser.set_defaults(module=module, subparser=subparser)
72
+ module.add_arguments(subparser)
73
+
74
+ args = parser.parse_args(argv)
75
+ setup_logging(args.debug)
76
+
77
+ if not hasattr(args, "module"):
78
+ parser.error("Please provide the name of a subcommand to run")
79
+ else:
80
+ module = args.module
81
+ if hasattr(args.module, "validate"):
82
+ subparser = args.subparser
83
+ args.module.validate(args, subparser)
84
+ del args.subparser
85
+ del args.module
86
+ del args.debug
87
+ try:
88
+ module.main(args)
89
+ except CommandLineError as e:
90
+ logger.error(str(e), exc_info=True)
91
+ logger.debug("Command line error. Traceback:", exc_info=True)
92
+ sys.exit(1)
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
gaftools/_version.py ADDED
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '1.1.2'
21
+ __version_tuple__ = version_tuple = (1, 1, 2)
gaftools/args.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ The code has been taken from WhatsHap.
3
+ Link to WhatsHap: https://github.com/whatshap/whatshap
4
+ """
5
+
6
+ from argparse import ArgumentParser, RawDescriptionHelpFormatter
7
+ import sys
8
+
9
+
10
+ class HelpfulArgumentParser(ArgumentParser):
11
+ """An ArgumentParser that prints full help on errors."""
12
+
13
+ def __init__(self, *args, **kwargs):
14
+ if "formatter_class" not in kwargs:
15
+ kwargs["formatter_class"] = RawDescriptionHelpFormatter
16
+ super().__init__(*args, **kwargs)
17
+
18
+ def error(self, message):
19
+ self.print_help(sys.stderr)
20
+ args = {"prog": "gaftools", "message": message}
21
+ self.exit(2, "%(prog)s error: %(message)s\n" % args)
@@ -0,0 +1,26 @@
1
+ """
2
+ The code has been taken from WhatsHap.
3
+ Link to WhatsHap: https://github.com/whatshap/whatshap
4
+ """
5
+
6
+ import sys
7
+ import resource
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CommandLineError(Exception):
14
+ """An anticipated command-line error occurred. This ends up as a user-visible error message"""
15
+
16
+
17
+ def log_memory_usage(include_children=False):
18
+ if sys.platform == "linux":
19
+ if include_children:
20
+ memory_kb = (
21
+ resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
22
+ + resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
23
+ )
24
+ else:
25
+ memory_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
26
+ logger.info("Maximum memory usage: %.3f GB", memory_kb / 1e6)
@@ -0,0 +1,65 @@
1
+ """
2
+ Find the genomic sequence of a given GFA path.
3
+ """
4
+
5
+ import logging
6
+ from gaftools.cli import log_memory_usage
7
+ from gaftools.timer import StageTimer
8
+ from gaftools.gfa import GFA
9
+ from gaftools.utils import FileWriter
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def run(gfa_path, input_path, output=None, fasta=False):
15
+ timers = StageTimer()
16
+
17
+ graph = GFA(gfa_path)
18
+ if input_path[0] in [">", "<"]:
19
+ # detected node path
20
+ nodes = [input_path]
21
+ path_seqs = [graph.extract_path(input_path)]
22
+ else:
23
+ # detected file
24
+ reader = open(input_path, "r")
25
+ nodes = []
26
+ path_seqs = []
27
+ for line in reader:
28
+ nodes.append(line.strip())
29
+ path_seqs.append(graph.extract_path(nodes[-1]))
30
+ reader.close()
31
+
32
+ writer = FileWriter(output)
33
+ if fasta:
34
+ for node, path_seq in zip(nodes, path_seqs):
35
+ writer.write(f">seq_{node}\n")
36
+ writer.write(f"{path_seq}\n")
37
+ else:
38
+ for node, path_seq in zip(nodes, path_seqs):
39
+ writer.write(f"{path_seq}\n")
40
+
41
+ writer.close()
42
+
43
+ logger.info("\n== SUMMARY ==")
44
+ total_time = timers.total()
45
+ log_memory_usage()
46
+ logger.info("Total time: %9.2f s", total_time)
47
+
48
+
49
+ # fmt:off
50
+ def add_arguments(parser):
51
+ arg = parser.add_argument
52
+ # Positional arguments
53
+ arg("gfa_path", metavar="GFA",
54
+ help="GFA file (can be bgzip-compressed)")
55
+ arg("input_path", metavar="path",
56
+ help='GFA node path to retrieve the sequence (e.g., ">s82312<s82313") with the quotes OR a filepath containing node paths in different lines')
57
+ arg("-o", "--output", default=None,
58
+ help="Output file. If omitted, use standard output.")
59
+ arg("-f", "--fasta", action="store_true",
60
+ help="Flag to output the sequence as a FASTA file with the seqeunce named seq_<node path>")
61
+ # fmt:on
62
+
63
+
64
+ def main(args):
65
+ run(**vars(args))
@@ -0,0 +1,340 @@
1
+ """
2
+ Converting a GFA file to rGFA format using the W-lines
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import logging
8
+ import re
9
+ from pysam import libcbgzf
10
+ from gaftools.utils import FileWriter
11
+ from gaftools.timer import StageTimer
12
+ from gaftools.cli import log_memory_usage
13
+
14
+ logger = logging.getLogger(__name__)
15
+ timers = StageTimer()
16
+
17
+ orient_switch = {"+": "-", "-": "+", ">": "<", "<": ">"}
18
+
19
+
20
+ class Node:
21
+ def __init__(self, line):
22
+ self.ln: int = len(line[2])
23
+
24
+
25
+ class OutputNode:
26
+ def __init__(self, ln, so, sn, sr, orient=">"):
27
+ self.ln: int = ln
28
+ self.so: int = so
29
+ self.sn: str = sn
30
+ self.sr: int = sr
31
+ self.orient: str = orient
32
+
33
+ # converting tag dictionary to string format.
34
+ def tags_to_string(self):
35
+ return f"\tLN:i:{self.ln}\tSO:i:{self.so}\tSN:Z:{self.sn}\tSR:i:{self.sr}"
36
+
37
+
38
+ def run(gfa=None, reference_name="CHM13", reference_tagged=False, seqfile=None, output=None):
39
+ # if reference node info is missing, then need to tag the reference nodes based on the W-line given by user.
40
+ with timers("read_gfa"):
41
+ logger.info("Reading GFA file.")
42
+ node_dict, walks = process_gfa(gfa)
43
+ tmp_walk_file = FileWriter(output + "-walks.tmp.gz")
44
+ with timers("tag_reference"):
45
+ logger.info("Tagging reference nodes and writing reference walks to temp file.")
46
+ create_ref_tags(node_dict, walks, reference_name, gfa, tmp_walk_file, reference_tagged)
47
+ # if reference node info is already tagged (as is the case with the minigraph-cactus gfa), then just need to fill the rest of the information.
48
+ if seqfile:
49
+ sample_order = get_sample_order(seqfile)
50
+ for index, sample in enumerate(sample_order):
51
+ if sample == reference_name:
52
+ continue
53
+ logger.info(
54
+ f"Tagging assembly nodes for sample {sample} and writing corrected walks to temp file."
55
+ )
56
+ create_assembly_tags(node_dict, walks, sample, index, gfa, tmp_walk_file)
57
+ tmp_walk_file.close()
58
+ # reading the temporary file to write the rGFA file.
59
+ writer = FileWriter(output)
60
+ logger.info("Writing S and L lines to temp file.")
61
+ stats_counter = write_rGFA(gfa, node_dict, writer)
62
+ with timers("write_w_lines"):
63
+ logger.info("Writing the corrected W lines to the rGFA file.")
64
+ with libcbgzf.BGZFile(output + "-walks.tmp.gz", "rb") as reader:
65
+ for line in reader:
66
+ writer.write(line.decode("utf-8"))
67
+ writer.write("\n")
68
+ with timers("cleanup"):
69
+ logger.info("Cleaning up temporary files.")
70
+ os.remove(output + "-walks.tmp.gz")
71
+ for key, value in stats_counter.items():
72
+ logger.info(f"Number of {key}: {value}")
73
+ if stats_counter["rank-0 nodes"] == 0:
74
+ if reference_tagged:
75
+ logger.warning(
76
+ "No rank-0 nodes found in the GFA file. Either --reference-tagged was given but no reference nodes were tagged or no. Output rGFA will not have any rank-0 nodes."
77
+ )
78
+ else:
79
+ logger.warning(
80
+ "No rank-0 nodes found in the GFA file. Please check the W-lines for the reference genome. Output rGFA will not have any rank-0 nodes."
81
+ )
82
+ logger.info("\n== SUMMARY ==")
83
+ total_time = timers.total()
84
+ log_memory_usage()
85
+ # fmt: off
86
+ logger.info("Time to read GFA file: %9.2f s", timers.elapsed("read_gfa"))
87
+ logger.info("Time to tag reference nodes: %9.2f s", timers.elapsed("tag_reference"))
88
+ logger.info("Time to tag assembly nodes: %9.2f s", timers.elapsed("walk_generator")+timers.elapsed("write_temp_walk")+timers.elapsed("checking_node_class")+timers.elapsed("checking_orientation")+timers.elapsed("splitting_walk"))
89
+ logger.info(" Time for walk generator: %9.2f s", timers.elapsed("walk_generator"))
90
+ logger.info(" Time for writing temporary walks: %9.2f s", timers.elapsed("write_temp_walk"))
91
+ logger.info(" Time for checking node class: %9.2f s", timers.elapsed("checking_node_class"))
92
+ logger.info(" Time for checking orientation: %9.2f s", timers.elapsed("checking_orientation"))
93
+ logger.info(" Time for splitting walk: %9.2f s", timers.elapsed("splitting_walk"))
94
+ logger.info("Time to write S lines: %9.2f s", timers.elapsed("write_s_lines"))
95
+ logger.info("Time to write L lines: %9.2f s", timers.elapsed("write_l_lines"))
96
+ logger.info("Time to write W lines: %9.2f s", timers.elapsed("write_w_lines"))
97
+ logger.info("Time to cleanup temporary files: %9.2f s", timers.elapsed("cleanup"))
98
+ logger.info("Time spent on rest: %9.2f s", total_time - timers.sum())
99
+ logger.info("Total time: %9.2f s", total_time)
100
+ # fmt: on
101
+
102
+
103
+ # fmt: off
104
+ def add_arguments(parser):
105
+ arg = parser.add_argument
106
+ # Positional arguments
107
+ arg('gfa', metavar='GFA',
108
+ help='GFA file (can be bgzip-compressed). This GFA should have a W-line corresponding to the reference genome or the reference nodes have to be tagged already.')
109
+ arg("--reference-name", metavar='REFERENCE NAME', default='CHM13',
110
+ help="The name of the reference genome given in the W-line. Default: CHM13")
111
+ arg("--reference-tagged", default=False, action="store_true",
112
+ help="Flag to denote reference nodes are already tagged in the GFA.")
113
+ arg("--seqfile", metavar='SEQFILE',
114
+ help='File containing the sequence in which assemblies were given. Provide the seqfile given as part of running minigraph-cactus. It has the format: <assembly_name><tab><assembly_path>'
115
+ 'There should be W lines for each assembly in the GFA.')
116
+ arg("--output", metavar='rGFA',
117
+ help='Output rGFA (bgzipped if the file ends with .gz). If omitted, use standard output.')
118
+ # fmt: on
119
+
120
+
121
+ def validate(args, parser):
122
+ if not os.path.exists(args.gfa):
123
+ parser.error(f"GFA file {args.gfa} does not exist.")
124
+ if args.seqfile and not os.path.exists(args.seqfile):
125
+ parser.error(f"Seqfile {args.seqfile} does not exist.")
126
+ if not args.reference_name and not args.reference_tagged:
127
+ parser.error(
128
+ "Either provide the reference name with --reference-name or flag that reference nodes are already tagged with --reference-tagged."
129
+ )
130
+
131
+
132
+ # getting the order of the samples from the seqfile.
133
+ def get_sample_order(seqfile):
134
+ order = []
135
+ with open(seqfile, "r") as reader:
136
+ for line in reader:
137
+ sample, _ = line.strip().split("\t")
138
+ order.append(sample)
139
+ return order
140
+
141
+
142
+ # This function processes the GFA file and returns the node dictionary and the walk dictionary.
143
+ def process_gfa(gfa):
144
+ nodes = {}
145
+ walks = {}
146
+ gzipped = False
147
+ opened_file = None
148
+ if gfa.endswith(".gz"):
149
+ opened_file = libcbgzf.BGZFile(gfa, "rb")
150
+ gzipped = True
151
+ else:
152
+ opened_file = open(gfa, "r")
153
+ while True:
154
+ offset = opened_file.tell()
155
+ line = opened_file.readline().decode("utf-8") if gzipped else opened_file.readline()
156
+ if not line:
157
+ break
158
+ if line.startswith("S"):
159
+ line = line.strip().split("\t")
160
+ node_id = line[1]
161
+ if len(line) > 3:
162
+ ln = len(line[2])
163
+ for field in line[3:]:
164
+ tag, _, value = field.split(":")
165
+ if tag == "LN":
166
+ ln = int(value)
167
+ elif tag == "SN":
168
+ sn = value
169
+ elif tag == "SO":
170
+ so = int(value)
171
+ elif tag == "SR":
172
+ sr = int(value)
173
+ nodes[node_id] = OutputNode(ln=ln, so=so, sn=sn, sr=sr)
174
+ else:
175
+ nodes[node_id] = Node(line)
176
+ elif line.startswith("W"):
177
+ _, name, hap, _, _, _, _ = line.strip().split(
178
+ "\t",
179
+ )
180
+ try:
181
+ walks[(name, hap)].append(offset)
182
+ except KeyError:
183
+ walks[(name, hap)] = [offset]
184
+ opened_file.close()
185
+ return nodes, walks
186
+
187
+
188
+ def yield_walks(file, offsets, gzipped):
189
+ for offset in offsets:
190
+ file.seek(offset)
191
+ line = file.readline().decode("utf-8") if gzipped else file.readline()
192
+ _, _, _, assm_name, start, end, path = line.strip().split(
193
+ "\t",
194
+ )
195
+ yield assm_name, int(start), int(end), path
196
+
197
+
198
+ def revcomp(seq):
199
+ return seq.translate(str.maketrans("ACGTN", "TGCAN"))[::-1]
200
+
201
+
202
+ # creating the rGFA relevant tags for the nodes coming from the reference genome.
203
+ # the walk corresponding to reference genome should be (by design) in the 5' to 3' orientation and without cycles.
204
+ def create_ref_tags(nodes, walks, reference_name, file, tmp_walk_file, reference_tagged):
205
+ gzipped = False
206
+ if file.endswith(".gz"):
207
+ opened_file = libcbgzf.BGZFile(file, "rb")
208
+ gzipped = True
209
+ else:
210
+ opened_file = open(file, "r")
211
+ try:
212
+ walks_list = yield_walks(opened_file, walks[(reference_name, "0")], gzipped)
213
+ except KeyError:
214
+ logger.error(f"No walks found for reference genome {reference_name}.")
215
+ sys.exit(1)
216
+ for assm_name, walk_start, walk_end, walk_path in walks_list:
217
+ tmp_walk_file.write(
218
+ f"W\t{reference_name}\t0\t{assm_name}\t{walk_start}\t{walk_end}\t{walk_path}\n"
219
+ )
220
+ if reference_tagged:
221
+ continue
222
+ sn = f"{reference_name}#{assm_name}"
223
+ # check how slow this part is.
224
+ walk = list(filter(None, re.split("(>)|(<)", walk_path)))
225
+ so = walk_start # initializing SO tag
226
+ for i in range(len(walk)):
227
+ if walk[i] in [">", "<"]:
228
+ assert walk[i] == ">", "Reference walk should be in the 5' to 3' orientation."
229
+ continue
230
+ id = walk[i]
231
+ assert isinstance(nodes[id], Node), f"Reference node {id} is not a rank-0 node."
232
+ nodes[id] = OutputNode(ln=nodes[id].ln, so=so, sn=sn, sr=0)
233
+ so = so + nodes[id].ln
234
+ opened_file.close()
235
+
236
+
237
+ # creating the rGFA relevant tags for the nodes coming from the assemblies.
238
+ # for assemblies, the assembly sequence can be in the 3' to 5' orientation. Then the node orientation is affected
239
+ # but the SO tags are exclusively based on how the reference sequence looks.
240
+ # in hprc-v1.0-minigraph-chm13.gfa.gz, the HG00438#1#JAHBCB010000006.1 contig in the file HG00438.paternal.f1_assembly_v2_genbank.fa is in the reverse orientation.
241
+ # you can see how the nodes of that contig (s483177, s483178, s483179) are oriented in the bubble >s13080>s13086
242
+ def create_assembly_tags(nodes, walks, sample, index, file, tmp_walk_file):
243
+ if "." in sample:
244
+ sample_name, hap = sample.split(".")
245
+ else:
246
+ sample_name, hap = sample, "0"
247
+ gzipped = False
248
+ if file.endswith(".gz"):
249
+ opened_file = libcbgzf.BGZFile(file, "rb")
250
+ gzipped = True
251
+ else:
252
+ opened_file = open(file, "r")
253
+ with timers("walk_generator"):
254
+ try:
255
+ walks_list = yield_walks(opened_file, walks[(sample_name, hap)], gzipped)
256
+ except KeyError:
257
+ logger.error(f"No walks found for sample {sample_name}#{hap}.")
258
+ return
259
+ for assm_name, walk_start, walk_end, walk_path in walks_list:
260
+ # writing the initial fields of walk to a temporary file.
261
+ with timers("write_temp_walk"):
262
+ tmp_walk_file.write(f"W\t{sample_name}\t{hap}\t{assm_name}\t{walk_start}\t{walk_end}\t")
263
+ sn = f"{sample_name}#{hap}#{assm_name}"
264
+ with timers("splitting_walk"):
265
+ walk = list(filter(None, re.split("(>)|(<)", walk_path)))
266
+ so = walk_start # initializing SO tag
267
+ orient = None
268
+ for i in range(len(walk)):
269
+ if walk[i] in [">", "<"]:
270
+ orient = walk[i]
271
+ continue
272
+ id = walk[i]
273
+ # writing the temporary walk file and doing this conversion increased runtime significantly.
274
+ # is this from the IO operation or the switching operations?
275
+ with timers("checking_node_class"):
276
+ if isinstance(nodes[id], Node):
277
+ nodes[id] = OutputNode(ln=nodes[id].ln, so=so, sn=sn, sr=index, orient=orient)
278
+ with timers("checking_orientation"):
279
+ if nodes[id].orient == "<":
280
+ orient = orient_switch[orient]
281
+ with timers("write_temp_walk"):
282
+ tmp_walk_file.write(f"{orient}{id}")
283
+ so = so + nodes[id].ln
284
+ with timers("write_temp_walk"):
285
+ tmp_walk_file.write("\n")
286
+ opened_file.close()
287
+
288
+
289
+ # writing the rGFA file.
290
+ def write_rGFA(gfa, nodes, writer):
291
+ stats_counter = {"rank-0 nodes": 0}
292
+ reader_gzipped = False
293
+ reader = None
294
+ if gfa.endswith(".gz"):
295
+ reader = libcbgzf.BGZFile(gfa, "rb")
296
+ reader_gzipped = True
297
+ else:
298
+ reader = open(gfa, "r")
299
+ while True:
300
+ line = reader.readline().decode("utf-8") if reader_gzipped else reader.readline()
301
+ if not line:
302
+ break
303
+ if line.startswith("W"):
304
+ # writing the W lines from the temporary file.
305
+ continue
306
+ if line.startswith("S"):
307
+ with timers("write_s_lines"):
308
+ id = line.split("\t")[1]
309
+ node = nodes[id]
310
+ if isinstance(node, Node):
311
+ # no tags have been assigned to this node
312
+ node = OutputNode(ln=node.ln, sn="unknown", so=0, sr=-1)
313
+ stats_counter["unknown nodes"] += 1
314
+ stats_counter["rank-0 nodes"] += 1 if node.sr == 0 else 0
315
+ _, id, seq = line.strip().split("\t")[0:3]
316
+ if node.orient == "<":
317
+ seq = revcomp(seq)
318
+ new_line = f"S\t{id}\t{seq}"
319
+ new_line += node.tags_to_string()
320
+ new_line += "\n"
321
+ writer.write(new_line)
322
+ elif line.startswith("L"):
323
+ with timers("write_l_lines"):
324
+ _, n1, o1, n2, o2, overlap = line.strip().split("\t")
325
+ if nodes[n1].orient == "<":
326
+ o1 = orient_switch[o1]
327
+ if nodes[n2].orient == "<":
328
+ o2 = orient_switch[o2]
329
+ if o1 == o2 and o1 == "-":
330
+ writer.write(f"L\t{n2}\t+\t{n1}\t+\t{overlap}\n")
331
+ else:
332
+ writer.write(f"L\t{n1}\t{o1}\t{n2}\t{o2}\t{overlap}\n")
333
+ else:
334
+ writer.write(line)
335
+ reader.close()
336
+ return stats_counter
337
+
338
+
339
+ def main(args):
340
+ run(**vars(args))