UNPKG

phylogician-ts

Version:

Module to read, manipulate and write phylogenetic trees. Written in TypeScript

624 lines 21.3 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const loglevel_colored_prefix_1 = require("loglevel-colored-prefix"); const newick = __importStar(require("newick-reader")); const BalancedSample_1 = require("./BalancedSample"); const TreeNode_1 = require("./TreeNode"); class Tree { constructor(loglevel = 'info') { this.nodes = []; this.numberOfNodes = 0; this.logLevel = loglevel; this.logger = new loglevel_colored_prefix_1.Logger(this.logLevel); } /** * Builds a Tree from newick string * * @param {string} data * @memberof Tree */ buildTree(data) { if (this.nodes.length !== 0) { throw new Error(`There is a phylogeny in this instance with ${this.getAllLeafIds().length} leafs`); } const treeObject = newick.read(data); this.makeTree(treeObject); this.setAsRoot(0).setDefaultLabelDisplay(); return this; } /** * Returns the maximum hops to child leafs to all nodes. * * @returns {number[]} * @memberof Tree */ getAllMaxHopsToChildLeafs() { const maxHops = this.getMaxHopsToRoot(); const nodeListCounter = this.nodes.map(node => 0); const round = 1; const scanTree = (nodeIds, counter, r) => { const parentsToNextRound = new Set(); nodeIds.forEach(id => { const parentId = this.getNode(id).getParentNodeId(); if (parentId !== null) { parentsToNextRound.add(parentId); counter[parentId] = r; } }); if (r < maxHops) { scanTree(parentsToNextRound, counter, r + 1); } }; const leafs = new Set(this.getAllLeafIds()); scanTree(leafs, nodeListCounter, round); return nodeListCounter; } /** * Returns the sum of branch lengths from the node to the root * * @param {number} nodeId * @returns {number} * @memberof Tree */ getDistanceToRoot(nodeId) { const node = this.getNode(nodeId); let length = node.branchLength || 0; if (node.getParentNodeId() !== null) { const parentNodeId = node.getParentNodeId(); if (parentNodeId !== null) { length += this.getDistanceToRoot(parentNodeId); } } return length; } /** * Returns the number of hops from node to root * * @param {number} nodeId * @returns {number} * @memberof Tree */ getHopsToRoot(nodeId) { let hops = 1; const node = this.getNode(nodeId); if (node.getParentNodeId() !== null) { const parentNodeId = node.getParentNodeId(); if (parentNodeId !== null) { hops += this.getHopsToRoot(parentNodeId); } } else { return 0; } return hops; } /** * Returns the path of internal nodes to root * * @param {number} nodeId * @returns {number} * @memberof Tree */ getPathToRoot(nodeId) { const path = [nodeId]; const node = this.getNode(nodeId); const parentId = node.getParentNodeId(); if (parentId !== null) { this.getPathToRoot(parentId).forEach(hop => path.push(hop)); } return path; } /** * Returns the maximum distance to the root. * * @returns {number} * @memberof Tree */ getMaxDistanceToRoot() { const root = this.getRootNode(); if (root) { const leafs = this.getLeafsIds(root.id); let maxDist = 0; leafs.forEach(leaf => { const dist = this.getDistanceToRoot(leaf); maxDist = maxDist < dist ? dist : maxDist; }); return maxDist; } throw new Error('Root has not been set yet'); } /** * Returns maximum number of node hops to the root. * * @returns {number} * @memberof Tree */ getMaxHopsToRoot() { const root = this.getRootNode(); if (root) { const leafs = this.getLeafsIds(0); let maxHops = 0; leafs.forEach(leaf => { const hops = this.getHopsToRoot(leaf); maxHops = maxHops < hops ? hops : maxHops; }); return maxHops; } throw new Error('Root has not been set yet'); } /** * Return the common ancestor node between two nodes and the number of hops from the node passed in the first argument * * @param {number} node1 * @param {number} node2 * @returns {{ancestor: number, hops: number}} * @memberof Tree */ getCommonAncestor(node1, node2) { const log = this.logger.getLogger('Tree::getCommonAncestor'); log.debug(`Finding common ancestor between nodes ${node1} and ${node2}`); const path1 = this.getPathToRoot(node1); const path2 = this.getPathToRoot(node2); const minPath = Math.min(path1.length, path2.length); log.debug(`Paths: \n${path1}\n${path2}`); log.debug(`Min path to root: ${minPath}`); let commonAncestor = -1; for (let i = 0; i < minPath; i++) { const step1 = path1.pop(); const step2 = path2.pop(); log.debug(`Paths: ${step1} :: ${step2}`); if (typeof step1 !== 'undefined' && typeof step2 !== 'undefined' && step1 === step2) { log.debug('Found a match'); commonAncestor = step1; } else { break; } } if (commonAncestor === -1) { log.error(`No common ancestor found between ${node1} and ${node2}`); throw new Error(`No common ancestor found between ${node1} and ${node2}`); } return { ancestor: commonAncestor, hops: path1.length + 1, }; } /** * Return information about the common ancestor between the node passed in the argument and all leafs * * @param {number} node * @returns {Array<{ ancestor: number; hops: number; nodeId: number }>} * @memberof Tree */ getCommonAncestorWithEachLeaf(node, subtreeNodeId) { const log = this.logger.getLogger('Tree::getCommonAncestorWithEachLeaf'); log.info(`Finding common ancestor between node ${node} and all other leafs of the subtree`); // const allLeafs = this.getLeafsIds(subtreeNodeId); // const numberOfLeafs = allLeafs.length; // const processed: number[] = [node]; let current = node; if (current === null) { return []; } const result = []; let hops = 1; while (current !== subtreeNodeId) { log.debug(`Current: ${current}`); const ancestor = this.getNode(current).parent; const ancestorNode = this.getNode(ancestor); const otherChildren = ancestorNode.children.filter(child => child !== current); log.debug(`otherChildren: ${otherChildren}`); otherChildren.forEach(childId => { // const childIdNode = this.getNode(childId) log.debug(`Child: ${childId}`); let leafs = this.getLeafsIds(childId); if (leafs.length === 0) { leafs = [childId]; } // leafs = leafs.filter(leaf => leaf !== node) log.debug(`Leafs: ${leafs}`); leafs.forEach(nodeId => result.push({ ancestor, hops, nodeId })); }); hops++; current = ancestor; } return result; } /** * Get TreeNode with the given id * * @param {number} id * @returns {TreeNode} * @memberof Tree */ getNode(id) { const selected = this.nodes.find(n => n.id === id); if (!selected) { throw new Error(`Id ${id} not found.`); } return selected; } /** * Find node by exact name * * @param {string} name * @returns {TreeNode[]} * @memberof Tree */ findNodeByExactName(name) { return this.nodes.filter(n => n.name === name); } /** * Find nodes that match a Regular Expression * * @param {RegExp} regex * @returns {TreeNode[]} * @memberof Tree */ findNodeByMatch(regex) { return this.nodes.filter(n => n.name.match(regex)); } /** * Returns how many leafs the tree has. * * @returns {number} * @memberof Tree */ getNumberOfLeafs() { return this.nodes.filter(node => node.isLeaf()).length; } /** * Return root node * * @private * @returns {TreeNode} * @memberof Tree */ getRootNode() { const root = this.nodes.filter(node => node.isRoot()); if (root.length === 1) { return root[0]; } if (root.length > 1) { throw new Error(`There are ${root.length} roots with ids [${root.map(r => r.id).join(', ')}] assigned.`); } return null; } /** * Return array of node ids of internal nodes * * @returns {number[]} * @memberof Tree */ getInternalIds() { const internalIds = this.nodes.filter((d) => !d.isLeaf()).map(node => node.id); return internalIds; } /** * Sets the node with Id passed as root. Avoids setting 2 root nodes. * This DOES NOT re-root the tree, yet. * * @param {number} id * @returns {this} * @memberof Tree */ setAsRoot(id) { const oldRoot = this.getRootNode(); if (oldRoot === null) { this.getNode(id).setAsRoot(); } else if (oldRoot.id !== id) { oldRoot.unsetAsRoot(); this.getNode(id).setAsRoot(); } return this; } /** * Return ids of all child nodes. * * @param {number} id * @returns {number[]} * @memberof Tree */ getChildrenIds(id) { const node = this.getNode(id); const childrenNodeIds = []; node.children.forEach(child => { childrenNodeIds.push(child); const childNode = this.getNode(child); if (childNode.children) { const childrenOfChild = this.getChildrenIds(child); childrenOfChild.forEach(childOfChild => childrenNodeIds.push(childOfChild)); } }); return childrenNodeIds; } /** * Return the ids of leafs of the node passed as argument * * @param {number} id * @returns {number[]} * @memberof Tree */ getLeafsIds(id) { return this.getChildrenIds(id).filter(nodeId => this.getNode(nodeId).isLeaf()); } getAllLeafIds() { return this.nodes.filter(node => node.isLeaf()).map(node => node.id); } /** * Ladderizes the subtree from the node passed in argument * * @param {number} id * @returns {this} * @memberof Tree */ ladderize(id) { const log = this.logger.getLogger('Tree::ladderize2'); const newOrder = this.lad(id); let i = 0; const newNodeOrder = this.nodes .map(n => n.id) .map(nodeId => (newOrder.indexOf(nodeId) !== -1 ? newOrder[i++] : nodeId)); const newNodes = newNodeOrder.map(nodeId => this.getNode(nodeId)); this.nodes = newNodes; return this; } /** * Returns a newick of the subtree from parentId. If nothing is passed, it will return the entire tree. * * @param {number} [parentId=-1] * @returns {string} * @memberof Tree */ writeNewick(parentId = -1) { const iTreeObj = this.writeITreeObj(parentId); return newick.write(iTreeObj); } /** * Selects a balanced sample of N leafs in the subtree of the nodeId node. * * @param {number[]} [picked=[]] * @param {number} nodeId * @param {number} N * @returns {number[]} * @memberof Tree */ selectBalancedLeafs(picked = [], nodeId, N) { const log = this.logger.getLogger('Tree::selectBalancedLeafs'); const selected = []; log.info('finding all leafs'); const allLeafs = this.getLeafsIds(nodeId); log.info('resolving all mandatory picks'); const mandatoryPick = picked.map(p => { if (allLeafs.indexOf(p) === -1) { log.error(`Element ${p} not present in tree.`); throw new Error(`Element ${p} not present in tree.`); } return p; }); if (N > allLeafs.length) { return allLeafs; } if (N < picked.length) { log.error(`The total size of the sample passed in the parameter N is smaller than the number (${picked.length}) of elements passed.`); throw new Error(`The total size of the sample passed in the parameter N is smaller than the number (${picked.length}) of elements passed.`); } let prob = allLeafs.map(_ => 1 / allLeafs.length); let currentPick = -1; log.info(`selecting balanced sample from node ${nodeId} in addition to ${picked}`); while (selected.length < N) { log.info(`selected: ${JSON.stringify(selected)}`); log.info(`current Pick: ${currentPick}`); // const prob = previousProb.map(d => d); if (currentPick !== -1) { let probSum = 0; const cas = this.getCommonAncestorWithEachLeaf(currentPick, nodeId); cas.forEach(ca => { log.debug(`cas ${JSON.stringify(ca)}`); const i = allLeafs.indexOf(ca.nodeId); if (i !== -1) { log.debug(`i: ${i}`); prob[i] *= ca.hops * 2; probSum += prob[i]; } }); prob = prob.map(p => p / probSum); } log.debug(`leafs left: ${JSON.stringify(allLeafs)}`); log.debug(`Probability ${JSON.stringify(prob)}`); if (mandatoryPick.length) { const pick = mandatoryPick.pop(); log.info(`Mandatory pick left: ${JSON.stringify(mandatoryPick)}`); log.info(`Mandatory pick: ${pick}`); if (pick) { const i = allLeafs.indexOf(pick); if (i !== -1) { log.info(`Adding mandatory pick: ${pick}`); currentPick = allLeafs.splice(i, 1)[0]; selected.push(currentPick); prob.splice(i, 1); } } } else { const randNumber = Math.random(); log.info(`random pick: ${randNumber}`); let cumm = 0; for (let i = 0; i < prob.length; i++) { cumm += prob[i]; if (randNumber < cumm) { currentPick = allLeafs.splice(i, 1)[0]; selected.push(currentPick); prob.splice(i, 1); break; } } } log.info(`Selected: ${JSON.stringify(selected)}`); log.info(`${N - selected.length} to go`); } log.info(`Selected: ${JSON.stringify(selected)}`); return selected; } selectBalancedSample(picked = [], baseNodeId, N, add = false, seed) { const log = this.logger.getLogger('Tree::selectBalancedSample'); log.info(`Trying to get ${N} samples`); const bsamp = new BalancedSample_1.BalancedSample(this, this.logLevel); return bsamp.pick(picked, baseNodeId, N, add, seed); } selectBalancedSampleMixed(picked = [], baseNodeId, N, add = false, seed) { const log = this.logger.getLogger('Tree::selectBalancedSampleMixed'); log.info(`Trying to get ${N} samples`); const bsamp = new BalancedSample_1.BalancedSample(this, this.logLevel); return bsamp.pickMixed(picked, baseNodeId, N, add, seed); } /** * Return a subTree as a Tree object * It rework the indexes of nodes and parents. * * @param {number} nodeId * @returns * @memberof Tree */ getSubTree(nodeId) { const log = this.logger.getLogger('Tree::getSubTree'); const nwk = this.writeNewick(nodeId); const newTree = new Tree(this.logLevel); newTree.buildTree(nwk); return newTree; } /** * Return a list of nodes under a internal node. * * @param {number} nodeId * @returns * @memberof Tree */ getListOfNodesInSubTree(nodeId) { const log = this.logger.getLogger('Tree::getListOfNodesInSubTree'); log.info(`Selecting child nodes from ${nodeId}.`); const rootNode = this.getNode(nodeId); const newTree = [rootNode]; const childrenIds = this.getChildrenIds(nodeId); childrenIds.forEach(childId => newTree.push(this.getNode(childId))); return newTree; } /** * Set the color of branches of child nodes. * * @param {number} id * @param {string} color * @returns {this} * @memberof Tree */ setChildrenBranchColor(id, color) { this.getChildrenIds(id).forEach(childId => this.getNode(childId).setBranchColor(color)); return this; } /** * Order nodes for ladderization * * @protected * @param {number} id * @returns {number[]} * @memberof Tree */ lad(id) { const log = this.logger.getLogger('Tree::ladderize2'); let newOrder = [id]; const node = this.getNode(id); node.reverseLadderized = !node.reverseLadderized; const childrenSorted = node.children.sort((a, b) => { const choice = this.getLeafsIds(b).length - this.getLeafsIds(a).length; return node.reverseLadderized ? choice : -choice; }); childrenSorted.forEach(childId => { newOrder = newOrder.concat(this.lad(childId)); }); return newOrder; } /** * Writes a newick.ITree object from a parent node. * * @private * @param {number} [parentId=-1] * @returns {newick.ITree} * @memberof Tree */ writeITreeObj(parentId = -1) { const log = this.logger.getLogger('Tree::writeITreeObj'); let rootId = parentId; if (parentId === -1) { const rootNode = this.getRootNode(); rootId = rootNode ? rootNode.id : 0; } const parentNode = this.getNode(rootId); const iTreeObj = { branchLength: parentNode.branchLength, children: [], name: parentNode.name, }; const nodeOrder = this.nodes.map(n => n.id); const copyOfChildren = parentNode.children.sort((a, b) => { return nodeOrder.indexOf(a) - nodeOrder.indexOf(b); }); log.debug(`parent: ${parentId} - child ${JSON.stringify(copyOfChildren)}`); // console.log(`parent: ${parentId} - child ${JSON.stringify(copyOfChildren)}`) copyOfChildren.forEach(childId => { iTreeObj.children.push(this.writeITreeObj(childId)); }); return iTreeObj; } /** * Builds the this.nodes array by scanning the treeObject * * @private * @param {newick.ITree} treeObject * @returns {TreeNode} * @memberof Tree */ makeTree(treeObject) { const treeNode = new TreeNode_1.TreeNode(treeObject.name, treeObject.branchLength, this.numberOfNodes); this.numberOfNodes++; this.nodes.push(treeNode); const children = []; if (treeObject.children.length) { treeObject.children.forEach((child) => { const newchild = this.makeTree(child); const parentId = treeNode.id; if (parentId !== null) { newchild.setParentNodeId(parentId); } const childId = newchild.id; if (childId !== null) { treeNode.children.push(childId); } }); } return treeNode; } /** * Set default policy to show only labels of leafs * * @private * @returns * @memberof Tree */ setDefaultLabelDisplay() { this.nodes.forEach(node => { node.setShowLabel(node.isLeaf()); }); return this; } } exports.Tree = Tree; //# sourceMappingURL=Tree.js.map