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/.gitignore
ADDED
gaftools/__init__.py
ADDED
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)
|
gaftools/cli/__init__.py
ADDED
|
@@ -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))
|
gaftools/cli/gfa2rgfa.py
ADDED
|
@@ -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))
|