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/id_formats.py ADDED
@@ -0,0 +1,137 @@
1
+ """Detection and synthesis helpers for the HOG id "scheme" used by an OrthoXML file.
2
+
3
+ Three schemes are recognised:
4
+
5
+ - LOFT: after a duplication, paralogous branches get a ``.<num><letter>`` suffix
6
+ appended (e.g. ``1074943`` -> ``1074943.2a`` -> ``1074943.2a.7b``). Non-duplication
7
+ levels keep the same id as they move up/down the tree, so two different taxonomic
8
+ levels can end up sharing an id.
9
+ - LOFT_TAXID: LOFT plus a trailing ``_<taxid>`` tied to the specific taxonomic level,
10
+ which restores per-level uniqueness (e.g. ``HOG:0402997.2b.1a_47424``).
11
+ - GENERIC: no inferable structure; ids are opaque and used as-is.
12
+ """
13
+
14
+ import re
15
+ import string
16
+
17
+ LOFT = "LOFT"
18
+ LOFT_TAXID = "LOFT_TAXID"
19
+ GENERIC = "GENERIC"
20
+ SCHEMES = {LOFT, LOFT_TAXID, GENERIC}
21
+
22
+ # <fam><subhog>[_<taxid>], e.g. "HOG:0402997.2b.1a_47424" or "HOG:0402997" or "3".
23
+ # Kept identical to the grammar Ham.get_hog_by_id used to inline, for backward compatibility.
24
+ HOG_ID_RE = re.compile(r"^(?P<fam>(HOG:[A-Z]?)?(\d+))(?P<subhog>[0-9a-z.]*)(_(?P<taxid>-?\d+))?$")
25
+
26
+ # Strict LOFT dot-chain shape: one or more ".<digits><letters>" groups.
27
+ _LOFT_CHAIN_RE = re.compile(r"^(\.\d+[a-zA-Z]+)+$")
28
+
29
+ _CHAIN_SHAPE_THRESHOLD = 0.8
30
+ _TAXID_SUFFIX_THRESHOLD = 0.5
31
+
32
+
33
+ def detect_id_scheme(sample_ids):
34
+ """Classify a batch of raw HOG id strings into one of LOFT / LOFT_TAXID / GENERIC.
35
+
36
+ Ids without a subhog part (e.g. a bare top-level id like "HOG:0402997") are
37
+ ignored: they say nothing about the nested-level convention used by a file.
38
+ """
39
+ matches = [HOG_ID_RE.match(s) for s in sample_ids]
40
+ matches = [m for m in matches if m]
41
+
42
+ with_subhog = [m for m in matches if m.group('subhog')]
43
+ if not with_subhog:
44
+ return GENERIC
45
+
46
+ chain_shaped = [m for m in with_subhog if _LOFT_CHAIN_RE.match(m.group('subhog'))]
47
+ if len(chain_shaped) / len(with_subhog) < _CHAIN_SHAPE_THRESHOLD:
48
+ return GENERIC
49
+
50
+ with_taxid = [m for m in chain_shaped if m.group('taxid') is not None]
51
+ if len(with_taxid) / len(chain_shaped) > _TAXID_SUFFIX_THRESHOLD:
52
+ return LOFT_TAXID
53
+ return LOFT
54
+
55
+
56
+ def taxid_for_taxon(taxon):
57
+ """Best-effort numeric/string id for an ete4 taxonomy node.
58
+
59
+ Newick-derived trees carry no id props at all; phyloxml-derived trees store it
60
+ under 'taxon_id'; orthoxml-embedded taxonomies store it under 'id'. The `_taxid`
61
+ suffix must stay numeric to remain parseable by `HOG_ID_RE`/`Ham.get_hog_by_id`, so
62
+ this returns None (rather than e.g. the node's name) when no numeric id is found.
63
+ """
64
+ for key in ('id', 'taxon_id'):
65
+ value = taxon.props.get(key)
66
+ if value is None:
67
+ continue
68
+ try:
69
+ return int(value)
70
+ except (TypeError, ValueError):
71
+ continue
72
+ return None
73
+
74
+
75
+ def make_missing_hog_id(scheme, anchor_id, taxon):
76
+ """Build an id for a HOG synthesized for a taxonomic level absent from the XML.
77
+
78
+ Reuses the LOFT dot-chain of `anchor_id` (a neighboring real HOG's id) and, under
79
+ LOFT_TAXID, swaps in the taxid of `taxon` so each synthesized level gets a
80
+ distinct, resolvable id. Falls back to returning `anchor_id` unchanged (today's
81
+ behavior) under LOFT/GENERIC, if `anchor_id` doesn't parse, or if no numeric taxid
82
+ can be found for `taxon` (e.g. a newick tree with no embedded taxon ids) -- a
83
+ non-unique id is preferable to a synthesized one that can't be looked back up.
84
+ """
85
+ if scheme != LOFT_TAXID or anchor_id is None:
86
+ return anchor_id
87
+
88
+ m = HOG_ID_RE.match(anchor_id)
89
+ if not m:
90
+ return anchor_id
91
+
92
+ taxid = taxid_for_taxon(taxon)
93
+ if taxid is None:
94
+ return anchor_id
95
+
96
+ base = m.group('fam') + m.group('subhog')
97
+ return f"{base}_{taxid}"
98
+
99
+
100
+ # The first ".<idx><letters>" segment immediately following a known parent prefix.
101
+ _BRANCH_SEGMENT_RE = re.compile(r"^\.(?P<idx>\d+)(?P<letters>[a-zA-Z]+)")
102
+
103
+
104
+ def encode_paralog_branch(nr):
105
+ """0-indexed bijective base-26 branch letter: 0->'a', 25->'z', 26->'aa', 51->'az', 52->'ba', ...
106
+
107
+ Ported from the LOFT id-generation script's `_encodeParalogClusterId`.
108
+ """
109
+ letters = []
110
+ while nr // 26 > 0:
111
+ letters.append(string.ascii_lowercase[nr % 26])
112
+ nr = nr // 26 - 1
113
+ letters.append(string.ascii_lowercase[nr % 26])
114
+ return ''.join(letters[::-1])
115
+
116
+
117
+ def decode_paralog_branch(letters):
118
+ """Inverse of `encode_paralog_branch`."""
119
+ nr = 0
120
+ for ch in letters:
121
+ nr = nr * 26 + (ord(ch.lower()) - ord('a') + 1)
122
+ return nr - 1
123
+
124
+
125
+ def parse_branch_segment(hog_id, parent_prefix):
126
+ """Parse the `(idx, letters)` of the branch segment a real sibling's `hog_id` adds
127
+ immediately after `parent_prefix` (the enclosing hog's own `fam + subhog`).
128
+
129
+ Returns None if `hog_id` is None or doesn't extend `parent_prefix` with a proper
130
+ ".<digits><letters>" segment (e.g. a bare id, or one that doesn't share the prefix).
131
+ """
132
+ if hog_id is None or not hog_id.startswith(parent_prefix):
133
+ return None
134
+ m = _BRANCH_SEGMENT_RE.match(hog_id[len(parent_prefix):])
135
+ if not m:
136
+ return None
137
+ return int(m.group('idx')), m.group('letters')
pyham/iham.py ADDED
@@ -0,0 +1,364 @@
1
+ from string import Template
2
+ import logging
3
+ import json
4
+ import datetime
5
+ import lxml.etree as etree
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class IHAM(object):
10
+
11
+ def __init__(self, newick_str, hog):
12
+ self.hog = hog
13
+ self.newick_str = newick_str
14
+ self.html_template = self._get_html_template()
15
+ self.famdata = json.dumps(self._get_famdata())
16
+ self.orthoxml = OrthoXML_manager(self.hog)
17
+ try:
18
+ numeric_hogid = int(self.hog.hog_id)
19
+ hogid = "HOG:{:07d}".format(numeric_hogid)
20
+ except ValueError:
21
+ hogid = self.hog.hog_id
22
+ self.HTML = self.html_template.safe_substitute({'name': hog.hog_id,
23
+ 'hog_id': hogid,
24
+ "tree": self.newick_str,
25
+ "orthoxml": self.orthoxml.get_orthoxml_str(),
26
+ "fam_data": self.famdata
27
+ })
28
+
29
+ def _get_html_template(self):
30
+ x =Template('''<!DOCTYPE html>
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="UTF-8">
34
+ <title>$hog_id</title>
35
+ <script src="https://d3js.org/d3.v3.js"></script>
36
+
37
+ <!-- Bootstrap CDN -->
38
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
39
+ integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
40
+ <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
41
+ integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
42
+ crossorigin="anonymous"></script>
43
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"
44
+ integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
45
+ crossorigin="anonymous"></script>
46
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"
47
+ integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
48
+ crossorigin="anonymous"></script>
49
+
50
+ <!-- Roboto font -->
51
+ <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
52
+
53
+ <script src="https://cdn.jsdelivr.net/gh/DessimozLab/iHam@master/build/iHam.js"></script>
54
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/DessimozLab/iHam@master/build/iHam.css" type="text/css"/>
55
+
56
+ <style>
57
+ body {
58
+ font-family: 'Roboto', sans-serif;
59
+ }
60
+
61
+ #updating {
62
+ position: absolute;
63
+ left: 20px;
64
+ display: none;
65
+ }
66
+
67
+ .alert_remove {
68
+ margin-bottom: 0px;
69
+ padding: 4px;
70
+ display: none;
71
+ }
72
+
73
+ .alert-link {
74
+ cursor: pointer;
75
+ }
76
+
77
+ #header {
78
+ margin-left: 20px;
79
+ margin-bottom: 20px;
80
+ }
81
+
82
+ #header > h3 {
83
+ margin-top: 20px;
84
+ margin-bottom: 20px;
85
+ }
86
+
87
+ #menu-bar > div {
88
+ display: inline-block;
89
+ }
90
+
91
+ .dropdown-toggle {
92
+ padding-top: 7px;
93
+ padding-bottom: 7px;
94
+ }
95
+ </style>
96
+ </head>
97
+ <body>
98
+
99
+ <div id="header">
100
+ <h3>Hierarchical group $hog_id open at level of <span id="current-node"></span></h3>
101
+
102
+ <div id="menu-bar">
103
+
104
+ <div id="gene-tooltips-dropdown" class="dropdown">
105
+ <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-toggle="dropdown"
106
+ aria-haspopup="true" aria-expanded="false">
107
+ Show gene tooltips on
108
+ </button>
109
+ <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
110
+ <a class="dropdown-item active" href="#">Click</a>
111
+ <a class="dropdown-item" href="#">Mouseover</a>
112
+ </div>
113
+ </div>
114
+
115
+ <div id="percentage-coverage-selector">
116
+ <button class="btn btn-sm btn-outline-dark" type="button"
117
+ aria-haspopup="true" aria-expanded="false">Remove columns under <input id="set_min_coverage"
118
+ type="number" step="10"
119
+ value="0"
120
+ min="0" max="100">% of
121
+ species coverage
122
+ </button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <div id="updating">
128
+ Updating...
129
+ </div>
130
+
131
+ <div class="alert alert-info text-center alert_remove"
132
+ role="alert"
133
+ >
134
+ Lowly supported hogs have been removed as per settings.
135
+ <a class="alert-link" id="reset_column_filter">Click here to reset.</a>
136
+ </div>
137
+
138
+
139
+ <div style="width: 1500px; min-width: 500px;" id="iham"></div>
140
+
141
+ <script>
142
+
143
+ (function (div) {
144
+
145
+ const data = {
146
+ "tree": '$tree',
147
+ "orthoxml": `$orthoxml`,
148
+ "fam_data": $fam_data
149
+ }
150
+
151
+
152
+ var theme = iHam()
153
+ .on('node_selected', function (node) {
154
+ d3.select('#current-node')
155
+ .text(node.node_name());
156
+ })
157
+ .orthoxml(data.orthoxml)
158
+ .show_oma_link(false)
159
+ .newick(data.tree)
160
+ .fam_data(data.fam_data)
161
+ .tree_width(330)
162
+ .board_width(530)
163
+ .query_gene({id: 3965})
164
+ .on("updating", function () {
165
+ d3.select("#updating")
166
+ .style("display", 'block');
167
+ })
168
+ .on("updated", function () {
169
+ d3.select("#updating")
170
+ .style("display", "none");
171
+ })
172
+ .on("hogs_removed", function (what) {
173
+ if (what.length) {
174
+ d3.select(".alert_remove")
175
+ .style("display", "block")
176
+ } else {
177
+ d3.select(".alert_remove")
178
+ .style("display", "none");
179
+ }
180
+ });
181
+
182
+ theme(div);
183
+
184
+ // Update the color schemas
185
+
186
+ // Update event for gene tooltips
187
+ d3.select("#gene-tooltips-dropdown")
188
+ .selectAll("a")
189
+ .on("click", function () {
190
+ // Manage state of menu itself
191
+ d3.select(this.parentNode).selectAll("a").classed("active", false);
192
+ d3.select(this)
193
+ .classed("active", true);
194
+
195
+ if (d3.select(this).text() === "Click") {
196
+ theme.gene_tooltips_on("click");
197
+ }
198
+ if (d3.select(this).text() === "Mouseover") {
199
+ theme.gene_tooltips_on("mouseover");
200
+ }
201
+ });
202
+
203
+ // Set minimum species coverage
204
+ d3.select("#percentage-coverage-selector").select("input")
205
+ .on("input", function () {
206
+ theme.coverage_threshold(d3.select(this).property("value"));
207
+ });
208
+
209
+ // Reset the coverage
210
+ d3.select("#reset_column_filter")
211
+ .on("click", function () {
212
+ d3.select("#percentage-coverage-selector").select("input").property("value", 0);
213
+ theme.coverage_threshold(0);
214
+ })
215
+ })(document.getElementById('iham'));
216
+
217
+
218
+ </script>
219
+ </body>
220
+ </html>''')
221
+ return x
222
+
223
+ def _get_famdata(self):
224
+
225
+ fd = []
226
+
227
+ for gene in self.hog.get_all_descendant_genes():
228
+ data = {}
229
+ data["taxon"] = {"species": gene.genome.name}
230
+ data["xrefid"] = gene.prot_id
231
+ data["sequence_length"] = 0
232
+ data["gc_content"] = 0
233
+ data["protid"] = gene.prot_id
234
+ data["id"] = gene.unique_id
235
+
236
+ fd.append(data)
237
+
238
+ return fd
239
+
240
+ class OrthoXML_manager(object):
241
+
242
+ def __init__(self, hog):
243
+
244
+ self.hog = hog
245
+
246
+ origin = "OMA"
247
+ vers = datetime.date.today().strftime("%b %Y")
248
+ origin += " standalone (bottom-up)"
249
+
250
+ self.xml = self._create_xml(origin, vers)
251
+
252
+ self._add_species_data()
253
+
254
+ self._add_groups()
255
+
256
+ def _create_xml(self, origin, originVersion):
257
+
258
+ xml_core = etree.Element("orthoXML")
259
+ xml_core.set("origin", origin)
260
+ xml_core.set("originVersion", originVersion)
261
+ xml_core.set("version", "0.3")
262
+ xml_core.set("xmlns", "http://orthoXML.org/2011/")
263
+ return xml_core
264
+
265
+ def _add_species_data(self):
266
+
267
+ sp_data = self.hog.get_all_descendant_genes_clustered_by_species()
268
+
269
+ for species, childs in sp_data.items():
270
+
271
+ species_xml = etree.Element("species")
272
+ species_xml.set("name", species.name)
273
+ species_xml.set("NCBITaxId", str(-1))
274
+ self.xml.insert(0, species_xml)
275
+
276
+ # Add <database> into <species>
277
+ database_xml = etree.SubElement(species_xml, "database")
278
+ database_xml.set("name", "None")
279
+ database_xml.set("version", "None")
280
+
281
+ # Add <genes> TAG into <database>
282
+ genes_xml = etree.SubElement(database_xml, "genes")
283
+
284
+ # Fill <genes> with <gene>
285
+ for gene_obj in childs:
286
+ gene_xml = etree.SubElement(genes_xml, "gene")
287
+ gene_xml.set("id", str(gene_obj.unique_id))
288
+ gene_xml.set("protId", str(gene_obj.prot_id))
289
+
290
+ def _add_groups(self):
291
+
292
+ from . import abstractgene as absGene
293
+
294
+ def _process_child(child, current_xml):
295
+
296
+ if isinstance(child, absGene.Gene):
297
+ generef_xml = etree.SubElement(current_xml, "geneRef")
298
+ generef_xml.set('id', str(child.unique_id))
299
+ else:
300
+ _visit(child, current_xml)
301
+
302
+ def _visit(hog, parent):
303
+
304
+ if len(hog.children) == 1:
305
+ current_hog_xml = parent
306
+
307
+ elif len(hog.duplications) >= 1:
308
+
309
+ dup_child = []
310
+
311
+ for duplicationNode in hog.duplications:
312
+ dup_child += duplicationNode.children
313
+
314
+ remaining_hog = list(set(hog.children) - set(dup_child))
315
+
316
+ if len(remaining_hog) == 0 and len(hog.duplications) == 1 :
317
+
318
+ current_hog_xml = parent
319
+
320
+ else:
321
+
322
+ current_hog_xml = etree.SubElement(parent, "orthologGroup")
323
+ taxon = etree.SubElement(current_hog_xml, "property")
324
+ taxon.set("name", 'TaxRange')
325
+ taxon.set("value", hog.genome.name)
326
+
327
+ current_hog_xml.set('id', str(hog.hog_id))
328
+
329
+ else:
330
+ current_hog_xml = etree.SubElement(parent, "orthologGroup")
331
+ taxon = etree.SubElement(current_hog_xml, "property")
332
+ taxon.set("name", 'TaxRange')
333
+ taxon.set("value", hog.genome.name)
334
+
335
+ current_hog_xml.set('id', str(hog.hog_id))
336
+
337
+ processed_child = []
338
+ if len(hog.duplications) > 0:
339
+
340
+ for duplicationNode in hog.duplications:
341
+
342
+ paralogGroup = etree.SubElement(current_hog_xml, "paralogGroup")
343
+
344
+ for child in duplicationNode.children:
345
+ _process_child(child, paralogGroup)
346
+ processed_child.append(child)
347
+
348
+ remaining_hog = list(set(hog.children) - set(processed_child))
349
+ for child in remaining_hog:
350
+ _process_child(child, current_hog_xml)
351
+
352
+ self.groupsxml = etree.SubElement(self.xml, "groups")
353
+
354
+ _visit(self.hog, self.groupsxml)
355
+
356
+ def get_orthoxml_str(self):
357
+ return etree.tostring(self.xml, encoding='utf8', method='xml', pretty_print=False).decode('utf-8')
358
+
359
+
360
+
361
+
362
+
363
+
364
+