Skip to content

Commit

Permalink
Correctly handle reversions & multiple mutations
Browse files Browse the repository at this point in the history
The function `collectMutations` now doesn't report reversions (where the tip state = the ancestral state) and combines multiple mutations (e.g. A->B->C is now A->C rather than two separate mutations).
  • Loading branch information
jameshadfield committed Mar 8, 2021
1 parent 9e3e4c3 commit ddaff9e
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/components/tree/infoPanels/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const MutationTable = ({mutations}) => {
Object.entries(mutations)
.sort(geneSortFn)
.map(([gene, muts], index) => (
item(index === 0 ? "Mutations from root" : "", gene + ": " + [...muts].sort(mutSortFn).join(", ").substring(0, 200))
item(index === 0 ? "Mutations from root" : "", gene + ": " + muts.sort(mutSortFn).join(", ").substring(0, 200))
))
);
};
Expand Down
25 changes: 21 additions & 4 deletions src/util/treeMiscHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ export function collectGenotypeStates(nodes) {

/**
* Collect mutations from node `fromNode` to the root.
* We may want to expand this funciton to take a second argument as the "stopping node"
* Reversions (e.g. root -> A<pos>B -> B<pos>A -> fromNode) will not be reported
* Multiple mutations (e.g. root -> A<pos>B -> B<pos>C -> fromNode) will be represented as A<pos>C
* We may want to expand this function to take a second argument as the "stopping node"
* @param {TreeNode} fromNode
*/
export const collectMutations = (fromNode, include_nuc=false) => {
Expand All @@ -112,8 +114,15 @@ export const collectMutations = (fromNode, include_nuc=false) => {
if (n.branch_attrs && n.branch_attrs.mutations && Object.keys(n.branch_attrs.mutations).length) {
Object.entries(n.branch_attrs.mutations).forEach(([gene, muts]) => {
if ((gene === "nuc" && include_nuc) || gene !== "nuc") {
if (!mutations[gene]) mutations[gene] = new Set();
muts.forEach((m) => mutations[gene].add(m));
if (!mutations[gene]) mutations[gene] = {};
muts.forEach((m) => {
const [from, pos, to] = [m.slice(0, 1), m.slice(1, -1), m.slice(-1)]; // note: `pos` is a string
if (mutations[gene][pos]) {
mutations[gene][pos][0] = from; // mutation already seen => update ancestral state.
} else {
mutations[gene][pos] = [from, to];
}
});
}
});
}
Expand All @@ -124,6 +133,14 @@ export const collectMutations = (fromNode, include_nuc=false) => {
}
};
walk(fromNode);

// update structure to be returned
Object.keys(mutations).forEach((gene) => {
mutations[gene] = Object.entries(mutations[gene])
.map(([pos, [from, to]]) => {
if (from===to) return undefined; // reversion to ancestral (root) state
return `${from}${pos}${to}`;
})
.filter((value) => !!value);
});
return mutations;
};
55 changes: 55 additions & 0 deletions test/treeHelpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { collectMutations } from "../src/util/treeMiscHelpers";
import { treeJsonToState } from "../src/util/treeJsonProcessing";

/**
* `dummyTree` is a simple tree with two tips: tipX and tipY
* root to tipX mutations:
* single mutation at position 100 in gene "GENE" of A->B
* a reversion of C->D->C at position 200
* multiple mutations at 300 E->F->G
* root to tipY mutations:
* ["A100B", "C200D", "E300F"]
*/
const dummyTree = treeJsonToState({
name: "ROOT",
children: [
{
name: "node1",
branch_attrs: {mutations: {GENE: ["A100B", "C200D", "E300F"]}},
children: [
{ // start 1st child of node1
name: "node1.1",
branch_attrs: {mutations: {GENE: ["D200C", "F300G"]}},
children: [
{
name: "tipX"
}
]
},
{ // start 2nd child of node1
name: "tipY"
}
]
}
]
}).nodes;


test("Tip->root mutations are correctly parsed", () => {
const tipXMutations = collectMutations(getNodeByName(dummyTree, "tipX")).GENE;
const tipYMutations = collectMutations(getNodeByName(dummyTree, "tipY")).GENE;
expect(tipXMutations.sort())
.toEqual(["A100B", "E300G"].sort()); // note that pos 200 (reversion) has no mutations here
expect(tipYMutations.sort())
.toEqual(["A100B", "C200D", "E300F"].sort());
});

function getNodeByName(tree, name) {
let namedNode;
const recurse = (node) => {
if (node.name === name) namedNode = node;
else if (node.children) node.children.forEach((n) => recurse(n));
};
recurse(tree[0]);
return namedNode;
}

0 comments on commit ddaff9e

Please sign in to comment.