hogprof 0.3.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.
- hogprof/__init__.py +5 -0
- hogprof/cli.py +484 -0
- hogprof/extract_levels_hits.py +518 -0
- hogprof/generate_jaccard_distr.py +516 -0
- hogprof/lshbuilder.py +710 -0
- hogprof/metadata.py +126 -0
- hogprof/presets.py +21 -0
- hogprof/profiler.py +333 -0
- hogprof/splitter.py +53 -0
- hogprof/utils/__init__.py +1 -0
- hogprof/utils/goautils.py +229 -0
- hogprof/utils/hashutils.py +190 -0
- hogprof/utils/orthoxml.py +83 -0
- hogprof/utils/phylo.py +305 -0
- hogprof/utils/pyhamutils.py +256 -0
- hogprof-0.3.0.dist-info/METADATA +231 -0
- hogprof-0.3.0.dist-info/RECORD +21 -0
- hogprof-0.3.0.dist-info/WHEEL +5 -0
- hogprof-0.3.0.dist-info/entry_points.txt +3 -0
- hogprof-0.3.0.dist-info/licenses/LICENSE +21 -0
- hogprof-0.3.0.dist-info/top_level.txt +1 -0
hogprof/utils/phylo.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wrapper module isolating the ete3 backend.
|
|
3
|
+
Provides functions to read and load newick trees
|
|
4
|
+
and owns the details about the underlying format
|
|
5
|
+
that HogProf expects.
|
|
6
|
+
|
|
7
|
+
This module is expected to be used instead
|
|
8
|
+
directly importing ete3 and any of its functions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
import pickle
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Union, Iterable
|
|
16
|
+
import ete3
|
|
17
|
+
from Bio import Entrez
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# contains the key-value args passed to ete3 every time
|
|
23
|
+
# we need to parse the tree from newick
|
|
24
|
+
default_kwargs = {
|
|
25
|
+
"format" : 1,
|
|
26
|
+
"quoted_node_names" : True
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def from_file(filename: Union[str, Path], **kwargs) -> ete3.Tree:
|
|
30
|
+
logger.debug(f"Reading file {filename}")
|
|
31
|
+
ete3_kwargs = default_kwargs.copy()
|
|
32
|
+
ete3_kwargs.update(kwargs)
|
|
33
|
+
file_str = Path(filename).absolute().as_posix()
|
|
34
|
+
return ete3.Tree(file_str, **ete3_kwargs)
|
|
35
|
+
|
|
36
|
+
def from_string(string: str, **kwargs) -> ete3.Tree:
|
|
37
|
+
ete3_kwargs = default_kwargs.copy()
|
|
38
|
+
ete3_kwargs.update(kwargs)
|
|
39
|
+
return ete3.Tree(string, **ete3_kwargs)
|
|
40
|
+
|
|
41
|
+
def to_file(tree: ete3.Tree, filename: Union[str, Path], **kwargs) -> None:
|
|
42
|
+
logger.debug(f"Saving to file {filename}")
|
|
43
|
+
ete3_kwargs = default_kwargs.copy()
|
|
44
|
+
ete3_kwargs.update(kwargs)
|
|
45
|
+
with open(filename, 'w') as outfile:
|
|
46
|
+
outfile.write(tree.write(**ete3_kwargs))
|
|
47
|
+
|
|
48
|
+
def to_string(tree: ete3.Tree, **kwargs) -> str:
|
|
49
|
+
ete3_kwargs = default_kwargs.copy()
|
|
50
|
+
ete3_kwargs.update(kwargs)
|
|
51
|
+
return tree.write(**ete3_kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def add_orphans(orphan_info, tree, genome_ids_list, verbose=False):
|
|
55
|
+
"""
|
|
56
|
+
Fix the NCBI taxonomy by adding missing species.
|
|
57
|
+
:param: orphan_info: a dictionary containing info from the NCBI on the missing taxa
|
|
58
|
+
:param: tree : an ete3 tree missing some species
|
|
59
|
+
:param: genome_ids_list: the comlete set of taxids that should be on the tree
|
|
60
|
+
:verbose: Bool print debugging stuff
|
|
61
|
+
:return: tree: a species tree with the orphan genomes added
|
|
62
|
+
"""
|
|
63
|
+
first = True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
newdict = {}
|
|
67
|
+
|
|
68
|
+
leaves = set([leaf.name for leaf in tree.get_leaves()])
|
|
69
|
+
|
|
70
|
+
orphans = set(genome_ids_list) - leaves
|
|
71
|
+
oldkeys = set(list(newdict.keys()))
|
|
72
|
+
|
|
73
|
+
keys = set()
|
|
74
|
+
i = 0
|
|
75
|
+
logger.debug("Starting orphan repair pass %d", i)
|
|
76
|
+
|
|
77
|
+
while first or ( len(orphans) > 0 and keys != oldkeys ) :
|
|
78
|
+
first = False
|
|
79
|
+
oldkeys = keys
|
|
80
|
+
leaves = set([leaf.name for leaf in tree.get_leaves()])
|
|
81
|
+
orphans = set(genome_ids_list) - leaves
|
|
82
|
+
logger.debug("Taxonomy still has %d orphan taxa", len(orphans))
|
|
83
|
+
for orphan in orphans:
|
|
84
|
+
if str(orphan_info[orphan][-1]) in newdict:
|
|
85
|
+
newdict[str(orphan_info[orphan][-1])].append(orphan)
|
|
86
|
+
else:
|
|
87
|
+
newdict[str(orphan_info[orphan][-1])] = [orphan]
|
|
88
|
+
keys = set(list(newdict.keys()))
|
|
89
|
+
for n in tree.traverse():
|
|
90
|
+
if n.name in newdict and n.name not in leaves:
|
|
91
|
+
for orph in newdict[n.name]:
|
|
92
|
+
n.add_sister(name=orph)
|
|
93
|
+
del newdict[n.name]
|
|
94
|
+
|
|
95
|
+
for orphan in orphans:
|
|
96
|
+
if len(orphan_info[orphan]) > 1:
|
|
97
|
+
orphan_info[orphan].pop()
|
|
98
|
+
|
|
99
|
+
newdict = {}
|
|
100
|
+
nodes = {}
|
|
101
|
+
logger.debug("Orphan taxa after repair: %s", sorted(orphans))
|
|
102
|
+
#clean up duplicates
|
|
103
|
+
for n in tree.traverse():
|
|
104
|
+
if n.name not in nodes:
|
|
105
|
+
nodes[ n.name] =1
|
|
106
|
+
else:
|
|
107
|
+
nodes[ n.name] +=1
|
|
108
|
+
|
|
109
|
+
for n in tree.traverse():
|
|
110
|
+
if nodes[ n.name] >1:
|
|
111
|
+
if n.is_leaf()== False:
|
|
112
|
+
n.delete()
|
|
113
|
+
nodes[ n.name]-= 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
return tree
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_tree(genomes, outdir=None):
|
|
120
|
+
"""
|
|
121
|
+
Generates a taxonomic tree using the ncbi taxonomy and
|
|
122
|
+
:param oma: a pyoma db object
|
|
123
|
+
:param saveTree: Bool for whether or not to save a mastertree newick file
|
|
124
|
+
:return: tree_string: a newick string tree: an ete3 object
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
ncbi = ete3.NCBITaxa()
|
|
128
|
+
genomes = set(genomes)
|
|
129
|
+
tree = ete3.PhyloTree(name='-1')
|
|
130
|
+
topo = ncbi.get_topology(genomes, collapse_subspecies=False)
|
|
131
|
+
tree.add_child(topo)
|
|
132
|
+
orphans = list(genomes - set([x.name for x in tree.get_leaves()]))
|
|
133
|
+
logger.warning('Missing taxa: %d', len(orphans))
|
|
134
|
+
|
|
135
|
+
orphans_info1 = {}
|
|
136
|
+
orphans_info2 = {}
|
|
137
|
+
for x in orphans:
|
|
138
|
+
search_handle = Entrez.efetch('taxonomy', id=str(x), retmode='xml')
|
|
139
|
+
record = next(Entrez.parse(search_handle))
|
|
140
|
+
logger.debug("NCBI taxonomy record for %s: %s", x, record)
|
|
141
|
+
orphans_info1[record['ParentTaxId']] = x
|
|
142
|
+
orphans_info2[x] = [x['TaxId'] for x in record['LineageEx']]
|
|
143
|
+
for n in tree.traverse():
|
|
144
|
+
if n.name in orphans_info1:
|
|
145
|
+
n.add_sister(name=orphans_info1[n.name])
|
|
146
|
+
logger.debug("Attached orphan taxon beside node %s", n.name)
|
|
147
|
+
orphans = set(genomes) - set([x.name for x in tree.get_leaves()])
|
|
148
|
+
tree = add_orphans(orphans_info2, tree, genomes)
|
|
149
|
+
orphans = set(genomes) - set([x.name for x in tree.get_leaves()])
|
|
150
|
+
tree_string = tree.write(format=1)
|
|
151
|
+
|
|
152
|
+
with open(outdir + 'master_tree.nwk', 'w') as nwkout:
|
|
153
|
+
nwkout.write(tree_string)
|
|
154
|
+
with open(outdir + '_master_tree.pkl', 'wb') as pklout:
|
|
155
|
+
pklout.write(pickle.dumps(tree))
|
|
156
|
+
|
|
157
|
+
return tree_string, tree
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def generate_taxa_index(tree , taxfilter= None, taxmask=None):
|
|
161
|
+
"""
|
|
162
|
+
Generates an index for the global taxonomic tree for all OMA
|
|
163
|
+
:param tree: ete3 tree
|
|
164
|
+
:return: taxaIndex: dictionary key: node name (species name); value: index
|
|
165
|
+
taxaIndexReverse: dictionary key: index: value: species name
|
|
166
|
+
"""
|
|
167
|
+
newtree = copy.deepcopy(tree)
|
|
168
|
+
for n in newtree.traverse():
|
|
169
|
+
if taxmask:
|
|
170
|
+
if str(n.name) == str(taxmask):
|
|
171
|
+
newtree = n
|
|
172
|
+
break
|
|
173
|
+
if taxfilter:
|
|
174
|
+
if n.name in taxfilter:
|
|
175
|
+
#set weight for descendants of n to 0
|
|
176
|
+
n.delete()
|
|
177
|
+
taxa_index = {}
|
|
178
|
+
taxa_index_reverse = {}
|
|
179
|
+
for i, n in enumerate(tree.traverse()):
|
|
180
|
+
taxa_index_reverse[i] = n.name
|
|
181
|
+
taxa_index[n.name] = i
|
|
182
|
+
|
|
183
|
+
return taxa_index, taxa_index_reverse
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TreeValidator:
|
|
187
|
+
"""Validate and repair a species tree before starting worker processes."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, filename: Path, species_names: Iterable[str] = ()):
|
|
190
|
+
self.filename = Path(filename)
|
|
191
|
+
self.species_names = {str(name) for name in species_names}
|
|
192
|
+
|
|
193
|
+
def run(self):
|
|
194
|
+
self._validate_format()
|
|
195
|
+
tree = from_file(self.filename)
|
|
196
|
+
|
|
197
|
+
self._fix_internal_species(tree)
|
|
198
|
+
|
|
199
|
+
leaf_names_before = tree.get_leaf_names()
|
|
200
|
+
new_tree = self._promote_single_leaves(tree.copy())
|
|
201
|
+
|
|
202
|
+
# repaired tree check: kept every leaf
|
|
203
|
+
assert sorted(new_tree.get_leaf_names()) == sorted(leaf_names_before)
|
|
204
|
+
# check: new tree is connected
|
|
205
|
+
assert all(child.up is node for node in new_tree.traverse() for child in node.children)
|
|
206
|
+
|
|
207
|
+
return new_tree
|
|
208
|
+
|
|
209
|
+
def _validate_format(self):
|
|
210
|
+
# validate name
|
|
211
|
+
valid = self.filename.suffix.lower() in [".nwk", ".newick"]
|
|
212
|
+
if not valid:
|
|
213
|
+
raise ValueError("Input tree must be in the newick format")
|
|
214
|
+
|
|
215
|
+
def _check_missing_duplicated_species(self, tree, nodes_by_name):
|
|
216
|
+
if not self.species_names:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# check if species list has the names not existing in the tree
|
|
220
|
+
missing = self.species_names - nodes_by_name.keys()
|
|
221
|
+
duplicated = {
|
|
222
|
+
name for name, nodes in nodes_by_name.items() if len(nodes) != 1
|
|
223
|
+
}
|
|
224
|
+
if missing or duplicated:
|
|
225
|
+
problems = []
|
|
226
|
+
if missing:
|
|
227
|
+
problems.append("missing species: " + ", ".join(sorted(missing)))
|
|
228
|
+
if duplicated:
|
|
229
|
+
problems.append(
|
|
230
|
+
"species mapped to multiple tree nodes: "
|
|
231
|
+
+ ", ".join(sorted(duplicated))
|
|
232
|
+
)
|
|
233
|
+
raise ValueError("Invalid species tree (" + "; ".join(problems) + ")")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _fix_internal_species(self, tree):
|
|
237
|
+
"""
|
|
238
|
+
Pyham raises the following error
|
|
239
|
+
`TypeError: species name 'XXX' maps to an ancestral name, not a leaf of the taxonomy`
|
|
240
|
+
if an internal node is marked as species. Repair the input tree for these cases
|
|
241
|
+
"""
|
|
242
|
+
if not self.species_names:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
nodes_by_name = {}
|
|
246
|
+
for node in tree.traverse():
|
|
247
|
+
if node.name in self.species_names:
|
|
248
|
+
nodes_by_name.setdefault(node.name, []).append(node)
|
|
249
|
+
|
|
250
|
+
self._check_missing_duplicated_species(tree, nodes_by_name)
|
|
251
|
+
|
|
252
|
+
repaired = []
|
|
253
|
+
# Postorder also handles the unlikely case of nested species nodes
|
|
254
|
+
for node in tree.traverse("postorder"):
|
|
255
|
+
if node.name not in self.species_names or node.is_leaf():
|
|
256
|
+
continue
|
|
257
|
+
if node.is_root():
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"Species {node.name!r} maps to the root of a non-trivial tree"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
parent = node.up
|
|
263
|
+
for child in list(node.get_children()):
|
|
264
|
+
child.detach()
|
|
265
|
+
parent.add_child(child)
|
|
266
|
+
repaired.append(node.name)
|
|
267
|
+
|
|
268
|
+
if repaired:
|
|
269
|
+
logger.warning(
|
|
270
|
+
"Converted %d internal species nodes to leaves: %s",
|
|
271
|
+
len(repaired),
|
|
272
|
+
", ".join(sorted(repaired)),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
invalid = [
|
|
276
|
+
name
|
|
277
|
+
for name, nodes in nodes_by_name.items()
|
|
278
|
+
if len(nodes) != 1 or not nodes[0].is_leaf()
|
|
279
|
+
]
|
|
280
|
+
if invalid:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
"Species did not resolve to leaves after tree repair: "
|
|
283
|
+
+ ", ".join(sorted(invalid))
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _promote_single_leaves(tree: ete3.Tree) -> ete3.Tree:
|
|
288
|
+
"""
|
|
289
|
+
If a node has a single descendant that's a leaf,
|
|
290
|
+
makes it a sister node
|
|
291
|
+
"""
|
|
292
|
+
new_tree = tree
|
|
293
|
+
|
|
294
|
+
count = 0
|
|
295
|
+
for node in new_tree.traverse("postorder"):
|
|
296
|
+
children = node.get_children()
|
|
297
|
+
to_delete = not node.is_root() and len(children) == 1 and children[0].is_leaf()
|
|
298
|
+
if to_delete:
|
|
299
|
+
logger.debug("Reparenting node %s", children[0].name)
|
|
300
|
+
count += 1
|
|
301
|
+
node.delete()
|
|
302
|
+
|
|
303
|
+
if count > 0:
|
|
304
|
+
logger.info("Repaired %d internal single-child nodes", count)
|
|
305
|
+
return new_tree
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import pyham
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_orthoxml_oma(fam, db_obj):
|
|
11
|
+
orthoxml = db_obj.get_orthoxml(fam).decode()
|
|
12
|
+
return orthoxml
|
|
13
|
+
|
|
14
|
+
def get_orthoxml_tar(fam, tar):
|
|
15
|
+
f = tar.extractfile(fam)
|
|
16
|
+
if f is not None:
|
|
17
|
+
return f.read()
|
|
18
|
+
else:
|
|
19
|
+
raise Exception( member + ' : not found in tarfile ')
|
|
20
|
+
return orthoxml
|
|
21
|
+
|
|
22
|
+
def get_species_from_orthoxml(orthoxml):
|
|
23
|
+
NCBI_taxid2name = {}
|
|
24
|
+
root = ET.fromstring(orthoxml)
|
|
25
|
+
for child in root:
|
|
26
|
+
if 'species' in child.tag:
|
|
27
|
+
NCBI_taxid2name[child.attrib['NCBITaxId']] = child.attrib['name']
|
|
28
|
+
return NCBI_taxid2name
|
|
29
|
+
|
|
30
|
+
def switch_name_ncbi_id(orthoxml , mapdict = None ):
|
|
31
|
+
#swap ncbi taxid for species name to avoid ambiguity
|
|
32
|
+
#mapdict should be a mapping from species name to taxid if the info isnt in the orthoxmls
|
|
33
|
+
root = ET.fromstring(orthoxml)
|
|
34
|
+
for child in root:
|
|
35
|
+
if 'species' in child.tag:
|
|
36
|
+
child.attrib['name'] = child.attrib['NCBITaxId']
|
|
37
|
+
elif mapdict:
|
|
38
|
+
child.attrib['name'] = mapdict[child.attrib['name']]
|
|
39
|
+
|
|
40
|
+
orthoxml = ET.tostring(root, encoding='unicode', method='xml')
|
|
41
|
+
return orthoxml
|
|
42
|
+
|
|
43
|
+
def reformat_treenames( tree , mapdict = None ):
|
|
44
|
+
#tree is an ete3 tree instance
|
|
45
|
+
#replace ( ) - / . and spaces with underscores
|
|
46
|
+
#iterate over all nodes
|
|
47
|
+
for node in tree.traverse():
|
|
48
|
+
if mapdict:
|
|
49
|
+
node.name = mapdict[node.name]
|
|
50
|
+
else:
|
|
51
|
+
node.name = node.name.replace('(', '').replace(')', '').replace('-', '_').replace('/', '_')
|
|
52
|
+
return tree
|
|
53
|
+
|
|
54
|
+
def reformat_names_orthoxml(orthoxml , mapdict = None ):
|
|
55
|
+
#replace ( ) - / . and spaces with underscores
|
|
56
|
+
root = ET.fromstring(orthoxml)
|
|
57
|
+
for child in root:
|
|
58
|
+
if 'species' in child.tag:
|
|
59
|
+
child.attrib['name'] = child.attrib['name'].replace('(', '').replace(')', '').replace('-', '_').replace('/', '_')
|
|
60
|
+
elif mapdict:
|
|
61
|
+
child.attrib['name'] = mapdict[child.attrib['name']]
|
|
62
|
+
orthoxml = ET.tostring(root, encoding='unicode', method='xml')
|
|
63
|
+
return orthoxml
|
|
64
|
+
|
|
65
|
+
def create_nodemapping(tree):
|
|
66
|
+
#create a mapping from node name to node
|
|
67
|
+
nodemapping = {}
|
|
68
|
+
for i,node in enumerate(tree.traverse()):
|
|
69
|
+
nodemapping[node.name] = str(i)
|
|
70
|
+
|
|
71
|
+
#assert number of nodes is equal to number of unique names
|
|
72
|
+
assert len([n for n in tree.traverse()]) == len(set(nodemapping.values()))
|
|
73
|
+
|
|
74
|
+
return nodemapping
|
|
75
|
+
|
|
76
|
+
def tree2numerical(tree):
|
|
77
|
+
mapper = create_nodemapping(tree)
|
|
78
|
+
for i,node in enumerate(tree.traverse()):
|
|
79
|
+
node.name = mapper[node.name]
|
|
80
|
+
return tree , mapper
|
|
81
|
+
|
|
82
|
+
def orthoxml2numerical(orthoxml , mapper):
|
|
83
|
+
#print(orthoxml)
|
|
84
|
+
try:
|
|
85
|
+
root = ET.fromstring(orthoxml)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
with open( orthoxml , 'r') as f:
|
|
88
|
+
orthoxml = f.read()
|
|
89
|
+
root = ET.fromstring(orthoxml)
|
|
90
|
+
for child in root:
|
|
91
|
+
if 'name' in child.attrib:
|
|
92
|
+
child.attrib['name'] = mapper[child.attrib['name']]
|
|
93
|
+
orthoxml = ET.tostring(root, encoding='unicode', method='xml')
|
|
94
|
+
return orthoxml
|
|
95
|
+
|
|
96
|
+
def _root_treemap(ham, root_hog, *, dataset_nodes, **kwargs):
|
|
97
|
+
"""Make a treemap for the rootHOG"""
|
|
98
|
+
if dataset_nodes is not None and root_hog.genome.name not in dataset_nodes:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
return ham.create_tree_profile(hog=root_hog).treemap
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _check_limits(treemap, limit_events):
|
|
105
|
+
"""
|
|
106
|
+
Check if |losses| AND |duplications| are less than limit_events
|
|
107
|
+
"""
|
|
108
|
+
duplications = sum(node.dupl or 0 for node in treemap.traverse())
|
|
109
|
+
losses = sum(node.lost or 0 for node in treemap.traverse())
|
|
110
|
+
return duplications >= limit_events or losses >= limit_events
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _subhog_keys(hogs, family):
|
|
114
|
+
"""Generate keys for subhogs in family"""
|
|
115
|
+
bases = [
|
|
116
|
+
f"{hog.genome.name}_{hog.hog_id if hog.hog_id is not None else family}"
|
|
117
|
+
for hog in hogs
|
|
118
|
+
]
|
|
119
|
+
reserved = set(bases)
|
|
120
|
+
seen = set()
|
|
121
|
+
suffixes = {}
|
|
122
|
+
for base in bases:
|
|
123
|
+
key = base
|
|
124
|
+
suffix = suffixes.get(base, 2)
|
|
125
|
+
while key in seen or (key != base and key in reserved):
|
|
126
|
+
key = f"{base}__{suffix}"
|
|
127
|
+
suffix += 1
|
|
128
|
+
suffixes[base] = suffix
|
|
129
|
+
seen.add(key)
|
|
130
|
+
yield key
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _subhog_treemaps(ham, root_hog, *, family, limit_species,
|
|
134
|
+
limit_events, dataset_nodes, verbose):
|
|
135
|
+
"""Make treemaps for the levels mode"""
|
|
136
|
+
hogs = root_hog.get_all_descendant_hogs()
|
|
137
|
+
|
|
138
|
+
fallback_id = root_hog.hog_id if root_hog.hog_id is not None else family
|
|
139
|
+
treemaps = {}
|
|
140
|
+
|
|
141
|
+
# for every subhog
|
|
142
|
+
for key, hog in zip(_subhog_keys(hogs, fallback_id), hogs):
|
|
143
|
+
|
|
144
|
+
# check that the subHOG taxa was not filtered out
|
|
145
|
+
if dataset_nodes is not None and hog.genome.name not in dataset_nodes:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# check the minimal number of represented species
|
|
149
|
+
if len(hog.get_all_descendant_genes_clustered_by_species()) < limit_species:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
treemap = ham.create_tree_profile(hog=hog).treemap
|
|
153
|
+
|
|
154
|
+
# check the minimal number of events
|
|
155
|
+
if _check_limits(treemap, limit_events):
|
|
156
|
+
treemaps[key] = treemap
|
|
157
|
+
|
|
158
|
+
if verbose:
|
|
159
|
+
logger.debug("Family %s: retained %d of %d HOG profiles",
|
|
160
|
+
family, len(treemaps), len(hogs))
|
|
161
|
+
return treemaps
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Profiling strategies supported in HogProf
|
|
165
|
+
_PROFILE_STRATEGIES = {
|
|
166
|
+
# the default strategy -- 1 profile per rootHOG
|
|
167
|
+
"root": _root_treemap,
|
|
168
|
+
|
|
169
|
+
# Levels -- 1 profile per rootHOG + every subHOG
|
|
170
|
+
"levels": _subhog_treemaps
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_ham_treemap_from_row(row, tree_string,
|
|
175
|
+
*,
|
|
176
|
+
swap_ids=True,
|
|
177
|
+
orthoXML_as_string=True,
|
|
178
|
+
use_internal_name=True,
|
|
179
|
+
limit_species=10, limit_events=0,
|
|
180
|
+
dataset_nodes=None, verbose=False,
|
|
181
|
+
slicesubhogs=False):
|
|
182
|
+
"""Parse one root-HOG input and produce treemaps depending on the mode:
|
|
183
|
+
|
|
184
|
+
- Root mode (default) returns a treemap or None
|
|
185
|
+
- Levels mode (slicesubhogs=True) returns a dictionary,
|
|
186
|
+
including the root when it passes the species and event thresholds.
|
|
187
|
+
"""
|
|
188
|
+
family, orthoxml = row
|
|
189
|
+
|
|
190
|
+
if not orthoxml:
|
|
191
|
+
logger.warning("Family %s: empty OrthoXML provided", family)
|
|
192
|
+
return {} if slicesubhogs else None
|
|
193
|
+
|
|
194
|
+
if swap_ids:
|
|
195
|
+
if not orthoXML_as_string:
|
|
196
|
+
with open(orthoxml) as source:
|
|
197
|
+
orthoxml = source.read()
|
|
198
|
+
orthoxml = switch_name_ncbi_id(orthoxml)
|
|
199
|
+
orthoXML_as_string = True
|
|
200
|
+
|
|
201
|
+
ham = pyham.Ham(tree_string, orthoxml,
|
|
202
|
+
type_hog_file="orthoxml",
|
|
203
|
+
tree_format="newick_string",
|
|
204
|
+
use_internal_name=use_internal_name,
|
|
205
|
+
orthoXML_as_string=orthoXML_as_string)
|
|
206
|
+
|
|
207
|
+
roots = ham.get_list_top_level_hogs()
|
|
208
|
+
if len(roots) != 1:
|
|
209
|
+
raise ValueError(f"Family {family}: expected one top-level HOG, found {len(roots)}")
|
|
210
|
+
|
|
211
|
+
# pick the profiling strategy
|
|
212
|
+
strategy = _PROFILE_STRATEGIES["levels" if slicesubhogs else "root"]
|
|
213
|
+
|
|
214
|
+
return strategy(ham, roots[0],
|
|
215
|
+
family=family,
|
|
216
|
+
limit_species=limit_species, limit_events=limit_events,
|
|
217
|
+
dataset_nodes=None if dataset_nodes is None else set(dataset_nodes),
|
|
218
|
+
verbose=verbose)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_subhog_ham_treemaps_from_row(row, tree_string, **kwargs):
|
|
222
|
+
"""Compatibility entry point for levels-mode callers."""
|
|
223
|
+
return get_ham_treemap_from_row(row, tree_string, slicesubhogs=True, **kwargs)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def add_library_path(library_path):
|
|
227
|
+
"""Add the directory containing profiler.py to Python's sys.path"""
|
|
228
|
+
if not os.path.exists(library_path):
|
|
229
|
+
raise FileNotFoundError(f"Path does not exist: {library_path}")
|
|
230
|
+
profiler_dir = os.path.dirname(os.path.abspath(library_path))
|
|
231
|
+
if profiler_dir not in sys.path:
|
|
232
|
+
sys.path.append(profiler_dir)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def yield_families(h5file, start_fam):
|
|
236
|
+
"""
|
|
237
|
+
Given a h5file containing OMA server, returns an iterator over the families
|
|
238
|
+
(not sure if still in use)
|
|
239
|
+
:param h5file: omafile
|
|
240
|
+
:param start_fam: fam to start on
|
|
241
|
+
:return: fam number
|
|
242
|
+
"""
|
|
243
|
+
for row in h5file.root.OrthoXML.Index:
|
|
244
|
+
if row[0] > start_fam:
|
|
245
|
+
yield row[0]
|
|
246
|
+
def get_one_family(i, h5file):
|
|
247
|
+
'''
|
|
248
|
+
get one family from database
|
|
249
|
+
Args:
|
|
250
|
+
i : family number
|
|
251
|
+
h5file : OMA server file
|
|
252
|
+
Return :
|
|
253
|
+
family
|
|
254
|
+
Not sure if still in use
|
|
255
|
+
'''
|
|
256
|
+
return h5file.root.OrthoXML.Index[i][0]
|