phylogician-ts
Version:
Module to read, manipulate and write phylogenetic trees. Written in TypeScript
624 lines • 21.3 kB
JavaScript
"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