1 | var _ = require('lodash');
|
2 | var TreeModel = require('tree-model');
|
3 |
|
4 | function decorateTree(tree) {
|
5 | tree.lca = function lowestCommonAncestor(nodes) {
|
6 | var parentNodesInCommon = _.chain(nodes)
|
7 | .map(function (node) {
|
8 | if (!node || !_.isFunction(node.getPath)) {
|
9 | throw new Error('Cannot calculate lca with a null node');
|
10 | }
|
11 | return node.getPath();
|
12 | })
|
13 | .reduce(function (acc, nextPath) {
|
14 | return _.intersection(acc, nextPath)
|
15 | })
|
16 | .value();
|
17 | return parentNodesInCommon.pop();
|
18 | };
|
19 |
|
20 | tree.pathBetween = function pathBetweenNodes(from, to) {
|
21 |
|
22 | var lca, fromPath, toPath, fromLcaIdx, toLcaIdx, pathBetween;
|
23 |
|
24 |
|
25 | lca = tree.lca([from, to]);
|
26 |
|
27 |
|
28 | fromPath = _(from.getPath().reverse());
|
29 |
|
30 |
|
31 | toPath = _(to.getPath());
|
32 |
|
33 |
|
34 | fromLcaIdx = fromPath.findIndex(lca);
|
35 | toLcaIdx = toPath.findIndex(lca);
|
36 |
|
37 |
|
38 | pathBetween = fromPath.slice(0, fromLcaIdx).concat(toPath.slice(toLcaIdx).value());
|
39 |
|
40 | return pathBetween.value();
|
41 | };
|
42 | }
|
43 |
|
44 | function addPrototypeDecorations(tree) {
|
45 | var prototree = Object.getPrototypeOf(tree);
|
46 |
|
47 | if (!shouldDecorateTreePrototype(prototree)) {
|
48 | return;
|
49 | }
|
50 |
|
51 | prototree.depth = function calculateEffectiveNodeDepth(includeCompressedNodes) {
|
52 | var path = this.getPath()
|
53 | , depth = path.length - 1
|
54 | , compressedDepth;
|
55 |
|
56 | if (includeCompressedNodes) {
|
57 | compressedDepth = _.reduce(path, function (acc, n) {
|
58 | return acc + (n.compressedNodes ? n.compressedNodes.length : 0);
|
59 | }, 0);
|
60 | depth += compressedDepth;
|
61 | }
|
62 |
|
63 | return depth;
|
64 | };
|
65 |
|
66 | prototree.pathTo = function (to) {
|
67 | return tree.pathBetween(this, to);
|
68 | };
|
69 |
|
70 | prototree.leafNodes = function findAllLeafNodes() {
|
71 | return this.all(function (node) {
|
72 | return !node.hasChildren();
|
73 | });
|
74 | };
|
75 |
|
76 | prototree.lcaWith = function (otherNodes) {
|
77 | var nodes = _.clone(otherNodes);
|
78 | nodes.push(this);
|
79 | return tree.lca(nodes);
|
80 | };
|
81 |
|
82 | prototree.filterWalk = function (callback) {
|
83 | function filterWalkRecursive(node) {
|
84 | var evaluateChildren = callback(node);
|
85 | if (evaluateChildren && _.isArray(node.children)) {
|
86 | _.forEach(node.children, filterWalkRecursive)
|
87 | }
|
88 | }
|
89 |
|
90 | return filterWalkRecursive(this);
|
91 | }
|
92 | }
|
93 |
|
94 | function shouldDecorateTreePrototype(prototype) {
|
95 | return !(
|
96 | _.isFunction(prototype.depth) &&
|
97 | _.isFunction(prototype.pathTo) &&
|
98 | _.isFunction(prototype.leafNodes) &&
|
99 | _.isFunction(prototype.lcaWith)
|
100 | )
|
101 | }
|
102 |
|
103 | function indexTree(tree, attrs) {
|
104 | tree.indices = _.chain(attrs)
|
105 | .map(function (attr) {
|
106 | var result = {_attr: attr};
|
107 | tree.walk(function (node) {
|
108 | var key = node.model[attr];
|
109 | if (!_.isUndefined(key)) {
|
110 | result[key] = node;
|
111 | }
|
112 | });
|
113 | return result;
|
114 | })
|
115 | .indexBy('_attr')
|
116 | .value();
|
117 | }
|
118 |
|
119 | function pruneTree(tree, testNode) {
|
120 | var treeModel = new TreeModel({modelComparatorFn: tree.config.modelComparatorFn});
|
121 |
|
122 | function pruneChildren(source) {
|
123 | if (source.hasChildren()) {
|
124 | var shouldAdd = testNode(source);
|
125 | var kids=[];
|
126 | source.children.forEach(function (sourceChild) {
|
127 | var destChild = pruneChildren(sourceChild);
|
128 | if (destChild) kids.push(destChild);
|
129 | });
|
130 | if (shouldAdd || kids.length > 0) {
|
131 | if (kids.length === 1) {
|
132 | kids[0].model.distance_to_parent += source.model.distance_to_parent;
|
133 | return kids[0];
|
134 | }
|
135 | else {
|
136 |
|
137 | var model = _.clone(source.model);
|
138 | delete model.children;
|
139 | var parent = treeModel.parse(model);
|
140 | kids.forEach(function (k) {
|
141 | parent.addChild(k);
|
142 | });
|
143 | return parent;
|
144 | }
|
145 | }
|
146 | }
|
147 | else if (testNode(source)) {
|
148 | var leaf = _.clone(source.model);
|
149 | return treeModel.parse(leaf);
|
150 | }
|
151 | return false;
|
152 | }
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | var root = pruneChildren(tree);
|
158 | if (!root.model._id) {
|
159 | root.model._id = tree.model._id;
|
160 | root.model.tree_stable_id = tree.model.tree_stable_id;
|
161 | }
|
162 | if (tree.indices) {
|
163 | indexTree(root, Object.keys(tree.indices));
|
164 | }
|
165 | decorateTree(root);
|
166 | addPrototypeDecorations(root);
|
167 |
|
168 | return root;
|
169 | }
|
170 |
|
171 | function identity(geneA, geneB) {
|
172 | if (geneA === geneB) {
|
173 | return 1;
|
174 | }
|
175 |
|
176 | if (! geneA.model.consensus) {
|
177 | geneA.model.consensus = cigarToConsensus(geneA.model.cigar, geneA.model.sequence);
|
178 | }
|
179 | if (! geneB.model.consensus) {
|
180 | geneB.model.consensus = cigarToConsensus(geneB.model.cigar, geneB.model.sequence);
|
181 | }
|
182 |
|
183 | var seqA = geneA.model.consensus.sequence;
|
184 | var seqB = geneB.model.consensus.sequence;
|
185 | if (seqA.length !== seqB.length) {
|
186 | console.error('alignment sequences are not the same length');
|
187 | return 0;
|
188 | }
|
189 |
|
190 | var matchCnt = 0;
|
191 | var totalCnt = 0;
|
192 | let gapCode = '-'.charCodeAt(0);
|
193 | for(var i=0; i<seqA.length; i++) {
|
194 | totalCnt++;
|
195 | if (seqA[i] === seqB[i]) {
|
196 | if (seqA[i] === gapCode) totalCnt--;
|
197 | else matchCnt++;
|
198 | }
|
199 | }
|
200 | return matchCnt/totalCnt;
|
201 | }
|
202 |
|
203 | function cigarToConsensus(cigar, seq) {
|
204 |
|
205 | var pieces = cigar.split(/([DM])/);
|
206 | var clength=0;
|
207 | var stretch=0;
|
208 | pieces.forEach(function (piece) {
|
209 | if (piece === "M" || piece === "D") {
|
210 | if (stretch === 0) stretch = 1;
|
211 | clength += stretch;
|
212 | }
|
213 | else {
|
214 | stretch = +piece;
|
215 | }
|
216 | });
|
217 |
|
218 | var size = 0;
|
219 | var gap = '-'.charCodeAt(0);
|
220 | var alignseq = new Uint16Array(clength);
|
221 | var frequency = new Uint16Array(clength);
|
222 | alignseq.fill(gap);
|
223 | var offset=0;
|
224 | pieces.forEach(function (piece) {
|
225 | if (piece === "M") {
|
226 | if (stretch === 0) stretch = 1;
|
227 |
|
228 | frequency.fill(1,offset,offset + stretch);
|
229 | for(var i=0;i<stretch;i++) {
|
230 | offset++;
|
231 | alignseq[offset] = seq.charCodeAt(size + i);
|
232 | }
|
233 | size += stretch;
|
234 | stretch = 0;
|
235 | }
|
236 | else if (piece === "D") {
|
237 | if (stretch === 0) stretch = 1;
|
238 | offset += stretch;
|
239 | stretch = 0;
|
240 | }
|
241 | else if (!!piece) {
|
242 | stretch = +piece;
|
243 | }
|
244 | });
|
245 | return {sequence: alignseq, frequency: frequency};
|
246 | }
|
247 |
|
248 | function addConsensus(tree) {
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | if (tree.model.consensus) return;
|
254 |
|
255 | function mergeConsensi(A,B) {
|
256 | let res = _.cloneDeep(A);
|
257 | const len = A.sequence.length;
|
258 | for(let i=0; i<len; i++) {
|
259 | if (B.sequence[i] === res.sequence[i]) {
|
260 | res.frequency[i] += B.frequency[i];
|
261 | }
|
262 | else if (B.frequency[i] > res.frequency[i]) {
|
263 | res.frequency[i] = B.frequency[i];
|
264 | res.sequence[i] = B.sequence[i];
|
265 | }
|
266 | }
|
267 | return res;
|
268 | }
|
269 |
|
270 | function addConsensusToNode(node) {
|
271 | if (node.model.sequence && node.model.cigar) {
|
272 | node.model.consensus = cigarToConsensus(node.model.cigar, node.model.sequence);
|
273 | }
|
274 | else {
|
275 | addConsensusToNode(node.children[0]);
|
276 | addConsensusToNode(node.children[1]);
|
277 | node.model.consensus = mergeConsensi(node.children[0].model.consensus, node.children[1].model.consensus);
|
278 | }
|
279 | }
|
280 |
|
281 | addConsensusToNode(tree);
|
282 | removeGaps(tree);
|
283 | }
|
284 |
|
285 | function removeGaps(tree) {
|
286 |
|
287 |
|
288 | const msaLength = tree.model.consensus.frequency.length;
|
289 | let nonGapStarts = [];
|
290 | let nonGapLengths = [];
|
291 | let nonGapStart = -99;
|
292 | let nonGapLength = 0;
|
293 | let totalLength = 0;
|
294 | for (var i = 0; i < msaLength; i++) {
|
295 | if (tree.model.consensus.frequency[i] > 0) {
|
296 | if (i === nonGapStart + nonGapLength) {
|
297 | nonGapLength++;
|
298 | }
|
299 | else {
|
300 | nonGapStart = i;
|
301 | nonGapLength = 1;
|
302 | }
|
303 | }
|
304 | else {
|
305 | if (nonGapLength) {
|
306 | totalLength += nonGapLength;
|
307 | nonGapStarts.push(nonGapStart);
|
308 | nonGapLengths.push(nonGapLength);
|
309 | nonGapLength = 0;
|
310 | }
|
311 | }
|
312 | }
|
313 | if (nonGapLength) {
|
314 | totalLength += nonGapLength;
|
315 | nonGapStarts.push(nonGapStart);
|
316 | nonGapLengths.push(nonGapLength);
|
317 | }
|
318 |
|
319 | if (totalLength < msaLength) {
|
320 | function removeGapsFromNode(node) {
|
321 | let consensus = {
|
322 | sequence: new Uint16Array(totalLength),
|
323 | frequency: new Uint16Array(totalLength)
|
324 | };
|
325 | let srcSeqBuffer = node.model.consensus.sequence.buffer;
|
326 | let srcFreqBuffer = node.model.consensus.frequency.buffer;
|
327 | let dstSeqBuffer = consensus.sequence.buffer;
|
328 | let dstFreqBuffer = consensus.frequency.buffer;
|
329 | let dstOffset = 0;
|
330 | for(let i=0; i<nonGapStarts.length; i++) {
|
331 | let srcOffset = 2*nonGapStarts[i];
|
332 | let lengthInBytes = 2*nonGapLengths[i];
|
333 | let srcU8 = new Uint8Array(srcSeqBuffer, srcOffset, lengthInBytes);
|
334 | let dstU8 = new Uint8Array(dstSeqBuffer, dstOffset, lengthInBytes);
|
335 | dstU8.set(srcU8);
|
336 | srcU8 = new Uint8Array(srcFreqBuffer, srcOffset, lengthInBytes);
|
337 | dstU8 = new Uint8Array(dstFreqBuffer, dstOffset, lengthInBytes);
|
338 | dstU8.set(srcU8);
|
339 | dstOffset += lengthInBytes;
|
340 | }
|
341 | node.model.consensus = consensus;
|
342 | node.children.forEach(function (child) {
|
343 | removeGapsFromNode(child);
|
344 | });
|
345 | }
|
346 | removeGapsFromNode(tree);
|
347 | }
|
348 | }
|
349 |
|
350 | module.exports = {
|
351 | decorateTree: decorateTree,
|
352 | addPrototypeDecorations: addPrototypeDecorations,
|
353 | indexTree: indexTree,
|
354 | pruneTree: pruneTree,
|
355 | addConsensus: addConsensus,
|
356 | identity: identity
|
357 | }; |
\ | No newline at end of file |