pyham 2.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.
- pyham/TreeProfile.py +408 -0
- pyham/__init__.py +20 -0
- pyham/_version.py +24 -0
- pyham/abstractgene.py +726 -0
- pyham/genome.py +148 -0
- pyham/ham.py +984 -0
- pyham/id_formats.py +137 -0
- pyham/iham.py +364 -0
- pyham/mapper.py +414 -0
- pyham/parsers.py +959 -0
- pyham/taxonomy.py +349 -0
- pyham/utils.py +31 -0
- pyham/validation.py +914 -0
- pyham-2.0.0.dist-info/METADATA +200 -0
- pyham-2.0.0.dist-info/RECORD +19 -0
- pyham-2.0.0.dist-info/WHEEL +5 -0
- pyham-2.0.0.dist-info/entry_points.txt +2 -0
- pyham-2.0.0.dist-info/licenses/LICENSE.txt +19 -0
- pyham-2.0.0.dist-info/top_level.txt +1 -0
pyham/parsers.py
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from xml.etree.ElementTree import XMLParser
|
|
4
|
+
from tqdm.auto import tqdm
|
|
5
|
+
|
|
6
|
+
from . import abstractgene
|
|
7
|
+
from .genome import ExtantGenome
|
|
8
|
+
from .taxonomy import build_taxon_node, Taxonomy
|
|
9
|
+
from . import id_formats
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _label_and_species(node):
|
|
15
|
+
"""Return a (label, species_names) pair describing an AbstractGene for conflict reporting."""
|
|
16
|
+
if isinstance(node, abstractgene.HOG):
|
|
17
|
+
species = [g.genome.name for g in node.get_all_descendant_genes()]
|
|
18
|
+
return node.hog_id if node.hog_id is not None else "<no id>", species
|
|
19
|
+
label = node.prot_id or node.gene_id or node.unique_id
|
|
20
|
+
return label, [node.genome.name]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def short_level_name(genome):
|
|
24
|
+
"""Name of a genome's level, abbreviated if it was synthesized from many child names."""
|
|
25
|
+
return abstractgene.short_level_name(genome.name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _format_one_conflict(conflict, index=None, total=None):
|
|
29
|
+
header = f"Family {conflict.family_id} — {conflict.hog_id} claims level \"{conflict.resolved_level}\" ({conflict.claim_property})"
|
|
30
|
+
if index is not None:
|
|
31
|
+
header = f"[{index}/{total}] {header}"
|
|
32
|
+
|
|
33
|
+
lines = [header]
|
|
34
|
+
for label, species, detail in conflict.offending_members:
|
|
35
|
+
species_str = ", ".join(species)
|
|
36
|
+
if conflict.kind == "same_rank":
|
|
37
|
+
lines.append(f" conflict: {label} ({species_str}) resolves to that SAME level.")
|
|
38
|
+
elif conflict.kind == "inverted":
|
|
39
|
+
lines.append(f" conflict: \"{conflict.resolved_level}\" (claimed level) is itself a descendant of {label} ({species_str}) in the given tree.")
|
|
40
|
+
else: # disjoint
|
|
41
|
+
lines.append(f" conflict: {label} ({species_str}) is not below that level in the given tree.")
|
|
42
|
+
if detail:
|
|
43
|
+
lines.append(f" reason: {detail}")
|
|
44
|
+
|
|
45
|
+
if conflict.sibling_members:
|
|
46
|
+
sibling_str = "; ".join(f"{label} ({', '.join(species)})" for label, species in conflict.sibling_members)
|
|
47
|
+
lines.append(f" also at \"{conflict.resolved_level}\" in this family: {sibling_str}")
|
|
48
|
+
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_taxonomic_conflicts(conflicts: list, cap: int = 20) -> str:
|
|
53
|
+
"""Render a list of :obj:`abstractgene.TaxonomicConflict` records into a human-readable report."""
|
|
54
|
+
if not conflicts:
|
|
55
|
+
return "No taxonomic conflicts."
|
|
56
|
+
|
|
57
|
+
if len(conflicts) == 1:
|
|
58
|
+
return _format_one_conflict(conflicts[0])
|
|
59
|
+
|
|
60
|
+
total = len(conflicts)
|
|
61
|
+
shown = conflicts[:cap]
|
|
62
|
+
blocks = [_format_one_conflict(c, i, total) for i, c in enumerate(shown, start=1)]
|
|
63
|
+
report = "\n\n".join(blocks)
|
|
64
|
+
if total > cap:
|
|
65
|
+
report += f"\n\n... and {total - cap} more conflict(s) not shown here; see the .conflicts attribute for the complete list."
|
|
66
|
+
report += f"\n\n{total} conflicts total." if total != 1 else f"\n\n{total} conflict total."
|
|
67
|
+
return report
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OrthoXMLParser:
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
Custom parser for OrthoXML containing HOGs. It can take a FilterParser object to restrict the information to parse.
|
|
74
|
+
|
|
75
|
+
The parse goes through the whole XML and create on the fly the required Ham objects:
|
|
76
|
+
- In the Xref/header, the parser creates the ExtantGenome and Gene objects.
|
|
77
|
+
- the taxonomy group and its taxon elements are parsed to create the taxonomy tree, if no species tree is provided.
|
|
78
|
+
- In the Groups section, it creates the HOGs with their hierarchy (parent/children links) and their related
|
|
79
|
+
AncestralGenomes.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
ham_object (:obj:`Ham`): Ham object to feed with created objects.
|
|
84
|
+
filterObj (:obj:`FilterParser`, optional): FilterParser object used to restrict the parsed information. Defaults
|
|
85
|
+
to None.
|
|
86
|
+
|
|
87
|
+
extant_gene_map (:obj:`dict`): dictionary of gene unique id mapped to their related :obj:`Gene`.
|
|
88
|
+
external_id_mapper (:obj:`dict`): dictionary of xref id mapped to their list of possible unique ids.
|
|
89
|
+
toplevel_hogs (:obj:`dict`): dictionary of top level hog id mapped to their related :obj:`HOG`.
|
|
90
|
+
|
|
91
|
+
cpt (:obj:`int`): Counter of parsed hogs.
|
|
92
|
+
hog_stack (:obj:`list`): Stack of hogs currently parsed. Reset at each top level hog change.
|
|
93
|
+
current_species (:obj:`ExtantGenome`): Pointer to the current species parsed during xref first step.
|
|
94
|
+
paralog_stack (:obj:`list`): Stack of position in hog stack when paralogy event occured and the related DuplicationNode.
|
|
95
|
+
Reset at each top level hog change.
|
|
96
|
+
in_paralogGroup (:obj:`int`): last position in the stack where a duplication occured.
|
|
97
|
+
skip_this_hog (:obj:`Boolean`): Boolean to skip the current hog or not (used when filtering option is set).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, ham_object, taxonomy=None, filterObject=None, id_schema: str = None, with_progress: bool = False,
|
|
101
|
+
fail_fast: bool = False):
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
Args:
|
|
105
|
+
ham_object (:obj:`Ham`): Ham object to feed with created objects.
|
|
106
|
+
filterObject (:obj:`FilterParser`, optional): FilterParser object used to restrict the parsed information.
|
|
107
|
+
with_progress (:bool:, optional): Whether to display a tqdm progress bar whilst parsing. Per-species
|
|
108
|
+
and per-500-HOGs messages are logged at DEBUG level so they don't garble the bar at INFO.
|
|
109
|
+
Defaults to None.
|
|
110
|
+
fail_fast (:bool:, optional): If True, raise :obj:`abstractgene.TaxonomicConflictError` immediately at the
|
|
111
|
+
first taxonomic conflict found. If False (default), keep parsing and collect every conflict found across
|
|
112
|
+
the whole file, raising a single combined error once parsing completes.
|
|
113
|
+
id_schema (:obj:`str`, optional): the HOG id scheme used by the orthoxml file (one of
|
|
114
|
+
:obj:`pyham.id_formats.SCHEMES`), used to synthesize ids for HOG levels inserted because the
|
|
115
|
+
species tree implies them but the file doesn't. Defaults to :obj:`pyham.id_formats.GENERIC`
|
|
116
|
+
(no synthesis) if not given.
|
|
117
|
+
"""
|
|
118
|
+
self.ham_object = ham_object
|
|
119
|
+
self.filterObj = filterObject
|
|
120
|
+
self.taxonomy = ham_object.taxonomy
|
|
121
|
+
self._id_schema = id_schema if id_schema is not None else id_formats.GENERIC
|
|
122
|
+
|
|
123
|
+
# usefull information
|
|
124
|
+
self.extant_gene_map = {}
|
|
125
|
+
self.external_id_mapper = defaultdict(list)
|
|
126
|
+
self.toplevel_hogs = {}
|
|
127
|
+
|
|
128
|
+
# On the fly variable
|
|
129
|
+
self.cpt = 0
|
|
130
|
+
self.hog_stack = []
|
|
131
|
+
self.paralog_stack = []
|
|
132
|
+
self.taxon_stack = []
|
|
133
|
+
self.current_species = None
|
|
134
|
+
self.in_paralogGroup = None
|
|
135
|
+
self.paralogyNode = None
|
|
136
|
+
self.skip_this_hog = False
|
|
137
|
+
self._genomes_to_add = []
|
|
138
|
+
|
|
139
|
+
# id of the top-level family being parsed (None between families), used to give errors context
|
|
140
|
+
self.current_family_id = None
|
|
141
|
+
self._family_index = 0
|
|
142
|
+
# OrthoXML species that couldn't be mapped onto the species tree, as (name, reason) pairs;
|
|
143
|
+
# collected over the whole <species> section and reported together before <groups>
|
|
144
|
+
self.unmapped_species = []
|
|
145
|
+
self.n_species = 0
|
|
146
|
+
# ids of HOGs collapsed into their parent because they sat at the same taxonomic level
|
|
147
|
+
self.collapsed_same_level_hogs = []
|
|
148
|
+
|
|
149
|
+
# taxonomic conflict tracking
|
|
150
|
+
self.fail_fast = fail_fast
|
|
151
|
+
self.taxonomic_conflicts = []
|
|
152
|
+
self._conflicted_children = set()
|
|
153
|
+
|
|
154
|
+
# counter for freshly-minted duplication-branch indices (see
|
|
155
|
+
# _assign_duplication_branch_ids) -- seeded far above any realistic real index so
|
|
156
|
+
# it can never collide with one; only needs to be distinct from other
|
|
157
|
+
# freshly-minted indices, which a monotonic counter guarantees.
|
|
158
|
+
self._next_synthetic_dup_idx = 10 ** 4
|
|
159
|
+
|
|
160
|
+
# save progress bar option
|
|
161
|
+
self.with_progress = with_progress
|
|
162
|
+
|
|
163
|
+
def _build_gene(self, attrib):
|
|
164
|
+
gene = abstractgene.Gene(**attrib)
|
|
165
|
+
self.current_species.add_gene(gene)
|
|
166
|
+
self.extant_gene_map[gene.unique_id] = gene
|
|
167
|
+
for type, Id in attrib.items():
|
|
168
|
+
if type != "id":
|
|
169
|
+
self.external_id_mapper.setdefault(Id, []).append(gene.unique_id)
|
|
170
|
+
|
|
171
|
+
def _build_hog(self, attrib):
|
|
172
|
+
hog = abstractgene.HOG(arose_by_duplication=False, **attrib)
|
|
173
|
+
|
|
174
|
+
if self.in_paralogGroup == len(self.hog_stack):
|
|
175
|
+
self.paralogyNode.add_child(hog)
|
|
176
|
+
|
|
177
|
+
if len(self.hog_stack) > 0:
|
|
178
|
+
self.hog_stack[-1].add_child(hog)
|
|
179
|
+
self.hog_stack.append(hog)
|
|
180
|
+
|
|
181
|
+
def _update_progress_bar(self, tag):
|
|
182
|
+
if tag == "{http://orthoXML.org/2011/}species":
|
|
183
|
+
if not hasattr(self, 'sp_pbar'):
|
|
184
|
+
self.sp_pbar = tqdm(desc='Parsing Species')
|
|
185
|
+
self.sp_pbar.update()
|
|
186
|
+
elif tag == "{http://orthoXML.org/2011/}orthologGroup":
|
|
187
|
+
if not hasattr(self, 'hog_pbar'):
|
|
188
|
+
if hasattr(self, 'sp_pbar'):
|
|
189
|
+
self.sp_pbar.close()
|
|
190
|
+
logger.info("Parsing Species completed.")
|
|
191
|
+
delattr(self, 'sp_pbar')
|
|
192
|
+
self.hog_pbar = tqdm(desc='Parsing HOGs')
|
|
193
|
+
self.hog_pbar.update()
|
|
194
|
+
elif tag == "{http://orthoXML.org/2011/}groups":
|
|
195
|
+
if hasattr(self, 'hog_pbar'):
|
|
196
|
+
self.hog_pbar.close()
|
|
197
|
+
delattr(self, 'hog_pbar')
|
|
198
|
+
logger.info("Parsing completed.")
|
|
199
|
+
|
|
200
|
+
def close_progress_bars(self):
|
|
201
|
+
"""Close any progress bar left open, e.g. when parsing aborted before reaching </groups>."""
|
|
202
|
+
for attr in ('sp_pbar', 'hog_pbar'):
|
|
203
|
+
if hasattr(self, attr):
|
|
204
|
+
getattr(self, attr).close()
|
|
205
|
+
delattr(self, attr)
|
|
206
|
+
|
|
207
|
+
def _add_extant_genome_to_taxonomy(self, genome):
|
|
208
|
+
try:
|
|
209
|
+
taxon = self.taxonomy.get_extant_taxa_by_name(genome.name)
|
|
210
|
+
except KeyError as e:
|
|
211
|
+
self.unmapped_species.append((genome.name, e.args[0] if e.args else str(e)))
|
|
212
|
+
return
|
|
213
|
+
self.taxonomy.add_genome_to_node(taxon, genome)
|
|
214
|
+
|
|
215
|
+
def _assign_ancestral_genome(self, hog):
|
|
216
|
+
"""This method determines the ancestral genome for a HOG based on its children."""
|
|
217
|
+
child_genomes = [child.genome for child in hog.children]
|
|
218
|
+
tax_node = None
|
|
219
|
+
if hog.taxon_id is not None:
|
|
220
|
+
# If the HOG has a taxon_id, we can directly assign the ancestral genome based on the taxon.
|
|
221
|
+
tax_nodes = list(self.taxonomy.nodes_by_attr(id=hog.taxon_id))
|
|
222
|
+
if len(tax_nodes) == 1:
|
|
223
|
+
tax_node = tax_nodes[0]
|
|
224
|
+
if len(set(child_genomes)) == 1 and child_genomes[0].taxon == tax_node:
|
|
225
|
+
raise abstractgene.SameLevelHOGError(f"HOG '{hog.hog_id}' at {tax_node.name} has child at same level: {hog.children[0]}.")
|
|
226
|
+
# let's check if we can map it using the TaxRange or taxid property
|
|
227
|
+
if tax_node is None:
|
|
228
|
+
for key in ('TaxRange', 'taxid'):
|
|
229
|
+
try:
|
|
230
|
+
tax_value = hog[key]
|
|
231
|
+
tax_node = self.taxonomy.get_node_by_name(tax_value)
|
|
232
|
+
except (KeyError, AttributeError):
|
|
233
|
+
pass
|
|
234
|
+
else:
|
|
235
|
+
failing = [(child, child.genome) for child in hog.children
|
|
236
|
+
if not self.taxonomy.is_child_recursive(child.genome.taxon, tax_node)]
|
|
237
|
+
if not failing:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
if len(set(child_genomes)) == 1 and failing[0][1].taxon == tax_node:
|
|
241
|
+
raise abstractgene.SameLevelHOGError(f"HOG '{hog.hog_id}' at {tax_node.name} has child at same level: {failing[0][1]}.")
|
|
242
|
+
|
|
243
|
+
for conflict in self._build_taxonomic_conflicts(hog, key, tax_value, tax_node, failing):
|
|
244
|
+
if self.fail_fast:
|
|
245
|
+
raise abstractgene.TaxonomicConflictError(format_taxonomic_conflicts([conflict]), [conflict])
|
|
246
|
+
self.taxonomic_conflicts.append(conflict)
|
|
247
|
+
for child, _ in failing:
|
|
248
|
+
self._conflicted_children.add(child)
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
# if not, let's try with the children genomes
|
|
252
|
+
if tax_node is None:
|
|
253
|
+
if len(set(child_genomes)) == 1:
|
|
254
|
+
try:
|
|
255
|
+
tax_range = hog['TaxRange']
|
|
256
|
+
if hog['TaxRange'] == hog.children[0].genome.name:
|
|
257
|
+
raise abstractgene.SameLevelHOGError(f"HOG '{hog.hog_id}' at {tax_range} has child at same level: {hog.children[0]}.")
|
|
258
|
+
except (KeyError, AttributeError):
|
|
259
|
+
pass
|
|
260
|
+
tax_node = child_genomes[0].taxon.up
|
|
261
|
+
else:
|
|
262
|
+
tax_node = self.taxonomy.get_mrca_taxnode(*(g.taxon for g in child_genomes))
|
|
263
|
+
genome = self.taxonomy.get_genome_from_taxnode(tax_node)
|
|
264
|
+
genome.add_gene(hog)
|
|
265
|
+
logger.debug(f"Added {hog} to ancestral genome {genome.name}")
|
|
266
|
+
|
|
267
|
+
def _classify_conflict(self, child, child_node, tax_node):
|
|
268
|
+
"""Classify why `child_node` doesn't sit below `tax_node`, and build a human-readable reason.
|
|
269
|
+
|
|
270
|
+
Returns a (kind, detail) pair where kind is one of "same_rank" (child collapses onto the
|
|
271
|
+
same node as `tax_node`), "inverted" (tax_node is itself a descendant of child_node), or
|
|
272
|
+
"disjoint" (the two sit in unrelated branches of the given tree).
|
|
273
|
+
"""
|
|
274
|
+
if child_node == tax_node:
|
|
275
|
+
own_claim_prop, own_claim_val = None, None
|
|
276
|
+
for key in ('TaxRange', 'taxid'):
|
|
277
|
+
try:
|
|
278
|
+
own_claim_val = child[key]
|
|
279
|
+
own_claim_prop = key
|
|
280
|
+
break
|
|
281
|
+
except (KeyError, AttributeError):
|
|
282
|
+
continue
|
|
283
|
+
if own_claim_val is not None:
|
|
284
|
+
detail = (f"its own claimed {own_claim_prop}=\"{own_claim_val}\" is not a taxon in the given tree; "
|
|
285
|
+
f"the common ancestor of its members there is \"{tax_node.name}\" itself.")
|
|
286
|
+
else:
|
|
287
|
+
detail = f"the common ancestor of its members there is \"{tax_node.name}\" itself, the same level as its own parent."
|
|
288
|
+
return "same_rank", detail
|
|
289
|
+
|
|
290
|
+
if self.taxonomy.is_child_recursive(tax_node, child_node):
|
|
291
|
+
return "inverted", None
|
|
292
|
+
|
|
293
|
+
common = self.taxonomy.get_mrca_taxnode(child_node, tax_node)
|
|
294
|
+
intermediate = [n.name for n in reversed(self.taxonomy.get_path_up(child_node, common))]
|
|
295
|
+
chain = " > ".join([common.name] + intermediate + [child_node.name])
|
|
296
|
+
return "disjoint", f"its lineage there runs {chain}."
|
|
297
|
+
|
|
298
|
+
def _build_taxonomic_conflicts(self, hog, claim_property, claim_value, tax_node, failing):
|
|
299
|
+
"""Build one :obj:`abstractgene.TaxonomicConflict` per distinct conflict kind found among
|
|
300
|
+
`hog`'s failing children (in the overwhelmingly common case, all failing children share one
|
|
301
|
+
kind and this returns a single-element list)."""
|
|
302
|
+
failing_children = {child for child, _ in failing}
|
|
303
|
+
sibling_members = [_label_and_species(c) for c in hog.children if c not in failing_children]
|
|
304
|
+
|
|
305
|
+
by_kind = defaultdict(list)
|
|
306
|
+
for child, child_genome in failing:
|
|
307
|
+
kind, detail = self._classify_conflict(child, child_genome.taxon, tax_node)
|
|
308
|
+
label, species = _label_and_species(child)
|
|
309
|
+
by_kind[kind].append((label, species, detail))
|
|
310
|
+
|
|
311
|
+
family_id = (self.hog_stack[0].hog_id if self.hog_stack else hog.hog_id).split('_')[0]
|
|
312
|
+
return [
|
|
313
|
+
abstractgene.TaxonomicConflict(
|
|
314
|
+
family_id=family_id,
|
|
315
|
+
hog_id=hog.hog_id,
|
|
316
|
+
claim_property=claim_property,
|
|
317
|
+
claim_value=str(claim_value),
|
|
318
|
+
resolved_level=tax_node.name,
|
|
319
|
+
kind=kind,
|
|
320
|
+
offending_members=members,
|
|
321
|
+
sibling_members=sibling_members,
|
|
322
|
+
)
|
|
323
|
+
for kind, members in by_kind.items()
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
def _create_missing_hogs(self, child: abstractgene.AbstractGene, parent: abstractgene.HOG):
|
|
327
|
+
"""Create missing HOGs between a child and a parent genome.
|
|
328
|
+
Returns the oldest created HOG, or the child if no HOG was created."""
|
|
329
|
+
if child == parent:
|
|
330
|
+
raise ValueError("Child and parent genomes are the same, cannot create missing HOGs.")
|
|
331
|
+
|
|
332
|
+
missing_taxons = self.taxonomy.get_path_up(child.genome.taxon, parent.genome.taxon)
|
|
333
|
+
if len(missing_taxons) > 0:
|
|
334
|
+
# the youngest hog is removed from the oldest hog's children.
|
|
335
|
+
parent.remove_child(child)
|
|
336
|
+
# Then for each intermediate level in between the two hogs...
|
|
337
|
+
current_child = child
|
|
338
|
+
anchor_id = child.hog_id if hasattr(child, 'hog_id') else parent.hog_id
|
|
339
|
+
for tax in missing_taxons:
|
|
340
|
+
# ... we get the related ancestral genome of this level...
|
|
341
|
+
ancestral_genome = self.taxonomy.get_genome_from_taxnode(tax)
|
|
342
|
+
|
|
343
|
+
# ... we create the related hog and add it to the ancestral genome...
|
|
344
|
+
hog_id = id_formats.make_missing_hog_id(self._id_schema, anchor_id, ancestral_genome.taxon)
|
|
345
|
+
hog = abstractgene.HOG(id=hog_id)
|
|
346
|
+
setattr(hog, '_missing_in_xml', parent.genome)
|
|
347
|
+
ancestral_genome.add_gene(hog)
|
|
348
|
+
|
|
349
|
+
# ... we check if taxon correspond to child parent taxon ...
|
|
350
|
+
if ancestral_genome.taxon is not current_child.genome.taxon.up:
|
|
351
|
+
raise TypeError(
|
|
352
|
+
"HOG taxon {} is different than child parent taxon {}".format(ancestral_genome.taxon,
|
|
353
|
+
current_child.genome.taxon.up))
|
|
354
|
+
# ... we add the child if everything is fine.
|
|
355
|
+
hog.add_child(current_child)
|
|
356
|
+
current_child = hog
|
|
357
|
+
parent.add_child(current_child)
|
|
358
|
+
logger.debug(f"Created {len(missing_taxons)} missing HOGs between {child} and {parent}.")
|
|
359
|
+
return current_child if len(missing_taxons) > 0 else child
|
|
360
|
+
|
|
361
|
+
def _describe_child_not_below_parent(self, parent, child):
|
|
362
|
+
def describe(node):
|
|
363
|
+
label, species = _label_and_species(node)
|
|
364
|
+
species = sorted(set(species))
|
|
365
|
+
shown = ", ".join(species[:5]) + (f", ... ({len(species)} species)" if len(species) > 5 else "")
|
|
366
|
+
return f"{label} ({shown})"
|
|
367
|
+
|
|
368
|
+
others = "; ".join(describe(c) for c in parent.children if c is not child)
|
|
369
|
+
where = "the same level as" if child.genome.taxon == parent.genome.taxon else "a level above"
|
|
370
|
+
return (f"Group {describe(child)} resolves to \"{short_level_name(child.genome)}\", {where} its parent group "
|
|
371
|
+
f"{_label_and_species(parent)[0]}, but a nested group must sit below its parent. The parent also "
|
|
372
|
+
f"contains {others or 'nothing else'} as a separate lineage, which the species tree doesn't allow. "
|
|
373
|
+
f"The group nesting contradicts the species tree, e.g. because it comes from a gene tree that "
|
|
374
|
+
f"wasn't reconciled with this species tree.")
|
|
375
|
+
|
|
376
|
+
def _resolve_missing_levels(self, parent: abstractgene.HOG, children: list):
|
|
377
|
+
"""Insert missing intermediate HOG levels between `parent` and `children`.
|
|
378
|
+
|
|
379
|
+
If several children are missing the same intermediate taxon (i.e. the
|
|
380
|
+
reference species tree resolves this part of the taxonomy more finely
|
|
381
|
+
than the HOGs were built/augmented at), a single shared ancestral HOG is
|
|
382
|
+
created for all of them instead of one redundant HOG per child. Children
|
|
383
|
+
that are missing no shared level are delegated to `_create_missing_hogs`.
|
|
384
|
+
|
|
385
|
+
Children already flagged as a taxonomic conflict are skipped entirely:
|
|
386
|
+
their genome doesn't actually sit below `parent` in the given tree, so
|
|
387
|
+
there is no valid path to insert missing levels along.
|
|
388
|
+
"""
|
|
389
|
+
children = [c for c in children if c not in self._conflicted_children]
|
|
390
|
+
change = {}
|
|
391
|
+
for child in children:
|
|
392
|
+
if parent.genome.taxon.props["depth"] != child.genome.taxon.props["depth"] - 1:
|
|
393
|
+
if not self.taxonomy.is_child_recursive(child.genome.taxon, parent.genome.taxon):
|
|
394
|
+
raise abstractgene.EvolutionaryConceptError(self._describe_child_not_below_parent(parent, child))
|
|
395
|
+
change[child] = self.taxonomy.get_path_up(child.genome.taxon, parent.genome.taxon)
|
|
396
|
+
if not change:
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
groups = defaultdict(list)
|
|
400
|
+
for child, missing in change.items():
|
|
401
|
+
# missing[-1] is the taxon immediately below `parent`; children sharing
|
|
402
|
+
# any ancestor on the path back to `parent` necessarily share this one too.
|
|
403
|
+
groups[missing[-1]].append(child)
|
|
404
|
+
|
|
405
|
+
for tax_node, group_children in groups.items():
|
|
406
|
+
if len(group_children) == 1:
|
|
407
|
+
self._create_missing_hogs(group_children[0], parent)
|
|
408
|
+
else:
|
|
409
|
+
shared_genome = self.taxonomy.get_genome_from_taxnode(tax_node)
|
|
410
|
+
shared_hog_id = id_formats.make_missing_hog_id(self._id_schema, parent.hog_id, tax_node)
|
|
411
|
+
shared_hog = abstractgene.HOG(id=shared_hog_id)
|
|
412
|
+
setattr(shared_hog, "_missing_in_xml", parent.genome)
|
|
413
|
+
shared_genome.add_gene(shared_hog)
|
|
414
|
+
parent.add_child(shared_hog)
|
|
415
|
+
for child in group_children:
|
|
416
|
+
parent.remove_child(child)
|
|
417
|
+
shared_hog.add_child(child)
|
|
418
|
+
self._resolve_missing_levels(shared_hog, group_children)
|
|
419
|
+
|
|
420
|
+
def _assign_duplication_branch_ids(self, parent_hog: abstractgene.HOG, duplication, children: list):
|
|
421
|
+
"""Give every duplication branch in `children` (direct children of `parent_hog`,
|
|
422
|
+
all flagged as arising from `duplication`) its own distinguishing LOFT dot-chain
|
|
423
|
+
segment when it lacks a real id of its own -- instead of every id-less sibling
|
|
424
|
+
falling back to `parent_hog`'s own id unchanged, which collapses distinct paralog
|
|
425
|
+
lineages onto the same synthesized id (e.g. two independent post-duplication
|
|
426
|
+
genes both becoming "HOG:X.3a_71" instead of "HOG:X.3a.1a_71"/"HOG:X.3a.1b_71").
|
|
427
|
+
|
|
428
|
+
Real ids already present on some siblings are never touched, and their
|
|
429
|
+
(idx, letter) are reused/avoided when assigning the missing ones. If none of
|
|
430
|
+
`children` reveal a real index at all, a fresh one is minted from a counter that
|
|
431
|
+
only needs to be distinct from other freshly-minted indices (any synthesized id
|
|
432
|
+
is already uniquely prefixed by `parent_hog`'s own -- already unique -- id).
|
|
433
|
+
|
|
434
|
+
Returns the subset of `children` that got moved under a freshly-created wrapper
|
|
435
|
+
HOG, so the caller can exclude them from any further, generic gap-filling over
|
|
436
|
+
`parent_hog`'s direct children (they're no longer among them).
|
|
437
|
+
"""
|
|
438
|
+
if self._id_schema not in (id_formats.LOFT, id_formats.LOFT_TAXID):
|
|
439
|
+
return set()
|
|
440
|
+
|
|
441
|
+
needs_assignment = [c for c in children if getattr(c, 'hog_id', None) is None]
|
|
442
|
+
if not needs_assignment:
|
|
443
|
+
return set()
|
|
444
|
+
|
|
445
|
+
m = id_formats.HOG_ID_RE.match(parent_hog.hog_id) if parent_hog.hog_id else None
|
|
446
|
+
if not m:
|
|
447
|
+
return set()
|
|
448
|
+
prefix = m.group('fam') + m.group('subhog')
|
|
449
|
+
|
|
450
|
+
real_segments = [seg for seg in
|
|
451
|
+
(id_formats.parse_branch_segment(getattr(c, 'hog_id', None), prefix) for c in children)
|
|
452
|
+
if seg is not None]
|
|
453
|
+
if real_segments:
|
|
454
|
+
idx = real_segments[0][0]
|
|
455
|
+
used_numbers = {id_formats.decode_paralog_branch(letters)
|
|
456
|
+
for seg_idx, letters in real_segments if seg_idx == idx}
|
|
457
|
+
else:
|
|
458
|
+
idx = self._next_synthetic_dup_idx
|
|
459
|
+
self._next_synthetic_dup_idx += 1
|
|
460
|
+
used_numbers = set()
|
|
461
|
+
|
|
462
|
+
use_taxid = self._id_schema == id_formats.LOFT_TAXID
|
|
463
|
+
wrapped = set()
|
|
464
|
+
next_number = 0
|
|
465
|
+
for child in needs_assignment:
|
|
466
|
+
while next_number in used_numbers:
|
|
467
|
+
next_number += 1
|
|
468
|
+
used_numbers.add(next_number)
|
|
469
|
+
branch_chain = f".{idx}{id_formats.encode_paralog_branch(next_number)}"
|
|
470
|
+
|
|
471
|
+
if isinstance(child, abstractgene.HOG):
|
|
472
|
+
# a real orthologGroup element with no id attribute: relabel in place.
|
|
473
|
+
taxid = id_formats.taxid_for_taxon(child.genome.taxon) if use_taxid else None
|
|
474
|
+
child.hog_id = f"{prefix}{branch_chain}_{taxid}" if taxid is not None else f"{prefix}{branch_chain}"
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# bare gene: only worth a wrapper HOG if there's real vertical distance
|
|
478
|
+
# between the duplication and its species -- a real LOFT="..." geneRef
|
|
479
|
+
# attribute never carries a taxid either, so a wrapper-less label matches
|
|
480
|
+
# that same convention.
|
|
481
|
+
missing_taxons = self.taxonomy.get_path_up(child.genome.taxon, parent_hog.genome.taxon)
|
|
482
|
+
if not missing_taxons:
|
|
483
|
+
child.set_LOFT(branch_chain)
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
branch_taxon = missing_taxons[-1]
|
|
487
|
+
ancestral_genome = self.taxonomy.get_genome_from_taxnode(branch_taxon)
|
|
488
|
+
taxid = id_formats.taxid_for_taxon(branch_taxon) if use_taxid else None
|
|
489
|
+
branch_id = f"{prefix}{branch_chain}_{taxid}" if taxid is not None else f"{prefix}{branch_chain}"
|
|
490
|
+
wrapper = abstractgene.HOG(id=branch_id)
|
|
491
|
+
setattr(wrapper, "_missing_in_xml", parent_hog.genome)
|
|
492
|
+
ancestral_genome.add_gene(wrapper)
|
|
493
|
+
|
|
494
|
+
parent_hog.remove_child(child)
|
|
495
|
+
wrapper.add_child(child)
|
|
496
|
+
parent_hog.add_child(wrapper)
|
|
497
|
+
self._create_missing_hogs(child, wrapper)
|
|
498
|
+
|
|
499
|
+
duplication.remove_child(child)
|
|
500
|
+
duplication.add_child(wrapper)
|
|
501
|
+
wrapped.add(child)
|
|
502
|
+
|
|
503
|
+
return wrapped
|
|
504
|
+
|
|
505
|
+
def start(self, tag, attrib):
|
|
506
|
+
if tag == "{http://orthoXML.org/2011/}species":
|
|
507
|
+
self.current_species = ExtantGenome(**attrib)
|
|
508
|
+
self.n_species += 1
|
|
509
|
+
if self.taxonomy is not None:
|
|
510
|
+
self._add_extant_genome_to_taxonomy(self.current_species)
|
|
511
|
+
else:
|
|
512
|
+
self._genomes_to_add.append(self.current_species)
|
|
513
|
+
|
|
514
|
+
elif tag == "{http://orthoXML.org/2011/}gene" and (self.filterObj is None or attrib["id"] in self.filterObj.geneUniqueId):
|
|
515
|
+
self._build_gene(attrib)
|
|
516
|
+
|
|
517
|
+
elif tag == "{http://orthoXML.org/2011/}paralogGroup" and self.skip_this_hog is False:
|
|
518
|
+
cur_depth = len(self.hog_stack)
|
|
519
|
+
if len(self.paralog_stack) > 0 and self.paralog_stack[-1]['depth'] == cur_depth:
|
|
520
|
+
# we have a directly nested paralog group. use the same DuplicationNode
|
|
521
|
+
dNode = self.paralog_stack[-1]['node']
|
|
522
|
+
else:
|
|
523
|
+
dNode = abstractgene.DuplicationNode(self, id=attrib.get('og', None))
|
|
524
|
+
|
|
525
|
+
self.paralog_stack.append({'depth': cur_depth, 'node': dNode})
|
|
526
|
+
|
|
527
|
+
self.in_paralogGroup = self.paralog_stack[-1]['depth']
|
|
528
|
+
self.paralogyNode = self.paralog_stack[-1]['node']
|
|
529
|
+
|
|
530
|
+
elif tag == "{http://orthoXML.org/2011/}geneRef" and self.skip_this_hog is False:
|
|
531
|
+
|
|
532
|
+
gene = self.extant_gene_map[attrib['id']]
|
|
533
|
+
if 'LOFT' in attrib:
|
|
534
|
+
gene.set_LOFT(attrib['LOFT'])
|
|
535
|
+
self.hog_stack[-1].add_child(gene)
|
|
536
|
+
|
|
537
|
+
# if the gene is contained within a paralogousGroup need to update its .arose_by_duplication flag.
|
|
538
|
+
if self.in_paralogGroup == len(self.hog_stack):
|
|
539
|
+
self.paralogyNode.add_child(gene)
|
|
540
|
+
|
|
541
|
+
elif tag == "{http://orthoXML.org/2011/}orthologGroup":
|
|
542
|
+
if len(self.hog_stack) == 0:
|
|
543
|
+
# a new roothog
|
|
544
|
+
self._family_index += 1
|
|
545
|
+
self.current_family_id = attrib.get("id") or "#{} (without id)".format(self._family_index)
|
|
546
|
+
if self.with_progress:
|
|
547
|
+
self._update_progress_bar(tag)
|
|
548
|
+
|
|
549
|
+
if self.filterObj is not None and not attrib.get("id", None) in self.filterObj.hogsId:
|
|
550
|
+
self.skip_this_hog = True
|
|
551
|
+
# create HOG and add it to the stack (or a dummy value if we skip this hog)
|
|
552
|
+
if self.skip_this_hog:
|
|
553
|
+
self.hog_stack.append(0)
|
|
554
|
+
else:
|
|
555
|
+
self._build_hog(attrib)
|
|
556
|
+
|
|
557
|
+
elif tag == "{http://orthoXML.org/2011/}property" and not self.skip_this_hog:
|
|
558
|
+
self.hog_stack[-1].add_property(attrib["name"], attrib["value"])
|
|
559
|
+
|
|
560
|
+
elif tag == "{http://orthoXML.org/2011/}score" and not self.skip_this_hog:
|
|
561
|
+
self.hog_stack[-1].score(attrib['id'], float(attrib['value']))
|
|
562
|
+
|
|
563
|
+
elif tag == "{http://orthoXML.org/2011/}taxon":
|
|
564
|
+
taxon = build_taxon_node(**attrib)
|
|
565
|
+
self.taxon_stack.append(taxon)
|
|
566
|
+
if len(self.taxon_stack) > 1:
|
|
567
|
+
self.taxon_stack[-2].add_child(child=taxon)
|
|
568
|
+
|
|
569
|
+
elif tag == "{http://orthoXML.org/2011/}groups":
|
|
570
|
+
if self.taxonomy is None:
|
|
571
|
+
raise ValueError("Cannot parse this OrthoXML file without an explicit taxonomy tree. Please provide one.")
|
|
572
|
+
if self.unmapped_species:
|
|
573
|
+
from .validation import species_not_in_tree_error
|
|
574
|
+
raise species_not_in_tree_error(self.unmapped_species, self.n_species, self.taxonomy)
|
|
575
|
+
|
|
576
|
+
def end(self, tag):
|
|
577
|
+
|
|
578
|
+
if tag == "{http://orthoXML.org/2011/}species":
|
|
579
|
+
if self.with_progress:
|
|
580
|
+
self._update_progress_bar(tag)
|
|
581
|
+
logger.debug(f"Species {self.current_species.name} created.")
|
|
582
|
+
self.current_species = None
|
|
583
|
+
|
|
584
|
+
elif tag == "{http://orthoXML.org/2011/}taxon":
|
|
585
|
+
taxonomy = self.taxon_stack.pop()
|
|
586
|
+
if len(self.taxon_stack) == 0:
|
|
587
|
+
# we are at the root of the taxonomy tree. if no taxonomy is provided upfront, we create it.
|
|
588
|
+
if self.taxonomy is None:
|
|
589
|
+
self.taxonomy = Taxonomy(taxonomy)
|
|
590
|
+
# add all extant genomes to the taxonomy tree
|
|
591
|
+
for genome in self._genomes_to_add:
|
|
592
|
+
self._add_extant_genome_to_taxonomy(genome)
|
|
593
|
+
logger.info("Taxonomy tree created from OrthoXML taxonomy. All extant genomes added to the tree.")
|
|
594
|
+
else:
|
|
595
|
+
logger.info("OrthoXML taxonomy not used, using provided taxonomy tree instead.")
|
|
596
|
+
|
|
597
|
+
elif tag == "{http://orthoXML.org/2011/}paralogGroup" and self.skip_this_hog is False:
|
|
598
|
+
ln = self.paralog_stack.pop()
|
|
599
|
+
ln['node'].set_MRCA()
|
|
600
|
+
if len(self.paralog_stack) > 0:
|
|
601
|
+
self.in_paralogGroup = self.paralog_stack[-1]['depth']
|
|
602
|
+
self.paralogyNode = self.paralog_stack[-1]['node']
|
|
603
|
+
else:
|
|
604
|
+
self.in_paralogGroup = None
|
|
605
|
+
self.paralogyNode = None
|
|
606
|
+
|
|
607
|
+
elif tag == "{http://orthoXML.org/2011/}orthologGroup":
|
|
608
|
+
# get the latest hog
|
|
609
|
+
hog = self.hog_stack.pop()
|
|
610
|
+
if self.skip_this_hog:
|
|
611
|
+
if len(self.hog_stack) == 0:
|
|
612
|
+
self.skip_this_hog = False
|
|
613
|
+
self.current_family_id = None
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
try:
|
|
617
|
+
# assign the ancestral genome to this hog
|
|
618
|
+
self._assign_ancestral_genome(hog)
|
|
619
|
+
except abstractgene.SameLevelHOGError as e:
|
|
620
|
+
# this hog has children at the same level as its parent. Usually an 'augmented' HOG.
|
|
621
|
+
logger.info(str(e))
|
|
622
|
+
self.collapsed_same_level_hogs.append(hog.hog_id)
|
|
623
|
+
# remove this hog structure.
|
|
624
|
+
for child in hog.children:
|
|
625
|
+
child.parent = hog.parent
|
|
626
|
+
hog.parent.children.remove(hog)
|
|
627
|
+
hog.parent.children.extend(hog.children)
|
|
628
|
+
if hog.arose_by_duplication:
|
|
629
|
+
dupl_node = hog.arose_by_duplication
|
|
630
|
+
dupl_node.remove_child(hog)
|
|
631
|
+
for child in hog.children:
|
|
632
|
+
dupl_node.add_child(child)
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
# get all child clustered by dup if any (skip children already flagged as a taxonomic
|
|
636
|
+
# conflict -- their genome doesn't reliably sit below hog.genome in the given tree, so
|
|
637
|
+
# the MRCA-below-hog assertion below can't be trusted for them)
|
|
638
|
+
child_by_duplication = defaultdict(list)
|
|
639
|
+
for child in hog.children:
|
|
640
|
+
if child in self._conflicted_children:
|
|
641
|
+
continue
|
|
642
|
+
if child.arose_by_duplication:
|
|
643
|
+
child_by_duplication[child.arose_by_duplication].append(child)
|
|
644
|
+
|
|
645
|
+
# all duplications must have their MRCA equal to hog.genome or below
|
|
646
|
+
for duplication, children in child_by_duplication.items():
|
|
647
|
+
if not (duplication.MRCA == hog.genome or self.taxonomy.is_child_recursive(duplication.MRCA.taxon, hog.genome.taxon)):
|
|
648
|
+
copies = ", ".join(f"{_label_and_species(c)[0]} at \"{short_level_name(c.genome)}\""
|
|
649
|
+
for c in children)
|
|
650
|
+
raise abstractgene.EvolutionaryConceptError(
|
|
651
|
+
f"HOG {_label_and_species(hog)[0]} at \"{short_level_name(hog.genome)}\" contains a "
|
|
652
|
+
f"duplication whose copies ({copies}) split before \"{short_level_name(duplication.MRCA)}\", "
|
|
653
|
+
f"above the level of the HOG itself, but a HOG can't contain copies of a duplication older "
|
|
654
|
+
f"than itself. Usually the HOG's level was inferred from its member species because its "
|
|
655
|
+
f"TaxRange/taxid isn't a node of the species tree, or the group nesting contradicts the tree.")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# For each duplication
|
|
659
|
+
for duplication, children in child_by_duplication.items():
|
|
660
|
+
|
|
661
|
+
# add hog at the level of the duplication MRCA if its missing
|
|
662
|
+
if duplication.MRCA != hog.genome and self.taxonomy.is_child_recursive(duplication.MRCA.taxon, hog.genome.taxon):
|
|
663
|
+
# create the MRCA hog
|
|
664
|
+
mrca_genome = duplication.MRCA
|
|
665
|
+
mrca_hog_id = id_formats.make_missing_hog_id(self._id_schema, hog.hog_id, mrca_genome.taxon)
|
|
666
|
+
mrca_hog = abstractgene.HOG(id=mrca_hog_id)
|
|
667
|
+
setattr(mrca_hog, "_missing_in_xml", hog.genome)
|
|
668
|
+
mrca_genome.add_gene(mrca_hog)
|
|
669
|
+
|
|
670
|
+
# link it to the current hog as parent
|
|
671
|
+
hog.add_child(mrca_hog)
|
|
672
|
+
|
|
673
|
+
# add missing level down (from mrcaHOG to duplicated child)
|
|
674
|
+
for child in children:
|
|
675
|
+
# move child from hog to mrca_hog
|
|
676
|
+
hog.remove_child(child)
|
|
677
|
+
mrca_hog.add_child(child)
|
|
678
|
+
|
|
679
|
+
# give any duplication branch lacking its own real id a proper,
|
|
680
|
+
# distinguishing LOFT segment before filling any remaining gaps
|
|
681
|
+
wrapped = self._assign_duplication_branch_ids(mrca_hog, duplication, children)
|
|
682
|
+
remaining = [c for c in children if c not in wrapped]
|
|
683
|
+
|
|
684
|
+
# add missing levels if any, grouping children that are
|
|
685
|
+
# missing the same intermediate ancestor under one shared HOG
|
|
686
|
+
self._resolve_missing_levels(mrca_hog, remaining)
|
|
687
|
+
|
|
688
|
+
duplication.set_parent(mrca_hog)
|
|
689
|
+
for x in list(duplication.children):
|
|
690
|
+
duplication.remove_child(x)
|
|
691
|
+
for y in list(mrca_hog.children):
|
|
692
|
+
duplication.add_child(y)
|
|
693
|
+
|
|
694
|
+
# Otherwise simply add missing taxa between hog and duplicated child
|
|
695
|
+
else:
|
|
696
|
+
duplication.MRCA = self.taxonomy.get_genome_from_taxnode(hog.genome.taxon)
|
|
697
|
+
duplication.set_parent(hog)
|
|
698
|
+
|
|
699
|
+
# give any duplication branch lacking its own real id a proper,
|
|
700
|
+
# distinguishing LOFT segment before filling any remaining gaps
|
|
701
|
+
wrapped = self._assign_duplication_branch_ids(hog, duplication, children)
|
|
702
|
+
|
|
703
|
+
for child_direct in children:
|
|
704
|
+
if child_direct in wrapped:
|
|
705
|
+
continue
|
|
706
|
+
new_direct_child = self._create_missing_hogs(child_direct, hog)
|
|
707
|
+
duplication.remove_child(child_direct)
|
|
708
|
+
duplication.add_child(new_direct_child)
|
|
709
|
+
|
|
710
|
+
# insert any remaining missing intermediate levels, grouping children
|
|
711
|
+
# that are missing the same intermediate ancestor under one shared HOG
|
|
712
|
+
self._resolve_missing_levels(hog, list(hog.children))
|
|
713
|
+
|
|
714
|
+
if len(self.hog_stack) == 0:
|
|
715
|
+
hog_id = hog.hog_id.split('_')[0]
|
|
716
|
+
self.toplevel_hogs[hog_id] = hog
|
|
717
|
+
self.current_family_id = None
|
|
718
|
+
self.cpt += 1
|
|
719
|
+
if self.cpt % 500 == 0:
|
|
720
|
+
logger.debug("{} HOGs parsed. ".format(self.cpt))
|
|
721
|
+
|
|
722
|
+
elif tag == "{http://orthoXML.org/2011/}groups":
|
|
723
|
+
if self.with_progress:
|
|
724
|
+
self._update_progress_bar(tag)
|
|
725
|
+
|
|
726
|
+
def data(self, data):
|
|
727
|
+
# Ignore data inside nodes
|
|
728
|
+
pass
|
|
729
|
+
|
|
730
|
+
def close(self):
|
|
731
|
+
# Nothing special to do here
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class IDSchemeSniffer:
|
|
736
|
+
"""Lightweight OrthoXML parser target used to sample HOG ids for id-scheme
|
|
737
|
+
auto-detection (see :obj:`pyham.id_formats.detect_id_scheme`), without paying for a
|
|
738
|
+
full parse of the file.
|
|
739
|
+
|
|
740
|
+
Collects the `id` attribute of every `orthologGroup` element seen, and flips `done`
|
|
741
|
+
to True once a bounded number of top-level families or ids has been sampled -- the
|
|
742
|
+
caller is expected to stop feeding lines once `done` is True.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
MAX_FAMILIES = 3
|
|
746
|
+
MAX_IDS = 300
|
|
747
|
+
_TAG = "{http://orthoXML.org/2011/}orthologGroup"
|
|
748
|
+
|
|
749
|
+
def __init__(self):
|
|
750
|
+
self.samples = []
|
|
751
|
+
self.families_seen = 0
|
|
752
|
+
self._depth = 0
|
|
753
|
+
self.done = False
|
|
754
|
+
|
|
755
|
+
def start(self, tag, attrib):
|
|
756
|
+
if tag == self._TAG:
|
|
757
|
+
self._depth += 1
|
|
758
|
+
if 'id' in attrib:
|
|
759
|
+
self.samples.append(attrib['id'])
|
|
760
|
+
if len(self.samples) >= self.MAX_IDS:
|
|
761
|
+
self.done = True
|
|
762
|
+
|
|
763
|
+
def end(self, tag):
|
|
764
|
+
if tag == self._TAG:
|
|
765
|
+
self._depth -= 1
|
|
766
|
+
if self._depth == 0:
|
|
767
|
+
self.families_seen += 1
|
|
768
|
+
if self.families_seen >= self.MAX_FAMILIES:
|
|
769
|
+
self.done = True
|
|
770
|
+
|
|
771
|
+
def data(self, data):
|
|
772
|
+
pass
|
|
773
|
+
|
|
774
|
+
def close(self):
|
|
775
|
+
return
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def sniff_id_schema(file_object):
|
|
779
|
+
"""Sample the first few families of an OrthoXML file object and return the detected HOG id scheme
|
|
780
|
+
(see :obj:`pyham.id_formats.detect_id_scheme`)."""
|
|
781
|
+
sniffer = IDSchemeSniffer()
|
|
782
|
+
sniff_parser = XMLParser(target=sniffer)
|
|
783
|
+
# <species>/<taxonomy> can dwarf <groups> in size (e.g. for large exports with
|
|
784
|
+
# a big embedded taxonomy); skip straight to <groups> via a cheap text search
|
|
785
|
+
# instead of XML-parsing that whole preamble just to sample a few HOG ids.
|
|
786
|
+
sniff_parser.feed('<orthoXML xmlns="http://orthoXML.org/2011/">')
|
|
787
|
+
seen_groups = False
|
|
788
|
+
for line in file_object:
|
|
789
|
+
if not seen_groups:
|
|
790
|
+
idx = line.find('<groups')
|
|
791
|
+
if idx == -1:
|
|
792
|
+
continue
|
|
793
|
+
line = line[idx:]
|
|
794
|
+
seen_groups = True
|
|
795
|
+
sniff_parser.feed(line)
|
|
796
|
+
if sniffer.done:
|
|
797
|
+
break
|
|
798
|
+
return id_formats.detect_id_scheme(sniffer.samples)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
class FilterOrthoXMLParser(object):
|
|
802
|
+
"""
|
|
803
|
+
Custom OrthoXML parser use to read the orthoxml file and get required information base on specific query set. It use
|
|
804
|
+
the ParserFilter object as input to do the selection.
|
|
805
|
+
|
|
806
|
+
The parse goes through the whole XML and create on the fly the required Ham objects:
|
|
807
|
+
- In the Xref/header, the parser creates the ExtantGenome and Gene objects.
|
|
808
|
+
- In the Groups section, it creates the HOGs with their hierarchy (parent/children links) and their related
|
|
809
|
+
AncestralGenomes.
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
Attributes:
|
|
813
|
+
filterObj (:obj:`FilterParser`): Filter object with all the filtering information.
|
|
814
|
+
|
|
815
|
+
geneUniqueId (:obj:`list`): list of unique gene ids to store for the OrthoXMLParser.
|
|
816
|
+
hogsId (:obj:`list`): list of hogs ids to store for the OrthoXMLParser.
|
|
817
|
+
|
|
818
|
+
current_hog (:obj:`int`): Current hog id.
|
|
819
|
+
hog_stack (:obj:`list`): Stack of hogs currently parsed. Reset at each top level hog change.
|
|
820
|
+
hog_generef (:obj:`list`): list of unique gene ids contained into the current parsed hog.
|
|
821
|
+
add_this_hog (:obj:`Boolean`): Boolean to know if we keep the current hog for the OrthoXMLParser.
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
def __init__(self, filterO):
|
|
825
|
+
|
|
826
|
+
self.filterObj = filterO
|
|
827
|
+
|
|
828
|
+
self.geneUniqueId = []
|
|
829
|
+
self.hogsId = []
|
|
830
|
+
|
|
831
|
+
self.current_hog = None
|
|
832
|
+
self.hog_stack = []
|
|
833
|
+
self.hog_generef = []
|
|
834
|
+
self.add_this_hog = False
|
|
835
|
+
|
|
836
|
+
def start(self, tag, attrib):
|
|
837
|
+
|
|
838
|
+
if tag == "{http://orthoXML.org/2011/}gene":
|
|
839
|
+
if self.filterObj.GeneIntId_filter:
|
|
840
|
+
if attrib['id'] in self.filterObj.GeneIntId_filter:
|
|
841
|
+
self.geneUniqueId.append(attrib['id'])
|
|
842
|
+
|
|
843
|
+
if self.filterObj.GeneExtId_filter:
|
|
844
|
+
for xtid in attrib.values():
|
|
845
|
+
if xtid in self.filterObj.GeneExtId_filter:
|
|
846
|
+
self.geneUniqueId.append(attrib['id'])
|
|
847
|
+
|
|
848
|
+
elif tag == "{http://orthoXML.org/2011/}geneRef":
|
|
849
|
+
self.hog_generef.append(attrib['id'])
|
|
850
|
+
if attrib['id'] in self.geneUniqueId:
|
|
851
|
+
self.add_this_hog = True
|
|
852
|
+
|
|
853
|
+
elif tag == "{http://orthoXML.org/2011/}orthologGroup":
|
|
854
|
+
if len(self.hog_stack) == 0:
|
|
855
|
+
self.current_hog = attrib["id"]
|
|
856
|
+
if self.current_hog in self.filterObj.HOGId_filter:
|
|
857
|
+
self.add_this_hog = True
|
|
858
|
+
|
|
859
|
+
self.hog_stack.append(1)
|
|
860
|
+
|
|
861
|
+
def end(self, tag):
|
|
862
|
+
|
|
863
|
+
if tag == "{http://orthoXML.org/2011/}orthologGroup":
|
|
864
|
+
|
|
865
|
+
hog = self.hog_stack.pop()
|
|
866
|
+
|
|
867
|
+
if len(self.hog_stack) == 0:
|
|
868
|
+
|
|
869
|
+
if self.add_this_hog:
|
|
870
|
+
self.geneUniqueId = self.geneUniqueId + self.hog_generef
|
|
871
|
+
self.hogsId.append(self.current_hog)
|
|
872
|
+
|
|
873
|
+
self.current_hog = None
|
|
874
|
+
self.hog_generef = []
|
|
875
|
+
self.add_this_hog = False
|
|
876
|
+
|
|
877
|
+
def data(self, data):
|
|
878
|
+
# Ignore data inside nodes
|
|
879
|
+
pass
|
|
880
|
+
|
|
881
|
+
def close(self):
|
|
882
|
+
# Nothing special to do here
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
class PhyloXMLToETE:
|
|
889
|
+
def __init__(self, node_factory=None):
|
|
890
|
+
self.root = None
|
|
891
|
+
self.stack = []
|
|
892
|
+
self.current_tag = None
|
|
893
|
+
self.buffer = ''
|
|
894
|
+
self.in_taxonomy = False
|
|
895
|
+
self.taxonomy_data = None
|
|
896
|
+
self.in_clade = False
|
|
897
|
+
from .taxonomy import Tree
|
|
898
|
+
self.node_factory = node_factory if node_factory else Tree
|
|
899
|
+
|
|
900
|
+
def start(self, tag, attrib):
|
|
901
|
+
tag = self._strip_ns(tag)
|
|
902
|
+
self.current_tag = tag
|
|
903
|
+
|
|
904
|
+
if tag == "clade":
|
|
905
|
+
dist = attrib.get("branch_length", None)
|
|
906
|
+
node = self.node_factory({"dist": dist})
|
|
907
|
+
if len(self.stack) > 0:
|
|
908
|
+
self.stack[-1].add_child(node)
|
|
909
|
+
else:
|
|
910
|
+
self.root = node
|
|
911
|
+
self.stack.append(node)
|
|
912
|
+
|
|
913
|
+
elif tag == "taxonomy":
|
|
914
|
+
self.in_taxonomy = True
|
|
915
|
+
self.taxonomy_data = {}
|
|
916
|
+
|
|
917
|
+
def end(self, tag):
|
|
918
|
+
tag = self._strip_ns(tag)
|
|
919
|
+
|
|
920
|
+
if tag == "name" and not self.in_taxonomy:
|
|
921
|
+
if len(self.stack) == 0:
|
|
922
|
+
self.buffer = ""
|
|
923
|
+
return
|
|
924
|
+
# name of clade. -> set the name of the last node in the stack
|
|
925
|
+
self.stack[-1].name = self.buffer.strip()
|
|
926
|
+
|
|
927
|
+
elif self.in_taxonomy:
|
|
928
|
+
if tag in ('scientific_name', 'id', 'code'):
|
|
929
|
+
self.taxonomy_data[tag] = self.buffer.strip()
|
|
930
|
+
|
|
931
|
+
elif tag == "taxonomy":
|
|
932
|
+
node = self.stack[-1]
|
|
933
|
+
sci_name = self.taxonomy_data.get('scientific_name')
|
|
934
|
+
if sci_name:
|
|
935
|
+
node.add_prop('scientific_name', sci_name)
|
|
936
|
+
taxon_id = self.taxonomy_data.get('id')
|
|
937
|
+
if taxon_id:
|
|
938
|
+
node.add_prop('taxon_id', taxon_id)
|
|
939
|
+
code = self.taxonomy_data.get('code')
|
|
940
|
+
if code:
|
|
941
|
+
node.add_prop('code', code)
|
|
942
|
+
self.in_taxonomy = False
|
|
943
|
+
self.taxonomy_data = None
|
|
944
|
+
|
|
945
|
+
elif tag == "clade":
|
|
946
|
+
self.stack.pop()
|
|
947
|
+
|
|
948
|
+
self.buffer = ""
|
|
949
|
+
self.current_tag = None
|
|
950
|
+
|
|
951
|
+
def data(self, data):
|
|
952
|
+
if self.current_tag:
|
|
953
|
+
self.buffer += data
|
|
954
|
+
|
|
955
|
+
def close(self):
|
|
956
|
+
return self.root
|
|
957
|
+
|
|
958
|
+
def _strip_ns(self, tag):
|
|
959
|
+
return tag.split('}', 1)[-1] if '}' in tag else tag
|