GraDiv 0.1.0__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.
- gradiv/__init__.py +7 -0
- gradiv/_version.py +21 -0
- gradiv/algo.py +163 -0
- gradiv/functions.py +91 -0
- gradiv/main.py +241 -0
- gradiv/pggraph.py +361 -0
- gradiv/polymorphism.py +49 -0
- gradiv/tests.py +17 -0
- gradiv-0.1.0.dist-info/METADATA +763 -0
- gradiv-0.1.0.dist-info/RECORD +14 -0
- gradiv-0.1.0.dist-info/WHEEL +5 -0
- gradiv-0.1.0.dist-info/entry_points.txt +5 -0
- gradiv-0.1.0.dist-info/licenses/LICENSE +674 -0
- gradiv-0.1.0.dist-info/top_level.txt +1 -0
gradiv/__init__.py
ADDED
gradiv/_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 = '0.1.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
gradiv/algo.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import gradiv.pggraph as pggraph
|
|
2
|
+
import gradiv.polymorphism as polymorphism
|
|
3
|
+
from pybedtools import BedTool
|
|
4
|
+
import typing
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
def call_polymorphisms (graph_index:'pggraph.GraphIndex', id_table:bool=True, seq_table:bool=True) -> int :
|
|
8
|
+
"""
|
|
9
|
+
First version of the "child" algorithm. Detects polymorphisms in a pangenome graph and outputs them in CSV files.
|
|
10
|
+
:param graph_index: GraphIndex instance
|
|
11
|
+
:param id_table : Flag to output genotype table with node IDs
|
|
12
|
+
:param seq_table : Flag to output genotype table with sequences
|
|
13
|
+
:return: None
|
|
14
|
+
"""
|
|
15
|
+
logger = logging.getLogger("gradiv")
|
|
16
|
+
logger.debug("Calling polymorphism functions")
|
|
17
|
+
|
|
18
|
+
# output polymorphisms to a genotype table file
|
|
19
|
+
header = "previous_node"
|
|
20
|
+
for sample in graph_index.samples:
|
|
21
|
+
header += f',{sample}'
|
|
22
|
+
header += '\n'
|
|
23
|
+
f_id = None
|
|
24
|
+
f_seq = None
|
|
25
|
+
if id_table:
|
|
26
|
+
f_id = open(str(graph_index.id_table_path), 'w')
|
|
27
|
+
f_id.write(header)
|
|
28
|
+
if seq_table:
|
|
29
|
+
f_seq = open(str(graph_index.seq_table_path), 'w')
|
|
30
|
+
f_seq.write(header)
|
|
31
|
+
|
|
32
|
+
# scan the graph for atomic polymorphisms
|
|
33
|
+
nb_poly = 0
|
|
34
|
+
for segment in graph_index.bam_file_for_parsing :
|
|
35
|
+
# initialize variables
|
|
36
|
+
reversed_parent = None
|
|
37
|
+
is_reversed = False
|
|
38
|
+
|
|
39
|
+
parent = pggraph.Node(bam_segment=segment, sense=1)
|
|
40
|
+
parent.current_nodes = {parent.get_full_id(): parent}
|
|
41
|
+
# get all nodes implicated
|
|
42
|
+
links, forward_nodes, reverse_nodes, stop = parent.get_links(graph_index=graph_index)
|
|
43
|
+
if stop:
|
|
44
|
+
continue
|
|
45
|
+
nodes_both_senses = forward_nodes.intersection(reverse_nodes)
|
|
46
|
+
if parent.identifier in nodes_both_senses:
|
|
47
|
+
is_reversed = True
|
|
48
|
+
reversed_parent = pggraph.Node(bam_segment=segment, sense=-1)
|
|
49
|
+
reversed_parent.current_nodes = parent.current_nodes
|
|
50
|
+
parent.current_nodes[reversed_parent.get_full_id()] = reversed_parent
|
|
51
|
+
|
|
52
|
+
# create nodes
|
|
53
|
+
parent.create_nodes(nodes_id_set=forward_nodes, sense=1, graph_index=graph_index)
|
|
54
|
+
parent.create_nodes(nodes_id_set=reverse_nodes, sense=-1, graph_index=graph_index)
|
|
55
|
+
|
|
56
|
+
# resolve segments' samples when in both senses
|
|
57
|
+
for node_id in nodes_both_senses:
|
|
58
|
+
parent.current_nodes[f'+{node_id}'].samples, parent.current_nodes[f'-{node_id}'].samples = get_samples_per_sense(parent.current_nodes[f'+{node_id}'], graph_index=graph_index)
|
|
59
|
+
|
|
60
|
+
parent.add_children(links=links)
|
|
61
|
+
parent.add_parents(graph_index=graph_index)
|
|
62
|
+
|
|
63
|
+
# call polymorphisms
|
|
64
|
+
poly = call_polymorphism_from_parent(parent)
|
|
65
|
+
if poly.node_per_sample:
|
|
66
|
+
write_out_polymorphism(poly, parent, graph_index, f_id, f_seq)
|
|
67
|
+
nb_poly += 1
|
|
68
|
+
if is_reversed:
|
|
69
|
+
poly = call_polymorphism_from_parent(reversed_parent)
|
|
70
|
+
if poly.node_per_sample:
|
|
71
|
+
write_out_polymorphism(poly, reversed_parent, graph_index, f_id, f_seq)
|
|
72
|
+
nb_poly += 1
|
|
73
|
+
if f_id:
|
|
74
|
+
f_id.close()
|
|
75
|
+
if f_seq:
|
|
76
|
+
f_seq.close()
|
|
77
|
+
return nb_poly
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_samples_per_sense (node:pggraph.Node, graph_index:pggraph.GraphIndex) -> tuple[set, set] :
|
|
81
|
+
"""
|
|
82
|
+
Associate samples to the forward or reverse segment. Used only when both segments are present.
|
|
83
|
+
:param node: Node instance
|
|
84
|
+
:param graph_index: GraphIndex instance
|
|
85
|
+
:return: tuple of sets of samples (forward, reverse)
|
|
86
|
+
"""
|
|
87
|
+
segment_info = (set(), set())
|
|
88
|
+
for sample in node.samples:
|
|
89
|
+
bed_file = BedTool(str(graph_index.bed_files[sample]))
|
|
90
|
+
sample_node_presence = search_bed(node.identifier, bed_file)
|
|
91
|
+
if sample_node_presence[0] :
|
|
92
|
+
segment_info[0].add(sample)
|
|
93
|
+
elif sample_node_presence[1] :
|
|
94
|
+
segment_info[1].add(sample)
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError("Segment not found")
|
|
97
|
+
return segment_info
|
|
98
|
+
|
|
99
|
+
def search_bed (node_id:str, bed_file:BedTool) -> tuple[bool, bool] :
|
|
100
|
+
"""
|
|
101
|
+
Browse BED files and return presence or absence of forward and reverse given segment per sample
|
|
102
|
+
:param node_id: Full node ID (sign and ID)
|
|
103
|
+
:param bed_file: pybedtools BED file
|
|
104
|
+
:return: tuple of booleans (forward, reverse)
|
|
105
|
+
"""
|
|
106
|
+
presence_forward = False
|
|
107
|
+
presence_reverse = False
|
|
108
|
+
for interval in bed_file:
|
|
109
|
+
if len(interval.fields) >= 4:
|
|
110
|
+
segment_id = interval.name
|
|
111
|
+
if segment_id[1:] == node_id:
|
|
112
|
+
if segment_id[0] == '+':
|
|
113
|
+
presence_forward = True
|
|
114
|
+
else:
|
|
115
|
+
presence_reverse = True
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError("Incorrect BED format")
|
|
118
|
+
if presence_reverse and presence_forward:
|
|
119
|
+
return presence_forward, presence_reverse
|
|
120
|
+
return presence_forward, presence_reverse
|
|
121
|
+
|
|
122
|
+
def call_polymorphism_from_parent(parent:pggraph.Node) -> polymorphism.Polymorphism :
|
|
123
|
+
"""
|
|
124
|
+
Call potential polymorphism right after parent node.
|
|
125
|
+
:param parent: Node instance
|
|
126
|
+
:return: Polymorphism instance
|
|
127
|
+
"""
|
|
128
|
+
poly = polymorphism.Polymorphism()
|
|
129
|
+
grand_children = parent.get_grand_children()
|
|
130
|
+
for gc_id in grand_children:
|
|
131
|
+
substitution_nodes = parent.current_nodes[gc_id].parents.intersection(parent.children)
|
|
132
|
+
if len(substitution_nodes) > 1:
|
|
133
|
+
poly.add_substitution(parent, substitution_nodes, parent.current_nodes[gc_id])
|
|
134
|
+
if gc_id in parent.children:
|
|
135
|
+
poly.add_deletion(parent, parent.current_nodes[gc_id])
|
|
136
|
+
if len(substitution_nodes) == 1:
|
|
137
|
+
poly.add_substitution(parent, substitution_nodes, parent.current_nodes[gc_id])
|
|
138
|
+
return poly
|
|
139
|
+
|
|
140
|
+
def write_out_polymorphism(poly:polymorphism.Polymorphism, parent:pggraph.Node, graph_index:pggraph.GraphIndex, f_id:typing.TextIO=None, f_seq:typing.TextIO=None) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Write a single polymorphism to genotype tables.
|
|
143
|
+
:param poly: Polymorphism instance
|
|
144
|
+
:param parent: Node instance associated with the polymorphism
|
|
145
|
+
:param graph_index: GraphIndex instance
|
|
146
|
+
:param f_id: Flag to output genotype table with node IDs
|
|
147
|
+
:param f_seq: Flag to output genotype table with sequences
|
|
148
|
+
:return: None
|
|
149
|
+
"""
|
|
150
|
+
poly.add_undefined_values(graph_index.samples)
|
|
151
|
+
if f_id:
|
|
152
|
+
f_id.write(parent.get_full_id())
|
|
153
|
+
for sample in graph_index.samples:
|
|
154
|
+
f_id.write(f',{poly.node_per_sample[sample]}')
|
|
155
|
+
f_id.write('\n')
|
|
156
|
+
if f_seq:
|
|
157
|
+
f_seq.write(parent.get_full_id())
|
|
158
|
+
for sample in graph_index.samples:
|
|
159
|
+
if poly.node_per_sample[sample] in ["?", "*"]:
|
|
160
|
+
f_seq.write(f',{poly.node_per_sample[sample]}')
|
|
161
|
+
else:
|
|
162
|
+
f_seq.write(f',{parent.current_nodes[poly.node_per_sample[sample]].sequence}')
|
|
163
|
+
f_seq.write('\n')
|
gradiv/functions.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from sys import argv
|
|
2
|
+
from cloup import HelpFormatter, HelpTheme, Style, Context
|
|
3
|
+
import logging
|
|
4
|
+
from rich.logging import RichHandler
|
|
5
|
+
from rich.traceback import install
|
|
6
|
+
from gradiv.__init__ import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# context settings for gradiv command
|
|
10
|
+
CONTEXT_SETTINGS = Context.settings(
|
|
11
|
+
help_option_names=('-h', '--help'),
|
|
12
|
+
ignore_unknown_options=False,
|
|
13
|
+
align_option_groups=True,
|
|
14
|
+
align_sections=True,
|
|
15
|
+
show_constraints=True,
|
|
16
|
+
formatter_settings=HelpFormatter.settings(
|
|
17
|
+
max_width=120,
|
|
18
|
+
col1_max_width=40,
|
|
19
|
+
col2_min_width=60,
|
|
20
|
+
indent_increment=2,
|
|
21
|
+
col_spacing=1,
|
|
22
|
+
row_sep=None,
|
|
23
|
+
theme=HelpTheme.dark().with_(
|
|
24
|
+
invoked_command=Style(fg='bright_yellow'),
|
|
25
|
+
command_help=Style(fg='bright_cyan'),
|
|
26
|
+
heading=Style(fg='green', bold=True),
|
|
27
|
+
constraint=Style(fg='magenta'),
|
|
28
|
+
section_help=Style(fg='red'),
|
|
29
|
+
col1=Style(fg='bright_cyan'),
|
|
30
|
+
col2=Style(fg='white'),
|
|
31
|
+
epilog=Style(fg="bright_white", italic=True)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def init_log(verbosity:str="INFO") -> logging.Logger:
|
|
37
|
+
"""
|
|
38
|
+
Initialize the logger and configure console and file output.
|
|
39
|
+
:param str verbosity: Logging verbosity level.
|
|
40
|
+
:return: Logger object.
|
|
41
|
+
"""
|
|
42
|
+
verbosity = verbosity.upper()
|
|
43
|
+
debug = False
|
|
44
|
+
if verbosity == 'DEBUG':
|
|
45
|
+
debug = True
|
|
46
|
+
install(show_locals=True)
|
|
47
|
+
|
|
48
|
+
# create console handler
|
|
49
|
+
rich_handler = RichHandler(
|
|
50
|
+
rich_tracebacks=True if debug else False,
|
|
51
|
+
markup=True,
|
|
52
|
+
show_time=False,
|
|
53
|
+
show_level=False,
|
|
54
|
+
show_path=True if debug else False,
|
|
55
|
+
# console=shared_console
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# create file handler (a: append / w: write)
|
|
59
|
+
file_handler = logging.FileHandler('gradiv.log', mode='w', encoding='utf-8')
|
|
60
|
+
file_handler.setLevel(logging.DEBUG) # level debug and above
|
|
61
|
+
|
|
62
|
+
# set format
|
|
63
|
+
formatter = logging.Formatter(
|
|
64
|
+
fmt=f'%(asctime)s | {"%(name)-8s | %(funcName)-22s |" if debug else ""} %(levelname)-8s | %(message)s',
|
|
65
|
+
datefmt='%m-%d %H:%M'
|
|
66
|
+
)
|
|
67
|
+
file_handler.setFormatter(formatter)
|
|
68
|
+
rich_handler.setFormatter(formatter)
|
|
69
|
+
|
|
70
|
+
# initialize logger and set verbosity level
|
|
71
|
+
logger = logging.getLogger("gradiv")
|
|
72
|
+
logger.setLevel(getattr(logging, verbosity))
|
|
73
|
+
|
|
74
|
+
if not logger.handlers:
|
|
75
|
+
logger.addHandler(file_handler)
|
|
76
|
+
logger.addHandler(rich_handler)
|
|
77
|
+
|
|
78
|
+
logger.info("Starting GraDiv")
|
|
79
|
+
logger.info(f"GraDiv version: {__version__}")
|
|
80
|
+
logger.info(f"Verbosity level: {verbosity}")
|
|
81
|
+
logger.info(f"Command line run: {' '.join(argv)}")
|
|
82
|
+
return logger
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_stat_output(x) -> str:
|
|
87
|
+
"""Format statistics in output file for gradiv compute."""
|
|
88
|
+
if isinstance(x, float): return format(x, '.2f')
|
|
89
|
+
if x is None: return 'NA'
|
|
90
|
+
if isinstance(x, (list, tuple)): return f'({" ".join(map(format_stat_output, x))})'
|
|
91
|
+
return str(x)
|
gradiv/main.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from cloup import group, option, command, option_group
|
|
3
|
+
from cloup.constraints import ErrorFmt, RequireExactly
|
|
4
|
+
import egglib
|
|
5
|
+
from time import time
|
|
6
|
+
from gradiv.__init__ import __version__
|
|
7
|
+
import gradiv.algo as algo
|
|
8
|
+
import gradiv.pggraph as pggraph
|
|
9
|
+
from gradiv.functions import *
|
|
10
|
+
|
|
11
|
+
start_time = time()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# OPTION GROUPS
|
|
16
|
+
|
|
17
|
+
common_options = [
|
|
18
|
+
option("-v", "--verbosity", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False), default="INFO"),
|
|
19
|
+
option("-g", "--gratools-index", type=click.Path(exists=True, file_okay=False), required=True, help="Path to GraTools index"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
stat_options = RequireExactly(1).rephrased(
|
|
23
|
+
help="""The compute command needs a list of statistics to compute. One of those options has to be provided.""",
|
|
24
|
+
error=f'Exactly one statistic parameter must be provided:\n{ErrorFmt.param_list}'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# GRADIV COMMANDS
|
|
30
|
+
|
|
31
|
+
@group("gradiv",
|
|
32
|
+
help="A tool for diversity analysis of pangenome graphs",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
invoke_without_command=True,
|
|
35
|
+
context_settings=CONTEXT_SETTINGS)
|
|
36
|
+
@click.version_option(__version__, "-v", "--version", message="%(prog)s, version %(version)s")
|
|
37
|
+
def main_command(verbosity:str="INFO"):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@command("call",
|
|
42
|
+
short_help="Call polymorphisms from a GraTools pangenome graph index",
|
|
43
|
+
help="Call polymorphisms from a GraTools index and output them as genotype tables in a GraDiv directory",
|
|
44
|
+
no_args_is_help=True)
|
|
45
|
+
@option_group("call options", *common_options)
|
|
46
|
+
def call(gratools_index, verbosity:str="INFO"):
|
|
47
|
+
"""Generate genotype tables from a GraTools index and outputs them in a GraDiv directory"""
|
|
48
|
+
logger = init_log(verbosity)
|
|
49
|
+
|
|
50
|
+
gfa_index = pggraph.GraphIndex(gratools_index)
|
|
51
|
+
gfa_index.make_gradiv_directory()
|
|
52
|
+
gfa_index.make_bam_index_on_reads()
|
|
53
|
+
gfa_index.open_bam_for_parsing()
|
|
54
|
+
|
|
55
|
+
logger.info(f'Calling polymorphisms from "{gfa_index.gratools_directory}"')
|
|
56
|
+
logger.info(f'Output directory : {gfa_index.gradiv_directory}')
|
|
57
|
+
|
|
58
|
+
# call polymorphisms
|
|
59
|
+
nb_poly = algo.call_polymorphisms(graph_index=gfa_index, id_table=True, seq_table=True)
|
|
60
|
+
|
|
61
|
+
execution_time = time() - start_time
|
|
62
|
+
logger.info(f'Found {nb_poly} polymorphisms')
|
|
63
|
+
logger.info(f'Executed in {execution_time:.4f} seconds')
|
|
64
|
+
|
|
65
|
+
gfa_index.close()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@command("compute",
|
|
69
|
+
short_help="Compute statistics from a genotype table",
|
|
70
|
+
help="""Compute given statistics from a GraTools index and a genotype table generated with gradiv call.
|
|
71
|
+
To know which statistics ans options to provide, check out EggLib's documentation : https://www.egglib.org/.
|
|
72
|
+
Analysis can be run on a specific region (query sample will be required).
|
|
73
|
+
For more information, see GraDiv's documentation : https://forge.ird.fr/phim/gradiv.
|
|
74
|
+
""",
|
|
75
|
+
no_args_is_help=True,
|
|
76
|
+
show_constraints=True)
|
|
77
|
+
@option_group("compute options",
|
|
78
|
+
*common_options,
|
|
79
|
+
stat_options(
|
|
80
|
+
option("--stat-list", type=str, required=False, help="Comma separated list of statistics to compute"),
|
|
81
|
+
option("--stat-type", type=str, required=False, help="Type of statistic to compute (can be a comma-separated list)"),
|
|
82
|
+
option("--stat-file", type=click.Path(exists=True), required=False, help="Path to file providing statistics to compute")
|
|
83
|
+
),
|
|
84
|
+
option("-q", "--query", type=str, required=False, help="Sample name of query sequence to use as reference coordinates"),
|
|
85
|
+
option("-c", "--chrom", type=str, required=False, help="Chromosome to use as reference coordinates"),
|
|
86
|
+
option("-s", "--start-position", type=click.IntRange(min=0), required=False, help="Start position of specific region to compute statistics on"),
|
|
87
|
+
option("-e", "--end-position", type=click.IntRange(min=0), required=False, help="End position of specific region to compute statistics on"),
|
|
88
|
+
option("--structure", type=click.Path(exists=True), required=False, help="Path to file providing sample structure"),
|
|
89
|
+
option("--max-missing", type=int, required=False, help="Ignore polymorphisms with more missing data than this threshold"),
|
|
90
|
+
option("--maf", type=float, default=0, required=False, help="Ignore polymorphisms where the relative minority allele frequency is below this threshold"),
|
|
91
|
+
option("--only-diallelic", is_flag=True, required=False, help="Ignore polymorphisms with more than two alleles"))
|
|
92
|
+
def compute(gratools_index, query: str = None, chrom: str = None, start_position: int = None, end_position: int = None,
|
|
93
|
+
structure=None, stat_list: str = None, stat_type: str = None,
|
|
94
|
+
stat_file=None, max_missing: int = None, maf: float = 0, only_diallelic: bool = False,
|
|
95
|
+
verbosity: str = "INFO"):
|
|
96
|
+
"""Compute statistics from a GraDiv directory."""
|
|
97
|
+
logger = init_log(verbosity)
|
|
98
|
+
|
|
99
|
+
gfa_index = pggraph.GraphIndex(gratools_index)
|
|
100
|
+
gfa_index.make_bam_index_on_reads()
|
|
101
|
+
|
|
102
|
+
logger.info(f'Computing statistics from {gfa_index.id_table_path}')
|
|
103
|
+
logger.info(f'Output file : {gfa_index.stats_path}')
|
|
104
|
+
|
|
105
|
+
# open genotype table
|
|
106
|
+
f = open(str(gfa_index.id_table_path), "r")
|
|
107
|
+
f.readline()
|
|
108
|
+
|
|
109
|
+
# configure ComputeStats
|
|
110
|
+
cs = egglib.stats.ComputeStats(multi=True, multi_hits=False)
|
|
111
|
+
cs.configure(maf=maf)
|
|
112
|
+
|
|
113
|
+
# get structure
|
|
114
|
+
if structure:
|
|
115
|
+
structure_file = open(structure, "r")
|
|
116
|
+
ingroup_dict = {}
|
|
117
|
+
outgroup_dict = {}
|
|
118
|
+
samples = []
|
|
119
|
+
fields = structure_file.readline().strip().split(',')
|
|
120
|
+
field_to_index = {}
|
|
121
|
+
sample_index = 0
|
|
122
|
+
for i in range(len(fields)):
|
|
123
|
+
field_to_index[fields[i]] = i
|
|
124
|
+
|
|
125
|
+
legal_fields = {'cluster', 'population', 'outgroup'}
|
|
126
|
+
if fields[0] != 'sample':
|
|
127
|
+
logger.error('Structure file : First field must be "sample"')
|
|
128
|
+
raise ValueError('Structure file : First field must be "sample"')
|
|
129
|
+
for field in fields[1:]:
|
|
130
|
+
if field not in legal_fields:
|
|
131
|
+
logger.error(f'Structure file : "{field}" is not a valid field')
|
|
132
|
+
raise ValueError(f'Structure file : "{field}" is not a valid field')
|
|
133
|
+
|
|
134
|
+
for line in structure_file:
|
|
135
|
+
line = line.strip().split(',')
|
|
136
|
+
samples.append(line[0])
|
|
137
|
+
if 'outgroup' in fields and line[field_to_index['outgroup']] == "yes":
|
|
138
|
+
outgroup_dict[line[0]] = [sample_index] # list of a single element as only haploidy is supported
|
|
139
|
+
else:
|
|
140
|
+
if 'cluster' not in fields:
|
|
141
|
+
cluster = None
|
|
142
|
+
else:
|
|
143
|
+
cluster = line[field_to_index['cluster']]
|
|
144
|
+
if cluster not in ingroup_dict.keys():
|
|
145
|
+
ingroup_dict[cluster] = {}
|
|
146
|
+
if line[field_to_index['population']] not in ingroup_dict[cluster].keys():
|
|
147
|
+
ingroup_dict[cluster][line[field_to_index['population']]] = {}
|
|
148
|
+
ingroup_dict[cluster][line[field_to_index['population']]][line[0]] = [sample_index] # list of a single element as only haploidy is supported
|
|
149
|
+
sample_index += 1
|
|
150
|
+
|
|
151
|
+
# check if all samples are included
|
|
152
|
+
samples.sort()
|
|
153
|
+
if samples != gfa_index.samples:
|
|
154
|
+
logging.error('Structure file : incorrect sample names or missing samples')
|
|
155
|
+
raise ValueError('Structure file : incorrect sample names or missing samples')
|
|
156
|
+
|
|
157
|
+
struct = egglib.Structure()
|
|
158
|
+
struct.from_dict(ingroup_dict, outgroup=outgroup_dict)
|
|
159
|
+
cs.configure(struct=struct)
|
|
160
|
+
|
|
161
|
+
# passing statistics to compute
|
|
162
|
+
if stat_list :
|
|
163
|
+
cs.add_stats(*stat_list.split(","))
|
|
164
|
+
elif stat_type :
|
|
165
|
+
cs.add_stats(*[f"+{item}" for item in stat_type.split(",")])
|
|
166
|
+
elif stat_file :
|
|
167
|
+
open_stat_file = open(str(stat_file), "r")
|
|
168
|
+
stat_list_from_file = open_stat_file.readline().strip().split(",")
|
|
169
|
+
cs.add_stats(*stat_list_from_file)
|
|
170
|
+
open_stat_file.close()
|
|
171
|
+
#TODO: add debug : computed stats
|
|
172
|
+
|
|
173
|
+
by_position = False
|
|
174
|
+
if query and chrom:
|
|
175
|
+
by_position = True
|
|
176
|
+
logger.debug('Computing statistics by position')
|
|
177
|
+
else:
|
|
178
|
+
if query or chrom or start_position or end_position:
|
|
179
|
+
logger.error('Both query and chrom options are needed to compute statistics by region.')
|
|
180
|
+
raise ValueError('Both query and chrom options are needed to compute statistics by region.')
|
|
181
|
+
|
|
182
|
+
# compute stats
|
|
183
|
+
for line in f:
|
|
184
|
+
anchor_node = line.rstrip().split(',')[0]
|
|
185
|
+
allele_position = None
|
|
186
|
+
if by_position:
|
|
187
|
+
allele_position = gfa_index.get_site_position(node_id=anchor_node, query=query, chrom=chrom)
|
|
188
|
+
if allele_position == -1:
|
|
189
|
+
continue
|
|
190
|
+
if start_position and end_position and (allele_position < start_position or allele_position > end_position):
|
|
191
|
+
continue
|
|
192
|
+
split_line = line.rstrip().split(',')[1:]
|
|
193
|
+
sequences = []
|
|
194
|
+
for part in split_line:
|
|
195
|
+
if part not in ["?", "*"] :
|
|
196
|
+
segment = gfa_index.query_segment(part[1:])
|
|
197
|
+
if part[0] == '+' :
|
|
198
|
+
sequences.append(segment.query_sequence)
|
|
199
|
+
else:
|
|
200
|
+
sequences.append(pggraph.Node.reverse_complement(segment.query_sequence))
|
|
201
|
+
else :
|
|
202
|
+
sequences.append(part)
|
|
203
|
+
alleles = set(sequences) #TODO: get polymorphism type from it (filter on type/length)
|
|
204
|
+
alleles.discard("?")
|
|
205
|
+
alph = egglib.Alphabet("string", list(alleles), ["?"])
|
|
206
|
+
site = egglib.site_from_list(sequences, alph)
|
|
207
|
+
if by_position:
|
|
208
|
+
site.position = allele_position
|
|
209
|
+
# filter out polymorphisms
|
|
210
|
+
ignore_site = False
|
|
211
|
+
if max_missing is not None and site.num_missing > max_missing:
|
|
212
|
+
ignore_site=True
|
|
213
|
+
if only_diallelic and alph.num_exploitable > 2 :
|
|
214
|
+
ignore_site=True
|
|
215
|
+
if not ignore_site:
|
|
216
|
+
cs.process_site(site)
|
|
217
|
+
logger.debug('Computing all statistics')
|
|
218
|
+
stats = cs.results()
|
|
219
|
+
logger.debug('Statistics computed')
|
|
220
|
+
|
|
221
|
+
# outputs stats in CSV file
|
|
222
|
+
stat_names = []
|
|
223
|
+
for stat_name in stats.keys():
|
|
224
|
+
stat_names.append(stat_name)
|
|
225
|
+
|
|
226
|
+
canon_stat_order = [key for grp in ('site', 'unphased', 'phased', 'allelesize') for key in cs.stats_group(grp)]
|
|
227
|
+
stat_names.sort(key=canon_stat_order.index)
|
|
228
|
+
|
|
229
|
+
f_stats = open(str(gfa_index.stats_path), 'w')
|
|
230
|
+
f_stats.write("stat_name,value\n")
|
|
231
|
+
for stat_name in stat_names:
|
|
232
|
+
f_stats.write(f'{stat_name},{format_stat_output(stats[stat_name])}\n')
|
|
233
|
+
f_stats.close()
|
|
234
|
+
|
|
235
|
+
execution_time = time() - start_time
|
|
236
|
+
logger.info(f'Executed in {execution_time:.4f} seconds')
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# add commands to main
|
|
240
|
+
main_command.add_command(call)
|
|
241
|
+
main_command.add_command(compute)
|