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

Ultrametric option #444

merged 29 commits into from
Nov 20, 2020

Conversation

gwarmstrong
Copy link
Member

@gibsramen and I threw together a method for rendering trees as ultrametric (see #279 )

Here are some screenshots of the ultrametric trees:
Rectangular
Screen Shot 2020-11-09 at 4 26 57 PM
Circular
Screen Shot 2020-11-09 at 4 32 36 PM
Unrooted
Screen Shot 2020-11-09 at 6 54 35 PM

I am unsure why the unrooted layout does not appear to have equal root to tip distances is unclear to me and I could use some guidance here.

This method relies on the algorithm as described in comments and below:

         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

Questions for reviewers

  • Do you know why the unrooted layout renders as it does?
  • I did not spend a ton of time thinking out how this should be exposed to the user. Currently it is a checkbox under the layout page, and if it is check the "Ignore branch lengths" checkbox is ignored. How do you think we should code the interaction between these two functionalities?

@emperor-helper
Copy link

The following artifacts were built for this PR: empire-biplot.qzv, empire.qzv, empress-tree.qzv, just-fm.qzv, plain.qzv

@fedarko
Copy link
Collaborator

fedarko commented Nov 10, 2020

This looks awesome! Will go through things in depth tomorrow.

I did not spend a ton of time thinking out how this should be exposed to the user. Currently it is a checkbox under the layout page, and if it is check the "Ignore branch lengths" checkbox is ignored. How do you think we should code the interaction between these two functionalities?

If these options are mutually exclusive, I think using radio buttons for this might make sense for now. Alternately, we could set up something where clicking the "Make tree ultrametric?" checkbox (analogous to the "Draw barplots?" checkbox) hides the "ignore lengths" stuff and opens up a <div> with options for which ultrametric method to use. (But since there's just one ultrametric method supported -- for now at least -- I think just using radio buttons would be the easiest.)

@kwcantrell
Copy link
Collaborator

Thanks @gwarmstrong and @gibsramen, this is really cool.

Do you know why the unrooted layout renders as it does?

I'll be able to explain this better during our meeting today. Basically, the above algorithm works with circular/rectangular because branch lengths will always travel from a common point/direction. For example, in rectangular layout, branch lengths will always stretch the tree in the +x direction. Similarly in circular layout, all branch lengths will project out from a common point. This "standardizes" branch lengths. For example, say we have two sets of branches a->b and c->d, 'a' has a length of 1, 'b' has a length of 5, and both 'c' and 'd' have a length of 3. This means 'b' and 'd' will have a "total aggregate" length of 6. Thus, in rectangular layout, the x coordinate of 'b' and 'd' will be 6 and in circular layout, the l2 distance of 'b' and 'd' from the root will be equivalent. Thus, by setting the "total aggregate" length of all tips to be the same will make the tree appear ultrametric. However, in unrooted layout, you can essentially think the direction a node travels is "random". Basically, just because two nodes have the same "total aggregate" length, not necessarily mean they will be the same distance from the root (i.e. the l2 distance of 'b' and 'd' from the root will not be equivalent).

tldr in unrooted making the "total aggregate" length of all tips equivalent does not guarantee they will be the same distance from the root.

@gwarmstrong
Copy link
Member Author

tldr in unrooted making the "total aggregate" length of all tips equivalent does not guarantee they will be the same distance from the root.

Ah okay, I think my confusion can be explained away as a misunderstanding of where the root was. So to clarify, the root of the tree in the unrooted layout is probably somewhere around here?
Screen Shot 2020-11-09 at 6 54 35 PM

@kwcantrell
Copy link
Collaborator

Ah okay, I think my confusion can be explained away as a misunderstanding of where the root was. So to clarify, the root of the tree in the unrooted layout is probably somewhere around here?

Correct, so the "aggregate" branch lengths of the tips are the same however the l2 distances are different.

Copy link
Collaborator

@fedarko fedarko left a comment

Choose a reason for hiding this comment

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

Thanks @gwarmstrong and @gibsramen! This looks solid (and the algorithm seems to me like a really elegant way of doing this). I have some suggestions, but most of them are cosmetic things -- this is going to be super useful.

empress/support_files/templates/side-panel.html Outdated Show resolved Hide resolved
empress/support_files/templates/side-panel.html Outdated Show resolved Hide resolved
empress/support_files/templates/side-panel.html Outdated Show resolved Hide resolved
empress/support_files/templates/side-panel.html Outdated Show resolved Hide resolved
empress/support_files/templates/side-panel.html Outdated Show resolved Hide resolved
empress/support_files/js/empress.js Outdated Show resolved Hide resolved
// 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.

empress/support_files/js/layouts-util.js Outdated Show resolved Hide resolved
empress/support_files/js/layouts-util.js Outdated Show resolved Hide resolved
empress/support_files/js/layouts-util.js Outdated Show resolved Hide resolved
Co-authored-by: Marcus Fedarko <[email protected]>
@gibsramen
Copy link
Collaborator

Profiled the time to ultrametrify (officially coining this term) the EMP tree using performance.now(). Looks like, on average, the computation time of the ultrametric lengths is around 500+-50ms. This was for both rectangular & circular layouts.

Sandwiched this block of code per @gwarmstrong with time calls and took the difference.

Copy link
Collaborator

@fedarko fedarko left a comment

Choose a reason for hiding this comment

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

Some requested changes; overall this is looking great. Exciting stuff :D 🌳 🌲 🎋

// 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.

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.

tests/test-layouts-util.js Outdated Show resolved Hide resolved
empress/support_files/js/side-panel-handler.js Outdated Show resolved Hide resolved
}
}
// we want to make sure empress knows whether or not to ignore lengths
// at the moment this is most critical for empress._collapseClade
Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, uh, hm. So like I can see in the demo that clade collapsing does definitely work with ultrametric trees but... uh, I am not 100% sure why it's working??? I remember we had to jump through a few hoops to get ignore lengths to play nicely with clade collapsing.

Maybe part of it is that, like... since all of the tips wind up at the same "displacement", maybe whatever we set as the "deepest" node of a clade using the length info (here-ish) doesn't matter? Weird.

In any case it would probably be good to dig into this a bit at this morning's meeting. At the very least we should document why things are working for the sake of future us :P

empress/support_files/js/side-panel-handler.js Outdated Show resolved Hide resolved
empress/support_files/js/side-panel-handler.js Outdated Show resolved Hide resolved
empress/support_files/js/layouts-util.js Outdated Show resolved Hide resolved
@gwarmstrong gwarmstrong requested a review from fedarko November 18, 2020 20:21
Copy link
Collaborator

@fedarko fedarko left a comment

Choose a reason for hiding this comment

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

Two very, very small comments (one for dev documentation and one for user documentation). Basically good to merge as is.

empress/support_files/js/side-panel-handler.js Outdated Show resolved Hide resolved
empress/support_files/templates/side-panel.html Outdated Show resolved Hide resolved
@fedarko fedarko merged commit 9235709 into biocore:master Nov 20, 2020
@fedarko
Copy link
Collaborator

fedarko commented Nov 20, 2020

In the interest of getting a release out today, @ElDeveloper and I decided to just make the two small changes and merge this in. Thanks so much @gwarmstrong and @gibsramen for your work on this!!! 📏 🌲

@gwarmstrong
Copy link
Member Author

Thanks @fedarko !!

@ElDeveloper
Copy link
Member

📏 🎄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants