Skip to content

Commit

Permalink
:sparkes: parse NHX, add nhx, beast, and user defined annotation writ…
Browse files Browse the repository at this point in the history
…ing to newick
  • Loading branch information
LeoFeatherstone committed Mar 14, 2024
1 parent 44b1235 commit 9050b31
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 66 deletions.
23 changes: 12 additions & 11 deletions docs/examples/test/examples.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/////////////////////////////////////////////////////

import { readNewick, readTreesFromPhyloXML, readTreesFromNewick, writeNewick, Tree } from '@phylojs';
import { beastAnnotation } from '../../../src/io/writers/newick';

describe('Examples', () => {
test('RTTR', () => {
Expand Down Expand Up @@ -115,7 +116,7 @@ describe('Examples', () => {
}
}
}
expect(writeNewick(tree, true)).not.toEqual(newick);
expect(writeNewick(tree, beastAnnotation)).not.toEqual(newick);
});

test('annotations subset', () => {
Expand All @@ -132,7 +133,7 @@ describe('Examples', () => {
}
}

expect(writeNewick(tree, true)).not.toEqual(newick);
expect(writeNewick(tree, beastAnnotation)).not.toEqual(newick);
});

test('multiple trees', () => {
Expand Down Expand Up @@ -284,7 +285,7 @@ describe('Examples', () => {
});

// Expect annotations in newick with `true` flag
expect(writeNewick(tree, true)).not.toBe(nwk)
expect(writeNewick(tree, beastAnnotation)).not.toBe(nwk)
})

test('Pruning', () => {
Expand All @@ -298,10 +299,10 @@ describe('Examples', () => {
.nodeList[node.parent.id] // Select node's parent by `id`
.removeChild(node) // Pruning step

console.log(`
Original Nwk: ${nwk}
Pruned Tree: ${writeNewick(tree)}
`)
// console.log(`
// Original Nwk: ${nwk}
// Pruned Tree: ${writeNewick(tree)}
// `)
// Returns
// Original Nwk: ((A,B),(C,D));
// Pruned Tree: (("C":0.0,"D":0.0):0.0):0.0;
Expand All @@ -324,10 +325,10 @@ describe('Examples', () => {
.addChild(node.copy()) // .copy() to ensure we don't bump into recursion issues
}

console.log(`
Original Nwk: ${nwk}
Pruned Tree: ${writeNewick(tree)}
`)
// console.log(`
// Original Nwk: ${nwk}
// Pruned Tree: ${writeNewick(tree)}
// `)
// Return
// Original Nwk: ((A,B),(C,D));
// Pruned Tree: (((("A":0.0,"B":0.0):0.0)"A":0.0,(((("A":0.0,"B":0.0):0.0)"A":0.0,"B":0.0):0.0)"B":0.0):0.0,("C":0.0,"D":0.0):0.0):0.0;
Expand Down
32 changes: 19 additions & 13 deletions src/io/readers/newick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function kn_add_node(str: string, l: number, nodes: Node[], x: number) {
//tree.error |= 4; // <-- TODO: add unfinished annotation error
break;
}
z.annotation = parseAnnotations(str.slice(meta_beg + 1, i))
z.annotation = parseNewickAnnotations(str.slice(meta_beg + 1, i))
} else if (c == ':') { // Parse branch length
if (end == 0) end = i;
for (var j = ++i; i < str.length; ++i) {
Expand All @@ -127,7 +127,7 @@ function kn_add_node(str: string, l: number, nodes: Node[], x: number) {
if (end > beg) {
label = str.slice(beg, end)
.replace(/;$/g, "")
.replace(/^"|"$/g, "")
.replace(/^"|"$/g, "") // remove quotes
.replace(/^'|'$/g, "") // remove quotes

if(label.includes('#')) { // Hybrid case
Expand All @@ -144,10 +144,10 @@ function kn_add_node(str: string, l: number, nodes: Node[], x: number) {
}

/**
* Function parses hybrid id labels. Expects hybrid labels to contain #.
* Following Cardona et al. 2008, (https://doi.org/10.1186/1471-2105-9-532),
* function expects unparsed labels to be of the form [label]#[type]i[:branch-length]
* where '#' and i, the hybrid ID are mandatory. PhyloJS ignores the type annotation
* Function parses hybrid id labels, which are assumed to contain '#'.
* Following Cardona et al. 2008, (https://doi.org/10.1186/1471-2105-9-532).
* Function expects unparsed labels to be of the form [label]#[type]i[:branch-length]
* where '#' and i (the hybrid node ID) are mandatory. PhyloJS ignores the type annotation
* (H for hybridisation, LGT for lateral gene transfer, R for recombination) and extracts only
* the label and hybridID, following icyTREE.
* @param {string} label
Expand All @@ -161,7 +161,7 @@ export function parseHybridLabels(label: string) {

parsed['label'] = splitLabel[0].length > 0 ? splitLabel[0] : undefined;

let hybridID = Number(splitLabel[1].replace(/H|LGT|R/g, ""));
let hybridID = Number(splitLabel[1].replace(/H|LGT|R/g, "")); // remove hybridisation types
if (Number.isInteger(hybridID)) { // hybridID must be integer
parsed['hybridID'] = hybridID
} else {
Expand All @@ -173,20 +173,26 @@ export function parseHybridLabels(label: string) {

/**
* Parses newick annotations to object for storage
* in `Tree` object.
* in `Tree` object. Parses annotations in BEAST-type format [&...]
* and in NHX type [&&NHX:..], such as from RevBayes. Annotations in
* arrays are expected to be stored in braces, and separaged by ',' or ':'.
* For example ...Type={Blue,Res} or ...Type={Blue:Red}
* @param {string} annotations
* @returns {any}
*/
export function parseAnnotations(annotations: string) {
export function parseNewickAnnotations(annotations: string) {

// Remove the '&' at the start if it exists
if (annotations.startsWith('&')) {
// Remove the '&' at the start or '&&NHX' in the case of NHX
if (annotations.startsWith('&&NHX:')) {
annotations = annotations.slice(6);
}
else if (annotations.startsWith('&')) {
annotations = annotations.slice(1);
}

const annotation_object: any = {};

const pairs = annotations.split(/,(?![^{]*\})/g);
const pairs = annotations.split(/[,:](?![^{]*\})/g); // Split on all ',' and ':' not in braces '{}'

pairs.forEach(pair => {
const keyValue: string[] = pair.split('=');
Expand All @@ -198,7 +204,7 @@ export function parseAnnotations(annotations: string) {

annotation_object[key] = value
.replace(/{|}/g, '')
.split(','); // TODO: Split on ':' and parse NHX annotations
.split(/,|:/g);

} else {
annotation_object[key] = value
Expand Down
127 changes: 102 additions & 25 deletions src/io/writers/newick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,41 @@ import { Node, Tree } from '../../';
/**
* Writes tree in .newick format. Undefined branch lengths set to 0.
* @param {tree} tree The tree to write
* @param {boolean} annotate Boolean to include annotations. Default is false.
* @param {annotationWriter} string Function parsing node annotations to string. Defaults to empty string - no annotation case. Can be user Defined or use in-build beastAnnotations or nhxAnnotations
* @returns {string}
*/
export function writeNewick(tree: Tree, annotate = false): string {

export function writeNewick(
tree: Tree,
annotationWriter: (annotation: typeof Node.prototype.annotation) => string = annotation => ''
): string {

let newickStr = '';

if (tree.root !== undefined)
newickStr += newickRecurse(tree.root, annotate) + ';';
newickStr += newickRecurse(tree.root, annotationWriter) + ';';

return newickStr;
}

export function newickRecurse(node: Node, annotate: boolean): string {
/**
* Recurses through tree, writing building up nwk string as it foes
* @param {node} Node
* @param {annotationWriter} string Function parsing Node.annotation to string. Defaults to empty string, not writing annotations. Can be user Defined or use in-build beastAnnotations or nhxAnnotations
* @returns {string}
*/
export function newickRecurse(
node: Node,
annotationWriter: (annotation: typeof Node.prototype.annotation) => string = annotation => ''
): string {

let res = '';

if (!node.isLeaf()) {
res += '(';
for (let i = 0; i < node.children.length; i++) {
if (i > 0) res += ',';
res += newickRecurse(node.children[i], annotate);
res += newickRecurse(node.children[i], annotationWriter);
}
res += ')';
}
Expand All @@ -33,30 +50,90 @@ export function newickRecurse(node: Node, annotate: boolean): string {
} else if (node.label == undefined && node.hybridID !== undefined) {
res += `#${node.hybridID}`;
}

if (annotate) {
const keys = Object.keys(node.annotation);
if (keys.length > 0) {
res += '[&';
for (let idx = 0; idx < keys.length; idx++) {
const key = keys[idx];

if (idx > 0) res += ',';
res += `${key}=`;
const value = node.annotation[key];
if (Array.isArray(value)) {
res += `{${String(value.join(','))}}`; // Convert the array to a comma-separated string
} else {
res += `${String(value)}`; // Explicitly convert the value to a string
}
}
res += ']';
}
}

res += annotationWriter(node.annotation)

if (node.branchLength !== undefined) {
node.branchLength == 0 ? (res += ':0.0') : (res += `:${node.branchLength}`);
}

return res;
}

/**
* Writes node annotations to a string in the syle of BEAST.
* Eg: [&Type=A,Cols={Red,Blue}]
* @param {annotation} typeof Node.prototype.annotation
* @returns {string}
*/
export function beastAnnotation(
annotation: typeof Node.prototype.annotation
): string {

let res = '';

if (annotation !== undefined) {

const keys = Object.keys(annotation);

if (keys.length > 0) {
res += '[&';

for (let i = 0; i < keys.length; i++) {
const key = keys[i];

if (i > 0) res += ',';
res += `${key}=`;
const value = annotation[key];

if (Array.isArray(value)) {
res += `{${String(value.join(','))}}`; // Convert the array to a comma-separated string
} else {
res += `${String(value)}`; // Explicitly convert the value to a string
}
}

res += ']'
}
}

return res;

}

/**
* Writes node annotations to a string in the syle of NHX.
* Eg: [&&NHX:Type=A:Col=Red]. NHX does not appear to support
* array date for annotations. Please get in touch if it does!
* @param {annotation} typeof Node.prototype.annotation
* @returns {string}
*/
export function nhxAnnotation(
annotation: typeof Node.prototype.annotation
): string {

let res = '';

if (annotation !== undefined) {
const keys = Object.keys(annotation);

if (keys.length > 0) {
res += '[&&NHX:';

for (let i = 0; i < keys.length; i++) {
const key = keys[i];

if (i > 0) res += ':';
res += `${key}=`;

const value = annotation[key];
res += `${String(value)}`; // Explicitly convert the value to a string
}

res += ']'
}
}

return res;

}
9 changes: 6 additions & 3 deletions src/io/writers/nexus.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Tree } from '../../';
import { Tree, Node } from '../../';
import { newickRecurse } from './newick';

/** Writes tree in .nexus format. Undefined branch lengths set to 0.
* @param {tree} tree The tree to write
* @param {boolean} annotate Boolean to include annotations. Default is true.
*/
export function writeNexus(tree: Tree, annotate = true): string {
export function writeNexus(
tree: Tree,
annotationWriter: (annotation: typeof Node.prototype.annotation) => string = annotation => ''
): string {
let nexusStr = '#NEXUS\n\nbegin trees;\n';

if (tree.root !== undefined)
nexusStr +=
`\ttree tree_1 = [&R] ${newickRecurse(tree.root, annotate)};` + '\n';
`\ttree tree_1 = [&R] ${newickRecurse(tree.root, annotationWriter)};` + '\n';

nexusStr += 'end;';

Expand Down
1 change: 1 addition & 0 deletions test/data/testNHX.nhx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(((ADH2:0.1[&&NHX:S=human],ADH1:0.11[&&NHX:S=human]):0.05[&&NHX:S=primates:D=Y:B=100],ADHY:0.1[&&NHX:S=nematode],ADHX:0.12[&&NHX:S=insect]):0.1[&&NHX:S=metazoa:D=N],(ADH4:0.09[&&NHX:S=yeast],ADH3:0.13[&&NHX:S=yeast],ADH2:0.12[&&NHX:S=yeast],ADH1:0.11[&&NHX:S=yeast]):0.1[&&NHX:S=Fungi])[&&NHX:D=N];
Loading

0 comments on commit 9050b31

Please sign in to comment.