PythiaLabelGenerator 1.0.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.
- labelgenerator/__init__.py +15 -0
- labelgenerator/iqtree.py +142 -0
- labelgenerator/iqtree_parser.py +183 -0
- labelgenerator/label.py +231 -0
- labelgenerator/logger.py +32 -0
- labelgenerator/main.py +176 -0
- labelgenerator/raxmlng.py +173 -0
- pythialabelgenerator-1.0.0.dist-info/METADATA +215 -0
- pythialabelgenerator-1.0.0.dist-info/RECORD +12 -0
- pythialabelgenerator-1.0.0.dist-info/WHEEL +4 -0
- pythialabelgenerator-1.0.0.dist-info/entry_points.txt +2 -0
- pythialabelgenerator-1.0.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = importlib.metadata.distribution(__name__).version
|
|
5
|
+
except Exception:
|
|
6
|
+
__version__ = "unknown"
|
|
7
|
+
|
|
8
|
+
# Required if the package was installed via PyPi...
|
|
9
|
+
if __version__ == "unknown":
|
|
10
|
+
try:
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
__version__ = version("PythiaLabelGenerator")
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
labelgenerator/iqtree.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pypythia.custom_types import DataType
|
|
6
|
+
|
|
7
|
+
from labelgenerator.iqtree_parser import get_iqtree_results
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _iqtree_results_exist_and_done(prefix: pathlib.Path) -> bool:
|
|
11
|
+
iqtree_file = pathlib.Path(f"{prefix}.iqtree")
|
|
12
|
+
logfile = pathlib.Path(f"{prefix}.log")
|
|
13
|
+
|
|
14
|
+
if not iqtree_file.exists() or not logfile.exists():
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
"Total wall-clock time used" in iqtree_file.read_text()
|
|
19
|
+
and "Date and Time:" in logfile.read_text()
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_iqtree_model(data_type: DataType) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Get the IQ-TREE model for the given data type.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
data_type (DataType): The data type to get the model for.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The IQ-TREE model for the given data type.
|
|
32
|
+
For DNA data: GTR+G4+FO
|
|
33
|
+
For AA data: LG+G4+FO
|
|
34
|
+
For morphological data: MK
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
return {
|
|
38
|
+
DataType.DNA: "GTR+G4+FO",
|
|
39
|
+
DataType.AA: "LG+G4+FO",
|
|
40
|
+
DataType.MORPH: "MK",
|
|
41
|
+
}[data_type]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_statstests(
|
|
45
|
+
msa: pathlib.Path,
|
|
46
|
+
ml_trees: pathlib.Path,
|
|
47
|
+
best_tree: pathlib.Path,
|
|
48
|
+
iqtree: pathlib.Path,
|
|
49
|
+
model: str,
|
|
50
|
+
prefix: pathlib.Path,
|
|
51
|
+
seed: int = 0,
|
|
52
|
+
threads: Optional[int] = None,
|
|
53
|
+
is_morph: bool = False,
|
|
54
|
+
redo: bool = False,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Run IQ-TREE statistical tests on the given set of ML trees. Will run all available tests (bp-RELL, KH (+weighted), SH (+weighted), ELW, AU) using 10,000 RELL bootstrap replicates.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
msa (pathlib.Path): Path to the MSA file for which the ML trees were inferred.
|
|
61
|
+
ml_trees (pathlib.Path): Path to the file containing the ML trees.
|
|
62
|
+
best_tree (pathlib.Path): Path to the best ML tree.
|
|
63
|
+
iqtree (pathlib.Path): Path to the IQ-TREE executable.
|
|
64
|
+
model (str): The model to use for IQ-TREE.
|
|
65
|
+
prefix (pathlib.Path): Prefix for the output files.
|
|
66
|
+
seed (int): Seed for the random number generator. Defaults to 0.
|
|
67
|
+
threads (Optional[int]): Number of threads to use for IQ-TREE. Defaults to None. In this case, uses the IQ-TREE autoconfiguration.
|
|
68
|
+
is_morph (bool): Whether the data type is morphological. Defaults to False.
|
|
69
|
+
redo (bool): Whether to redo the analysis even if the results already exist. Defaults to False.
|
|
70
|
+
"""
|
|
71
|
+
if not redo and _iqtree_results_exist_and_done(prefix):
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
cmd = [
|
|
75
|
+
iqtree,
|
|
76
|
+
"-s",
|
|
77
|
+
msa,
|
|
78
|
+
"-m",
|
|
79
|
+
model,
|
|
80
|
+
"-pre",
|
|
81
|
+
prefix,
|
|
82
|
+
"-z",
|
|
83
|
+
ml_trees,
|
|
84
|
+
"-treediff",
|
|
85
|
+
"-te",
|
|
86
|
+
best_tree,
|
|
87
|
+
"-n",
|
|
88
|
+
0,
|
|
89
|
+
"-zb",
|
|
90
|
+
10000,
|
|
91
|
+
"-zw",
|
|
92
|
+
"-au",
|
|
93
|
+
"-seed",
|
|
94
|
+
seed,
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
if is_morph:
|
|
98
|
+
cmd.extend(["-st", "MORPH"])
|
|
99
|
+
|
|
100
|
+
if threads is not None:
|
|
101
|
+
cmd.extend(["-nt", threads])
|
|
102
|
+
|
|
103
|
+
if redo:
|
|
104
|
+
cmd.append("-redo")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
subprocess.check_output(list(map(str, cmd)), encoding="utf-8")
|
|
108
|
+
except subprocess.CalledProcessError as e:
|
|
109
|
+
raise RuntimeError(f"Running IQ-TREE command failed: {e.stdout}")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise RuntimeError("Running IQ-TREE command failed.") from e
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def filter_plausible_trees(
|
|
115
|
+
ml_trees: pathlib.Path,
|
|
116
|
+
iqtree_results: pathlib.Path,
|
|
117
|
+
plausible_ml_trees: pathlib.Path,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Filter the plausible ML trees based on the IQ-TREE results. A tree is plausible if all statistical tests (bp-RELL, KH (+weighted), SH (+weighted), ELW, AU) are significant.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
ml_trees (pathlib.Path): Path to the file containing the ML trees the IQ-TREE were performed for.
|
|
124
|
+
iqtree_results (pathlib.Path): Path to the IQ-TREE results file.
|
|
125
|
+
plausible_ml_trees (pathlib.Path): Path to the file to write the plausible ML trees to.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If the number of IQ-TREE results does not match the number of ML trees.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
iqtree_results = get_iqtree_results(iqtree_results)
|
|
132
|
+
newick_trees = [t.strip() for t in ml_trees.read_text().splitlines()]
|
|
133
|
+
|
|
134
|
+
if not len(iqtree_results) == len(newick_trees):
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"Number of IQ-TREE results does not match the number of ML trees."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
plausible_indices = [i for i, x in enumerate(iqtree_results) if x["plausible"]]
|
|
140
|
+
plausible_trees = [newick_trees[i] for i in plausible_indices]
|
|
141
|
+
|
|
142
|
+
plausible_ml_trees.write_text("\n".join(plausible_trees))
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import regex
|
|
4
|
+
|
|
5
|
+
# define some regex stuff
|
|
6
|
+
blanks = r"\s+" # matches >=1 subsequent whitespace characters
|
|
7
|
+
sign = r"[-+]?" # contains either a '-' or a '+' symbol or none of both
|
|
8
|
+
# matches ints or floats of forms '1.105' or '1.105e-5' or '1.105e5' or '1.105e+5'
|
|
9
|
+
float_re = r"\d+(?:\.\d+)?(?:[e][-+]?\d+)?"
|
|
10
|
+
|
|
11
|
+
tree_id_re = r"\d+" # tree ID is an int
|
|
12
|
+
llh_re = rf"{sign}{float_re}" # likelihood is a signed floating point
|
|
13
|
+
deltaL_re = rf"{sign}{float_re}" # deltaL is a signed floating point
|
|
14
|
+
# test result entry is of form '0.123 +'
|
|
15
|
+
test_result_re = rf"{float_re}{blanks}{sign}"
|
|
16
|
+
|
|
17
|
+
stat_test_name = r"[a-zA-Z-]+"
|
|
18
|
+
|
|
19
|
+
# table header is of form:
|
|
20
|
+
# Tree logL deltaL bp-RELL p-KH p-SH p-WKH p-WSH c-ELW p-AU
|
|
21
|
+
table_header = rf"Tree{blanks}logL{blanks}deltaL{blanks}(?:({stat_test_name})\s*)*"
|
|
22
|
+
table_header_re = regex.compile(table_header)
|
|
23
|
+
|
|
24
|
+
# a table entry in the .iqtree file looks for example like this:
|
|
25
|
+
# 5 -5708.931281 1.7785e-06 0.0051 - 0.498 + 0.987 + 0.498 + 0.987 + 0.05 + 0.453 +
|
|
26
|
+
table_entry = rf"({tree_id_re}){blanks}({llh_re}){blanks}({deltaL_re}){blanks}(?:({test_result_re})\s*)*"
|
|
27
|
+
table_entry_re = regex.compile(table_entry)
|
|
28
|
+
|
|
29
|
+
# if there is only a single plausible tree
|
|
30
|
+
# the line will look like this:
|
|
31
|
+
# 1 -88.9544627 0
|
|
32
|
+
table_entry_single_plausible_tree = (
|
|
33
|
+
rf"({tree_id_re}){blanks}({llh_re}){blanks}({deltaL_re})\s*"
|
|
34
|
+
)
|
|
35
|
+
table_entry_single_plausible_tree_re = regex.compile(table_entry_single_plausible_tree)
|
|
36
|
+
|
|
37
|
+
START_STRING = "USER TREES"
|
|
38
|
+
END_STRING = "TIME STAMP"
|
|
39
|
+
|
|
40
|
+
TEST_NAMES = ["bp-RELL", "p-KH", "p-SH", "p-WKH", "p-WSH", "c-ELW", "p-AU"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_relevant_section(input_file: pathlib.Path) -> list[str]:
|
|
44
|
+
"""
|
|
45
|
+
Returns the section between the START_STRING and END_STRING in the given file.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
input_file (pathlib.Path): Path to the .iqtree file to extract the section from.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A list of strings, each representing a line of the relevant section.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the section between START_STRING and END_STRING is empty.
|
|
55
|
+
"""
|
|
56
|
+
content = input_file.read_text().splitlines()
|
|
57
|
+
|
|
58
|
+
# now let's find the relevant lines
|
|
59
|
+
# the relevant lines are only between the start and end string
|
|
60
|
+
start = 0
|
|
61
|
+
end = 0
|
|
62
|
+
|
|
63
|
+
for i, line in enumerate(content):
|
|
64
|
+
if START_STRING in line:
|
|
65
|
+
start = i
|
|
66
|
+
if END_STRING in line:
|
|
67
|
+
end = i
|
|
68
|
+
|
|
69
|
+
if start == end:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"The section between START_STRING {START_STRING} and END_STRING {END_STRING} is empty. Please check the input file {input_file}."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return content[start:end]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_default_entry() -> dict:
|
|
78
|
+
"""
|
|
79
|
+
Returns a default entry for a single plausible tree.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A dict containing the default entry for a single plausible tree.
|
|
83
|
+
"""
|
|
84
|
+
return {
|
|
85
|
+
"plausible": 1,
|
|
86
|
+
"tests": {
|
|
87
|
+
"bp-RELL": {"score": 1, "significant": True},
|
|
88
|
+
"p-KH": {"score": 1, "significant": True},
|
|
89
|
+
"p-SH": {"score": 1, "significant": True},
|
|
90
|
+
"p-WKH": {"score": 1, "significant": True},
|
|
91
|
+
"p-WSH": {"score": 1, "significant": True},
|
|
92
|
+
"c-ELW": {"score": 1, "significant": True},
|
|
93
|
+
"p-AU": {"score": 1, "significant": True},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _regex_group_to_test_results(raw_results: list[str]) -> dict:
|
|
99
|
+
assert len(TEST_NAMES) == len(raw_results)
|
|
100
|
+
|
|
101
|
+
data = {"tests": {}}
|
|
102
|
+
num_passed = 0
|
|
103
|
+
|
|
104
|
+
for i, test in enumerate(TEST_NAMES):
|
|
105
|
+
test_result = raw_results[i]
|
|
106
|
+
score, significant = test_result.split(" ")
|
|
107
|
+
score = score.strip()
|
|
108
|
+
significant = significant.strip()
|
|
109
|
+
data["tests"][test] = {}
|
|
110
|
+
data["tests"][test]["score"] = float(score)
|
|
111
|
+
data["tests"][test]["significant"] = True if significant == "+" else False
|
|
112
|
+
|
|
113
|
+
if data["tests"][test]["significant"]:
|
|
114
|
+
num_passed += 1
|
|
115
|
+
|
|
116
|
+
data["plausible"] = num_passed == len(data["tests"].keys())
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_cleaned_table_entries(table_section: list[str]) -> list[dict]:
|
|
121
|
+
"""
|
|
122
|
+
Returns a list of dicts, each dict contains the iqtree test results for the respective tree.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
table_section (list[str]): A list of strings, each representing a line of the relevant section.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A list of dicts. Each dict contains the tree_id, llh, deltaL and all results of the performed
|
|
129
|
+
iqtree tests.
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
entries = []
|
|
133
|
+
for line in table_section:
|
|
134
|
+
line = line.strip()
|
|
135
|
+
# match the line against the regex defined above for a table entry
|
|
136
|
+
m = regex.match(table_entry_re, line)
|
|
137
|
+
|
|
138
|
+
# and match the line against the regex for a table entry in case of a single plausible tree
|
|
139
|
+
m_single_tree = regex.match(table_entry_single_plausible_tree_re, line)
|
|
140
|
+
|
|
141
|
+
if m:
|
|
142
|
+
# transform the raw results to a python dict
|
|
143
|
+
entry = _regex_group_to_test_results(m.captures(4))
|
|
144
|
+
entries.append(entry)
|
|
145
|
+
elif m_single_tree:
|
|
146
|
+
# if a match for a truncated table entry was found: we only have a single plausible tree
|
|
147
|
+
# => add the entry manually
|
|
148
|
+
entry = _get_default_entry()
|
|
149
|
+
entries.append(entry)
|
|
150
|
+
elif "= tree" in line:
|
|
151
|
+
# indicates that a tree is identical to one seen before
|
|
152
|
+
# => duplicate the results of this tree
|
|
153
|
+
_, id_of_identical_tree = line.rsplit(" ", 1)
|
|
154
|
+
id_of_identical_tree = int(id_of_identical_tree)
|
|
155
|
+
|
|
156
|
+
# IQ-Tree reports the results 1-indexed
|
|
157
|
+
# => to get the correct results we need to subtract one and access the entries
|
|
158
|
+
entry = entries[id_of_identical_tree - 1].copy()
|
|
159
|
+
entries.append(entry)
|
|
160
|
+
|
|
161
|
+
if not entries:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"No line in the given section matches the regex. Compare the regex and the given section. "
|
|
164
|
+
"Maybe the format has changed."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return entries
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_iqtree_results(iqtree_file: pathlib.Path) -> list[dict]:
|
|
171
|
+
"""
|
|
172
|
+
Returns a list of dicts, each dict contains the iqtree test results for the respective tree.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
iqtree_file (pathlib.Path): Path to the .iqtree file to extract the results from.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A list of dicts. Each dict contains the tree_id, llh, deltaL and all results of the performed
|
|
179
|
+
iqtree tests.
|
|
180
|
+
"""
|
|
181
|
+
section = get_relevant_section(iqtree_file)
|
|
182
|
+
entries = get_cleaned_table_entries(section)
|
|
183
|
+
return entries
|
labelgenerator/label.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pypythia.msa import parse_msa
|
|
5
|
+
|
|
6
|
+
from labelgenerator.iqtree import (
|
|
7
|
+
filter_plausible_trees,
|
|
8
|
+
get_iqtree_model,
|
|
9
|
+
run_statstests,
|
|
10
|
+
)
|
|
11
|
+
from labelgenerator.logger import log_runtime_information
|
|
12
|
+
from labelgenerator.raxmlng import infer_ml_trees, rf_distance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_label(
|
|
16
|
+
n_all: int,
|
|
17
|
+
rf_all: float,
|
|
18
|
+
n_unique_all: int,
|
|
19
|
+
n_plausible: int,
|
|
20
|
+
rf_plausible: float,
|
|
21
|
+
n_unique_plausible: int,
|
|
22
|
+
) -> float:
|
|
23
|
+
r"""
|
|
24
|
+
Compute the ground truth difficulty label for the given input values.
|
|
25
|
+
|
|
26
|
+
The ground-truth difficulty is computed according to our definition published in [Haag _et al._ (2022)](https://doi.org/10.1093/molbev/msac254):
|
|
27
|
+
|
|
28
|
+
Let $N_{\text{all}}$ be the number of inferred ML trees.
|
|
29
|
+
We first compute the average pairwise relative Robinson-Foulds (RF) distance between all trees ($RF_{\text{all}}$), as
|
|
30
|
+
well as the number of unique tree topologies among the inferred trees ($N^*_{\text{all}}$).
|
|
31
|
+
We filter the inferred trees using likelihood-based statistical tests to obtain the set of $N_{\text{pl}}$ _plausible
|
|
32
|
+
trees_.
|
|
33
|
+
We again compute the average pairwise RF distance between the plausible trees ($RF_{\text{pl}}$) and the number of
|
|
34
|
+
unique tree topologies among the plausible trees ($N^*_{\text{pl}}$).
|
|
35
|
+
|
|
36
|
+
The difficulty is then computed as follows:
|
|
37
|
+
|
|
38
|
+
$$
|
|
39
|
+
\text{difficulty} = \frac{1}{5} \cdot \bigg[ RF_{\text{all}} + RF_{\text{pl}}
|
|
40
|
+
+ \frac{N^*_{\text{all}}}{N_{\text{all}}} + \frac{N^*_{\text{pl}}}{N_{\text{pl}}}
|
|
41
|
+
+ \left( 1 - \frac{N_{\text{pl}}}{N_{\text{all}}} \right) \bigg]
|
|
42
|
+
$$
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
n_all (int): Number of inferred ML trees.
|
|
46
|
+
rf_all (float): Average pairwise RF distance between all trees.
|
|
47
|
+
n_unique_all (int): Number of unique tree topologies among the inferred trees.
|
|
48
|
+
n_plausible (int): Number of plausible trees.
|
|
49
|
+
rf_plausible (float): Average pairwise RF distance between the plausible trees.
|
|
50
|
+
n_unique_plausible (int): Number of unique tree topologies among the plausible trees.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
float: Ground truth difficulty label.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If the input values are not within the expected range.
|
|
57
|
+
- Number of unique trees is higher than the total number of trees.
|
|
58
|
+
- Number of unique plausible trees is higher than the number of plausible trees.
|
|
59
|
+
- Number of plausible trees is higher than the total number of trees.
|
|
60
|
+
- RF distance for all trees is not between 0 and 1.
|
|
61
|
+
- RF distance for plausible trees is not between 0 and 1.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
if n_unique_all > n_all:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"Number of unique trees cannot be higher than the total number of trees."
|
|
67
|
+
)
|
|
68
|
+
if n_unique_plausible > n_plausible:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Number of unique plausible trees cannot be higher than the number of plausible trees."
|
|
71
|
+
)
|
|
72
|
+
if n_plausible > n_all:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"Number of plausible trees cannot be higher than the total number of trees."
|
|
75
|
+
)
|
|
76
|
+
if not 0 <= rf_all <= 1:
|
|
77
|
+
raise ValueError("RF distance for all trees must be between 0 and 1.")
|
|
78
|
+
if not 0 <= rf_plausible <= 1:
|
|
79
|
+
raise ValueError("RF distance for plausible trees must be between 0 and 1.")
|
|
80
|
+
|
|
81
|
+
proportion_unique_all = n_unique_all / n_all
|
|
82
|
+
proportion_unique_plausible = n_unique_plausible / n_plausible
|
|
83
|
+
proportion_plausible = n_plausible / n_all
|
|
84
|
+
|
|
85
|
+
total = (
|
|
86
|
+
rf_all
|
|
87
|
+
+ proportion_unique_all
|
|
88
|
+
+ rf_plausible
|
|
89
|
+
+ proportion_unique_plausible
|
|
90
|
+
+ (1 - proportion_plausible)
|
|
91
|
+
)
|
|
92
|
+
label = total / 5
|
|
93
|
+
|
|
94
|
+
eps = 1e-9
|
|
95
|
+
assert -eps <= label <= 1 + eps, (
|
|
96
|
+
f"Label {label} is not between 0 and 1. Check the input values."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return label
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def compute_label(
|
|
103
|
+
msa_file: pathlib.Path,
|
|
104
|
+
raxmlng: pathlib.Path,
|
|
105
|
+
iqtree: pathlib.Path,
|
|
106
|
+
prefix: pathlib.Path,
|
|
107
|
+
model: Optional[str] = None,
|
|
108
|
+
n_trees: int = 100,
|
|
109
|
+
seed: int = 0,
|
|
110
|
+
threads: Optional[int] = None,
|
|
111
|
+
redo: bool = False,
|
|
112
|
+
log_info: bool = True,
|
|
113
|
+
) -> float:
|
|
114
|
+
"""
|
|
115
|
+
Compute the ground truth difficulty label for the given input MSA by inferring ML trees and running statistical tests.
|
|
116
|
+
See `labelgenerator.label.get_label` for the definition of the ground truth difficulty.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
msa_file (pathlib.Path): Path to the MSA file to compute the label for. Can be either in FASTA or PHYLIP format.
|
|
121
|
+
raxmlng (pathlib.Path): Path to the RAxML-NG executable.
|
|
122
|
+
iqtree (pathlib.Path): Path to the IQ-TREE executable.
|
|
123
|
+
prefix (pathlib.Path): Prefix for the output files.
|
|
124
|
+
model (str, optional): Substitution model to use for the ML tree inference. Defaults to None. In this case, the
|
|
125
|
+
model is inferred from the MSA based on the data type (`GTR+G` for DNA, `LG+G` for AA, and `MULTIx_GTR` for morphological data).
|
|
126
|
+
n_trees (int, optional): Number of ML trees to infer. Defaults to 100. Please note that the computed label is only comparable to
|
|
127
|
+
Pythia predictions if 100 trees are inferred.
|
|
128
|
+
seed (int, optional): Seed for the random number generator. Defaults to 0.
|
|
129
|
+
threads (int, optional): Number of threads to use for the ML tree inference. Defaults to None. In this case, the RAxML-NG and IQ-TREE autoconfigs are used.
|
|
130
|
+
redo (bool, optional): If True, the computations are redone even if the output files already exist. Defaults to False.
|
|
131
|
+
log_info (bool, optional): If True, runtime information is logged. Defaults to True.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
float: The ground truth difficulty label for the given MSA.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
msa_obj = parse_msa(msa_file)
|
|
138
|
+
model = model or msa_obj.get_raxmlng_model()
|
|
139
|
+
|
|
140
|
+
# 1. Infer 100 ML trees for the given MSA using RAxML-NG
|
|
141
|
+
if log_info:
|
|
142
|
+
log_runtime_information(f"Inferring {n_trees} ML trees using RAxML-NG.")
|
|
143
|
+
|
|
144
|
+
infer_ml_trees(
|
|
145
|
+
msa=msa_file,
|
|
146
|
+
raxmlng=raxmlng,
|
|
147
|
+
model=model,
|
|
148
|
+
prefix=prefix,
|
|
149
|
+
n_trees=n_trees,
|
|
150
|
+
seed=seed,
|
|
151
|
+
threads=threads,
|
|
152
|
+
redo=redo,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# 2. RF-Distance ML trees
|
|
156
|
+
ml_trees = pathlib.Path(f"{prefix}.raxml.mlTrees")
|
|
157
|
+
rfdistance_prefix = pathlib.Path(f"{prefix}.rfdist")
|
|
158
|
+
|
|
159
|
+
if log_info:
|
|
160
|
+
log_runtime_information("Computing RF-Distance between ML trees.")
|
|
161
|
+
|
|
162
|
+
n_unique_all, rf_all = rf_distance(
|
|
163
|
+
ml_trees=ml_trees, prefix=rfdistance_prefix, raxmlng=raxmlng, redo=redo
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if log_info:
|
|
167
|
+
log_runtime_information(f"> RF-Distance ML trees: {round(rf_all, 2)}")
|
|
168
|
+
log_runtime_information(f"> Unique topologies ML trees: {n_unique_all}")
|
|
169
|
+
|
|
170
|
+
# 3. IQ-TREE statistical tests
|
|
171
|
+
best_tree = pathlib.Path(f"{prefix}.raxml.bestTree")
|
|
172
|
+
iqtree_prefix = pathlib.Path(f"{prefix}.iqtree")
|
|
173
|
+
if log_info:
|
|
174
|
+
log_runtime_information("Running IQ-TREE statistical tests.")
|
|
175
|
+
run_statstests(
|
|
176
|
+
msa=msa_file,
|
|
177
|
+
ml_trees=ml_trees,
|
|
178
|
+
best_tree=best_tree,
|
|
179
|
+
iqtree=iqtree,
|
|
180
|
+
model=get_iqtree_model(msa_obj.data_type),
|
|
181
|
+
prefix=iqtree_prefix,
|
|
182
|
+
seed=seed,
|
|
183
|
+
threads=threads,
|
|
184
|
+
redo=redo,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# 4. Filter the plausible trees
|
|
188
|
+
iqtree_results = pathlib.Path(f"{iqtree_prefix}.iqtree")
|
|
189
|
+
plausible_ml_trees = pathlib.Path(f"{prefix}.raxml.plausibleTrees")
|
|
190
|
+
if log_info:
|
|
191
|
+
log_runtime_information("Filtering plausible ML trees.")
|
|
192
|
+
filter_plausible_trees(
|
|
193
|
+
ml_trees=ml_trees,
|
|
194
|
+
iqtree_results=iqtree_results,
|
|
195
|
+
plausible_ml_trees=plausible_ml_trees,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
n_plausible_trees = sum(1 for _ in plausible_ml_trees.open())
|
|
199
|
+
|
|
200
|
+
if log_info:
|
|
201
|
+
log_runtime_information(f"> Found {n_plausible_trees} plausible trees.")
|
|
202
|
+
|
|
203
|
+
# 5. RF-Distance plausible trees
|
|
204
|
+
rfdistance_plausible_prefix = pathlib.Path(f"{prefix}.rfdist.plausible")
|
|
205
|
+
if log_info:
|
|
206
|
+
log_runtime_information("Computing RF-Distance between plausible ML trees.")
|
|
207
|
+
n_unique_plausible, rf_plausible = rf_distance(
|
|
208
|
+
ml_trees=plausible_ml_trees,
|
|
209
|
+
prefix=rfdistance_plausible_prefix,
|
|
210
|
+
raxmlng=raxmlng,
|
|
211
|
+
redo=redo,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if log_info:
|
|
215
|
+
log_runtime_information(
|
|
216
|
+
f"> RF-Distance plausible trees: {round(rf_plausible, 2)}"
|
|
217
|
+
)
|
|
218
|
+
log_runtime_information(
|
|
219
|
+
f"> Unique topologies plausible trees: {n_unique_plausible}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# 6. Compute the ground truth difficulty
|
|
223
|
+
difficulty = get_label(
|
|
224
|
+
n_all=n_trees,
|
|
225
|
+
rf_all=rf_all,
|
|
226
|
+
n_unique_all=n_unique_all,
|
|
227
|
+
n_plausible=n_plausible_trees,
|
|
228
|
+
rf_plausible=rf_plausible,
|
|
229
|
+
n_unique_plausible=n_unique_plausible,
|
|
230
|
+
)
|
|
231
|
+
return difficulty
|
labelgenerator/logger.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import textwrap
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import loguru
|
|
6
|
+
from labelgenerator import __version__
|
|
7
|
+
|
|
8
|
+
SCRIPT_START = time.perf_counter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = loguru.logger
|
|
12
|
+
logger.remove()
|
|
13
|
+
logger.add(sys.stderr, format="{message}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_header():
|
|
17
|
+
return textwrap.dedent(
|
|
18
|
+
f"Difficulty LabelGenerator version {__version__} released by The Exelixis Lab\n"
|
|
19
|
+
f"Developed by: Julia Haag\n"
|
|
20
|
+
f"Latest version: https://github.com/tschuelia/LabelGenerator\n"
|
|
21
|
+
f"Questions/problems/suggestions? Please open an issue on GitHub.\n",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def log_runtime_information(message, log_runtime=True):
|
|
26
|
+
if log_runtime:
|
|
27
|
+
seconds = time.perf_counter() - SCRIPT_START
|
|
28
|
+
fmt_time = time.strftime("%H:%M:%S", time.gmtime(seconds))
|
|
29
|
+
time_string = f"[{fmt_time}] "
|
|
30
|
+
else:
|
|
31
|
+
time_string = ""
|
|
32
|
+
logger.info(f"{time_string}{message}")
|