UNPKG

10 kBJavaScriptView Raw
1var _ = require('lodash');
2var TreeModel = require('tree-model');
3
4function 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 // find the lowest commen ancestor
25 lca = tree.lca([from, to]);
26
27 // get the full path from -> root, and reverse it
28 fromPath = _(from.getPath().reverse());
29
30 // get the full path to -> root
31 toPath = _(to.getPath());
32
33 // find the index of lca in fromPath and toPath
34 fromLcaIdx = fromPath.findIndex(lca);
35 toLcaIdx = toPath.findIndex(lca);
36
37 // slice and combine the arrays to get the path between
38 pathBetween = fromPath.slice(0, fromLcaIdx).concat(toPath.slice(toLcaIdx).value());
39
40 return pathBetween.value();
41 };
42}
43
44function 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
94function shouldDecorateTreePrototype(prototype) {
95 return !(
96 _.isFunction(prototype.depth) &&
97 _.isFunction(prototype.pathTo) &&
98 _.isFunction(prototype.leafNodes) &&
99 _.isFunction(prototype.lcaWith)
100 )
101}
102
103function 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
119function 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 // create a new node and add the kids to it
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 // var model = _.clone(tree.model);
155 // delete model.children;
156 // var root = treeModel.parse(model);
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
171function 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
203function 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
248function addConsensus(tree) {
249 // generate a consensus for each node in the tree
250 // the consensus is a string and an array of frequencies (gap frequencies are always 0)
251 // for leaf nodes use the sequence and cigar attributes to define the node's consensus
252 // for internal nodes (2 children) select the consensus based on the frequency in the child nodes
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
285function removeGaps(tree) {
286 // if there are gaps in the tree root's consensus, identify them and remove them from the rest of the tree
287 // remove gaps from the consensus, and from the cigar string in the leaf nodes (maybe ?)
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) { // extending a non-gap
297 nonGapLength++;
298 }
299 else { // start of a new gap
300 nonGapStart = i;
301 nonGapLength = 1;
302 }
303 }
304 else { // gap position
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
350module.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