Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ultrametric option #444

Merged
merged 29 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3acf88a
ENH add function for computing ultrametric lengths
gwarmstrong Nov 10, 2020
51c9dd2
ENH make getUltraMetricLengths correspond to tree index
gwarmstrong Nov 10, 2020
9ddcb75
MAINT refactor layouts to take arbitrary length getter
gwarmstrong Nov 10, 2020
acb58c7
DOC include docstring argument for getLength
gwarmstrong Nov 10, 2020
cdf4426
STY make jsstye for ultrametric
gwarmstrong Nov 10, 2020
4f5fbf8
DOC add explanation of ultrametric algorithm
gwarmstrong Nov 10, 2020
4dd3bd6
Auto stash before rebase of "refs/heads/ultrametric-option"
gwarmstrong Nov 10, 2020
6f175cf
FIX lengthGetter pass into circularLayout
gwarmstrong Nov 10, 2020
8c36568
ENH add radio button and improve logic for branch length choice
gwarmstrong Nov 10, 2020
5b07e7e
FIX some comments
gwarmstrong Nov 12, 2020
61ada1e
ENH fix html stuff from marcus code review
gwarmstrong Nov 16, 2020
22093af
ENH add section for clade sorting
gwarmstrong Nov 16, 2020
9798a0a
ENH remove red from branch lengths warning
gwarmstrong Nov 16, 2020
27ce1f0
ENH change caps
gwarmstrong Nov 16, 2020
867eb96
ENH hide branch length warning when not in use
gwarmstrong Nov 16, 2020
cd13f59
ENH replace deteremine lenghs text
gwarmstrong Nov 16, 2020
4f94728
MAINT jsstyle for branch methods
gwarmstrong Nov 16, 2020
054de76
DOC add comment for _determineLengthGetter
gwarmstrong Nov 16, 2020
c4801e5
TST ensure ultrametric tree stays same
gwarmstrong Nov 16, 2020
fd037c4
MAINT style on test
gwarmstrong Nov 16, 2020
8020902
ENH clarify logic for determining branch lengths
gwarmstrong Nov 16, 2020
3656869
DOC more specific comments
gwarmstrong Nov 17, 2020
ae35490
INT change interface display of branch lengths
gwarmstrong Nov 17, 2020
e56dc7c
Update empress/support_files/js/side-panel-handler.js
gwarmstrong Nov 17, 2020
fdff5e4
MAINT remove ignoreLengths argument to layout functions
gwarmstrong Nov 17, 2020
32df93f
Merge branch 'ultrametric-option' of https://github.com/gwarmstrong/e…
gwarmstrong Nov 17, 2020
d492fed
MAINT tests style
gwarmstrong Nov 17, 2020
4b83692
Update empress/support_files/templates/side-panel.html
fedarko Nov 20, 2020
b9759ed
Update empress/support_files/js/side-panel-handler.js
fedarko Nov 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ define([
*/
this.ignoreLengths = false;

/**
* @type{String}
* Branch length method: one of "normal", "ignore", or "ultrametric"
*/
this.branchMethod = "normal";

/**
* @type{String}
* Leaf sorting method: one of "none", "ascending", or "descending"
Expand Down Expand Up @@ -357,14 +363,28 @@ define([
*/
Empress.prototype.getLayoutInfo = function () {
var data, i;
// set up length getter
var branchMethod = this.branchMethod;
var lengthGetter = LayoutsUtil.getLengthMethod(
branchMethod,
this._tree
);

// Rectangular
if (this._currentLayout === "Rectangular") {
data = LayoutsUtil.rectangularLayout(
this._tree,
4020,
4020,
this.ignoreLengths,
this.leafSorting
// since lengths for "ignoreLengths" are set by `lengthGetter`,
// we don't need (and should likely deprecate) the ignoreLengths
// option for the Layout functions since the layout function only
// needs to know lengths in order to layout a tree, it doesn't
// really need encapsulate all of the logic for determining
// what lengths it should lay out.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fair. My take on this is that I think figuring out lengths junk should be the job of the stuff in LayoutsUtils, just so we can avoid having logic in the main Empress class as much as we can (since it's already probs too big for its own good, and also to make testing easier).

Copy link
Member Author

@gwarmstrong gwarmstrong Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that passing a function for the lengths is nice because it opens the door for us to do some pretty cool stuff later, without having to make modifications to the LayoutsUtil module that would make those functions signatures more unmanageable.

E.g., say we wanted a feature where you could adjust branch lengths on the fly, you could define some function like

var userSetLengths = // some Object containing lengths that the user set explicitly
lengthGetter = function(i) {
    if (i in userSetLengths) {
        return userSetLengths[i];
    } else {
        return this._tree.length(i);
}

And it shouldn't really be LayoutsUtil's job to support this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we moved the logic from above into an Object of predefined length getters that lives in LayoutsUtil but leave the Layout functions extensible by functions.

E.g., this block would look something more like

lengthGetter = LayoutsUtil.lengthGetters[branchMethod];
if (this._currentLayout === "Rectangular") {
    data = LayoutsUtil.rectangularLayout(
        ...,
        lengthGetter
    )
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if this commit 8020902 resolve this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, sorry for taking so long to get back to you on this. Yeah I can definitely see the utility of defining this behavior with functions -- I think mainly I was just hesitant to add more stuff to Empress. I like 8020902's solution to this a lot; I think having this as a function that lives in LayoutsUtil (or at least somewhere that is outside of Empress and outside of each individual layout function in LayoutsUtil) gets us the best of both worlds.

I think this is basically resolved, IMO, although I do think that now that we're making this change we should probably bite the bullet and remove the ignoreLengths parameters. If it's possible it'd be nice to do that in this PR (just to avoid confusion with having these redundant arguments still existing in the codebase -- I don't think deprecation is necessary since AFAIK no one is really out here relying on Empress' APIs), but if you think it'd be too much of a pain I'm cool to open a new issue for it.

this.leafSorting,
undefined,
lengthGetter
);
this._yrscf = data.yScalingFactor;
for (i = 1; i <= this._tree.size; i++) {
Expand All @@ -384,8 +404,9 @@ define([
this._tree,
4020,
4020,
this.ignoreLengths,
this.leafSorting
this.leafSorting,
undefined,
lengthGetter
);
for (i = 1; i <= this._tree.size; i++) {
// remove old layout information
Expand All @@ -409,7 +430,8 @@ define([
this._tree,
4020,
4020,
this.ignoreLengths
undefined,
lengthGetter
);
for (i = 1; i <= this._tree.size; i++) {
// remove old layout information
Expand Down
173 changes: 155 additions & 18 deletions empress/support_files/js/layouts-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,128 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
return postOrderNodes;
}

/**
* Compute ultrametric lengths on a tree
*
* @param {BPTree} tree The tree to generate the lengths for.
*
* @returns {Object} Keys are the index position of the node in tree.
* Values are the length of the node in an ultrametric tree.
*/
function getUltrametricLengths(tree) {
var lengths = {};
var i;
var j;
var maxNodeToTipDistance = new Array(tree.size);
var depths = new Array(tree.size);
var nodeIndex;
var children;
var child;
/*
This loop is responsible for finding the maximum distance from
each node to its deepest tip.
*/
for (i = 1; i <= tree.size; i++) {
nodeIndex = tree.postorderselect(i);
if (tree.isleaf(nodeIndex)) {
maxNodeToTipDistance[nodeIndex] = 0;
} else {
var maxDist = 0;
children = tree.getChildren(nodeIndex);
for (j = 0; j < children.length; j++) {
child = children[j];
var childMaxLen =
maxNodeToTipDistance[child] + tree.length(child);
if (childMaxLen > maxDist) {
maxDist = childMaxLen;
}
}
maxNodeToTipDistance[nodeIndex] = maxDist;
}
}
/*
This loop is responsible for determining new branch lengths.
The lengths for intermediate nodes are effectively "stretched" until
their deepest descendant hits the deepest level in the whole tree.

E.g., if we are at the node represented by * in the tree below:

|--------------------------maxDistance-------------------------|
|--distanceAbove--| |---distanceBelow---|
|-length--| |-remainder-|
____
___________|
*__________| |_______
__________________| |__
|
|___________________________________________

then the branch will be extended so that its deepest tip has the
same depth as the deepest tip in the whole tree,
i.e., newLength = length + remainder
however, below it is equivalently calculated with
newLength = maxDistance - distanceAbove - distanceBelow

E.g.,
|--------------------------maxDistance-------------------------|
|--distanceAbove--| |---distanceBelow---|
|-length--||-remainder-|
____
___________|
*_______________________| |_______
__________________| |__
|
|___________________________________________

Repeated in a pre-order traversal, this will result in an ultrametric tree

*/
var maxDistance = maxNodeToTipDistance[tree.root()];
depths[tree.root()] = 0;
lengths[tree.root()] = tree.depth(tree.root());
for (i = 1; i <= tree.size; i++) {
nodeIndex = tree.preorderselect(i);
children = tree.getChildren(nodeIndex);
for (j = 0; j < children.length; j++) {
child = children[j];
var distanceAbove = depths[nodeIndex];
var distanceBelow = maxNodeToTipDistance[child];
lengths[child] = maxDistance - distanceAbove - distanceBelow;
depths[child] = distanceAbove + lengths[child];
}
}
return lengths;
}

/**
* Gets a method for determining branch lengths by name, parameterized on a tree.
*
* @param {String} methodName Method for determing branch lengths.
* One of ("ultrametric", "ignore", "normal").
* @param {BPTree} tree Tree that needs branch lengths determined.
* @returns {Function} A function that maps node indices to branch lengths.
*/
function getLengthMethod(methodName, tree) {
var lengthGetter;
if (methodName === "ultrametric") {
var ultraMetricLengths = getUltrametricLengths(tree);
lengthGetter = function (i) {
return ultraMetricLengths[i];
};
} else if (methodName === "ignore") {
lengthGetter = function (i) {
return 1;
};
} else if (methodName === "normal") {
lengthGetter = function (i) {
return tree.length(i);
};
} else {
throw "Invalid method: '" + methodName + "'.";
}
return lengthGetter;
}

/**
* Computes the "scale factor" for the circular / unrooted layouts.
*
Expand Down Expand Up @@ -137,12 +259,13 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
* displayed.
* @param {Float} height Height of the canvas where the tree will be
* displayed.
* @param {Boolean} ignoreLengths If falsy, branch lengths are used in the
* layout; otherwise, a uniform length of 1
* is used.
* @param {String} leafSorting See the getPostOrderNodes() docs above.
* @param {Boolean} normalize If true, then the tree will be scaled up to
* fill the bounds of width and height.
* @param {Function} lengthGetter Is a function that takes a single argument
* that corresponds to the index of a node in
* tree. Returns the length of the node at that
* index. Defaults to 'normal' method.
* @return {Object} Object with the following properties:
* -xCoords
* -yCoords
Expand All @@ -157,9 +280,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
tree,
width,
height,
ignoreLengths,
leafSorting,
normalize = true
normalize = true,
lengthGetter = null
) {
var maxWidth = 0;
var maxHeight = 0;
Expand All @@ -168,6 +291,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var yCoord = new Array(tree.size + 1).fill(0);
var highestChildYr = new Array(tree.size + 1);
var lowestChildYr = new Array(tree.size + 1);
if (lengthGetter === null) {
lengthGetter = getLengthMethod("normal", tree);
}

var postOrderNodes = getPostOrderNodes(tree, leafSorting);
var i;
Expand Down Expand Up @@ -203,7 +329,7 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var node = tree.postorder(prepos);
parent = tree.postorder(tree.parent(prepos));

var nodeLen = ignoreLengths ? 1 : tree.length(prepos);
var nodeLen = lengthGetter(prepos);
xCoord[node] = xCoord[parent] + nodeLen;
if (maxWidth < xCoord[node]) {
maxWidth = xCoord[node];
Expand Down Expand Up @@ -340,12 +466,13 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
* displayed.
* @param {Float} height Height of the canvas where the tree will be
* displayed.
* @param {Boolean} ignoreLengths If falsy, branch lengths are used in the
* layout; otherwise, a uniform length of 1
* is used.
* @param {String} leafSorting See the getPostOrderNodes() docs above.
* @param {Boolean} normalize If true, then the tree will be scaled up to
* fill the bounds of width and height.
* @param {Function} lengthGetter Is a function that takes a single argument
* that corresponds to the index of a node in
* tree. Returns the length of the node at that
* index. Defaults to 'normal' method.
* @return {Object} Object with the following properties:
* -x0, y0 ("starting point" x and y)
* -x1, y1 ("ending point" x and y)
Expand All @@ -363,9 +490,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
tree,
width,
height,
ignoreLengths,
leafSorting,
normalize = true
normalize = true,
lengthGetter = null
) {
// Set up arrays we're going to store the results in
var x0 = new Array(tree.size + 1).fill(0);
Expand Down Expand Up @@ -399,6 +526,10 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var maxY = 0,
minY = Number.POSITIVE_INFINITY;

if (lengthGetter === null) {
lengthGetter = getLengthMethod("normal", tree);
}

// Iterate over the tree in postorder, assigning angles
// Note that we skip the root (using "p < postOrderNodes.length - 1"),
// since the root's angle is irrelevant.
Expand Down Expand Up @@ -435,7 +566,7 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
var node = tree.postorder(prepos);
var parent = tree.postorder(tree.parent(prepos));

var nodeLen = ignoreLengths ? 1 : tree.length(prepos);
var nodeLen = lengthGetter(prepos);
radius[node] = radius[parent] + nodeLen;
}

Expand Down Expand Up @@ -572,11 +703,12 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
* displayed.
* @param {Float} height Height of the canvas where the tree will be
* displayed.
* @param {Boolean} ignoreLengths If falsy, branch lengths are used in the
* layout; otherwise, a uniform length of 1
* is used.
* @param {Boolean} normalize If true, then the tree will be scaled up to
* fill the bounds of width and height.
* @param {Function} lengthGetter Is a function that takes a single argument
* that corresponds to the index of a node in
* tree. Returns the length of the node at that
* index. Defaults to 'normal' method.
* @return {Object} Object with the following properties:
* -xCoords
* -yCoords
Expand All @@ -587,15 +719,18 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
tree,
width,
height,
ignoreLengths,
normalize = true
normalize = true,
lengthGetter = null
) {
var da = (2 * Math.PI) / tree.numleaves();
var x1Arr = new Array(tree.size + 1);
var x2Arr = new Array(tree.size + 1).fill(0);
var y1Arr = new Array(tree.size + 1);
var y2Arr = new Array(tree.size + 1).fill(0);
var aArr = new Array(tree.size + 1);
if (lengthGetter === null) {
lengthGetter = getLengthMethod("normal", tree);
}

var n = tree.postorderselect(tree.size);
var x1, y1, a;
Expand Down Expand Up @@ -628,7 +763,7 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
a += (tree.getNumTips(node) * da) / 2;

n = tree.postorderselect(node);
var nodeLen = ignoreLengths ? 1 : tree.length(n);
var nodeLen = lengthGetter(n);
x2 = x1 + nodeLen * Math.sin(a);
y2 = y1 + nodeLen * Math.cos(a);
x1Arr[node] = x1;
Expand Down Expand Up @@ -664,7 +799,9 @@ define(["underscore", "VectorOps", "util"], function (_, VectorOps, util) {
}

return {
getLengthMethod: getLengthMethod,
getPostOrderNodes: getPostOrderNodes,
getUltrametricLengths: getUltrametricLengths,
computeScaleFactor: computeScaleFactor,
rectangularLayout: rectangularLayout,
circularLayout: circularLayout,
Expand Down
Loading