tree almost_eq#133
Conversation
|
Perhaps we want to return a |
|
Had to change a Newick string in a test. See #134 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #133 +/- ##
===========================================
+ Coverage 96.32% 96.67% +0.35%
===========================================
Files 32 43 +11
Lines 4355 5029 +674
===========================================
+ Hits 4195 4862 +667
- Misses 160 167 +7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR implements an almost_eq method for phylogenetic trees to compare them while allowing small differences in branch lengths (controlled by an epsilon parameter). The method ignores internal representations and node indices, focusing on tree topology and branch lengths. This addresses issue #78 by providing an approximate equality comparison without implementing the Eq trait (which would be problematic due to f64 fields).
Changes:
- Added
almost_eqmethod toTreestruct that compares trees by traversing from each leaf to the root - Added comprehensive test coverage with 10 new test cases covering various scenarios
- Minor fixes including whitespace correction in a test string, test function rename, and added documentation comment
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| phylo/src/tree/mod.rs | Implements the almost_eq method with topology and branch length comparison, adds log::warn import, adds documentation comment for is_subtree, and includes a TODO comment |
| phylo/src/tree/tests.rs | Adds 10 comprehensive test cases for almost_eq, fixes whitespace in a test Newick string, and demonstrates usage of the new method |
| phylo/src/asr/tests.rs | Renames test function from reconstruct_ancestral_seqs_n_seqs_not_same_as_leaves to asr_n_seqs_not_same_as_leaves for brevity |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| postorder: Vec<NodeIdx>, | ||
| preorder: Vec<NodeIdx>, | ||
| leaf_ids: Vec<String>, | ||
| // TODO: what is meant by this? |
There was a problem hiding this comment.
The comment "TODO: what is meant by this?" should be removed or resolved before merging. This TODO appears to be unrelated to the PR's purpose and adds confusion to the codebase. If the meaning of the complete field is unclear, it should be documented properly rather than leaving a TODO comment.
| // TODO: what is meant by this? | |
| /// Indicates whether the tree has been fully constructed and its traversal | |
| /// orders and metadata are up to date. |
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
junniest
left a comment
There was a problem hiding this comment.
This is a much more complex problem (comparing trees) than it looked on the surface :D I think we still need to think how to tackle it consistently and properly.
| postorder: Vec<NodeIdx>, | ||
| preorder: Vec<NodeIdx>, | ||
| leaf_ids: Vec<String>, | ||
| // TODO: what is meant by this? |
There was a problem hiding this comment.
hahaha I agree with copilot on this, and its suggestion is correct. This was a field meant for the situation when the tree is being constructed and not nodes are all filled in, traversals are not initialised, etc. It's not a great way to handle that and I will try to refactor this out so that there's no way to get an incomplete tree in the first place. See #121.
| partitions | ||
| } | ||
|
|
||
| /// Returns true if `query` is in the subtree rooted at `node`. |
There was a problem hiding this comment.
This could actually be a pub method, I think. In that case, it would be nice to also have a docstring example. If you don't have the time to change it I will do that in #121.
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if the ids of the leaves are not unique; |
There was a problem hiding this comment.
I don't think that equality should check whether IDs are unique in each tree, since this is a different kind of check. It's a check on tree validity rather than on equality. I would say that you should assume that if the trees have already been created by this point (and they have), the trees themselves will be valid.
Actually, funnily enough, this made me add a new test for the newick parser and it does not fail when there are duplicate ids. I will fix that asap (#146)
| fn newick_parse_whitespace() { | ||
| let trees = from_newick( | ||
| " ( ((( (A:1 , B : 1.0) \n \n F:1,C:2.0 )G:1,D:3)H:+1.0 , E:4) I:1)\n;\n ", | ||
| " ( ((( (A:1 , B: 1.0) \n \n F:1,C:2.0 )G:1,D:3)H:+1.0 , E:4) I:1)\n;\n ", |
There was a problem hiding this comment.
I am a bit confused here. Why did you end up removing the whitespace? At the moment (at least for me) this test already works just fine (the parser does not include whitespace in the name).
| return false; | ||
| } | ||
| if self.n != other.n | ||
| || self.complete != other.complete |
There was a problem hiding this comment.
I will remove this flag, so you can remove this check. A created tree should always be complete and valid.
The other thing is that self.n must be equal to self.nodes.len(), so this will be a duplicate check which can also be removed.
There was a problem hiding this comment.
No its not. self.n is the number of leaves not nodes. If we assume that the tree struct is valid, is rooted and binary, then if the ns are equal it follows that the nodes.length also match.
| return false; | ||
| } | ||
| // checking the branch lengths | ||
| if n1.blen.is_nan() || n2.blen.is_nan() || (n1.blen - n2.blen).abs() > epsilon { |
There was a problem hiding this comment.
Here again, any NaNs with always fail the last comparison, so you only need that one for this check to work.
| } | ||
| } | ||
|
|
||
| // not comparing Tree.dirty as it is crate internal |
There was a problem hiding this comment.
Uuuugh, I think the dirty bitset should not be a part of the tree, now that I think about it... But I am not sure what would be a better place for it.
| None => None, | ||
| }; | ||
| } | ||
| if n2_option.is_some() { |
There was a problem hiding this comment.
And the trees need to be rooted the exact same way, which can be both good and bad. I am not sure if it's worth trying to compare trees with different rootings, but if we expect the rooting to be the same I think it should be said in the doctring.
| /// Compares two trees for approximate equality, allowing for small differences in branch | ||
| /// lengths. This comparison does not care about the order of nodes in the internal representation | ||
| /// or correspondence of [`Node`]s and [`NodeIdx`]s. The order of children of a node is ignored, | ||
| /// i.e., they are compared as sets. It returns `false` in the case of `NaN` branch lengths. |
There was a problem hiding this comment.
The last sentence isn't necessary, imo (see my comments about NaNs below). NaNs should not appear in any case, but if they do the whole structure is invalid anyway.
| } | ||
|
|
||
| #[test] | ||
| fn almost_eq_trees_with_wrong_internal_id() { |
There was a problem hiding this comment.
This is curious. Technically, if we have the same structure (same leaf clustering) the internal node names shouldn't matter. I am not sure how to make that into a rule here and whether we should bother with it at all, but it might pop up eventually.
Implemented a
almost_eqmethod to compare trees ignoring internal representations.Closes #78. As we not want to directly implement
Eqsince we are still comparingf64fields for whichEqis not implemented.